一、帶動畫刷新的小部件
比如AnimatedContainer,這一類小部件都以Animated開頭,示例如下:
AnimatedContainer(
duration: const Duration(seconds: 3),
width: 300,
height: 300,
color: Colors.blue,
)
將上面的顏色換成其他顏色然后熱更新一下,就會有顏色過渡動畫。這一類的動畫小部件只對自己的屬性起到動畫效果,他不能控制自己的child也執(zhí)行動畫。
差值器:curve
默認(rèn)是Curves.linear。他是水平勻速運(yùn)動。Curves里面還有很多,可以都看看。做一個(gè)勻速下落盒子的動畫,如下:
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: AnimatedPadding(
curve: Curves.linear,
duration: Duration(
seconds: 2
),
padding: EdgeInsets.only(top: 0),
child: Container(
width: 300,
height: 300,
color: Colors.blue,
),
),
),
);
}
二、兩種小部件來回切換動畫
上面的動畫提到了child切換時(shí),沒有動畫,那么我們要做到這樣的功能需要AnimatedSwitcher來實(shí)現(xiàn)。如下:
AnimatedSwitcher(
duration: Duration(seconds: 2),
child: Center(child: CircularProgressIndicator(),),//Image.network("https://www.baidu.com/img/flexible/logo/pc/result.png")
)
當(dāng)吧AnimatedSwitcher的child從Center(child: CircularProgressIndicator(),)換成Image,就會有過渡動畫。
值得一提的是,如果是Text只是換掉里面的文案,那么不會有動畫。比如:
AnimatedSwitcher(
duration: Duration(seconds: 2),
child: Center(child: Text("HI",style: TextStyle(fontSize: 41),),),
)
//把文案換成hello。
AnimatedSwitcher(
duration: Duration(seconds: 2),
child: Center(child: Text("HELLO",style: TextStyle(fontSize: 41),),),
)
為什么沒有動畫?因?yàn)橹皇歉奈陌?,flutter不會認(rèn)為是換了小部件,所以AnimatedSwitcher也不會執(zhí)行動畫,那么要想執(zhí)行動畫就要讓flutter覺得小部件改變了。就要用到key。
在Flutter中,當(dāng)重新build時(shí),會更新widget和element。
Flutter的UI是通過構(gòu)建widget樹來實(shí)現(xiàn)的,widget是不可變的,當(dāng)需要更新UI時(shí),會創(chuàng)建一個(gè)新的widget,并使用diff算法比較新舊widget的差異,然后更新element樹。如果widget樹中的某個(gè)widget的屬性發(fā)生了變化,或者父級widget調(diào)用了setState方法,那么Flutter會重新build該widget及其子樹。
在重新build時(shí),F(xiàn)lutter會根據(jù)widget的差異更新element樹。如果新舊widget的類型相同,F(xiàn)lutter會復(fù)用element,只更新相關(guān)的屬性值。如果新舊widget的類型不同,則會銷毀舊的element,并創(chuàng)建一個(gè)新的element。如果當(dāng)類型相同,key不同,flutter也會重新構(gòu)建widget。
所以把上面的代碼改成下面的加個(gè)key就行:
AnimatedSwitcher(
duration: const Duration(seconds: 2),
child: Center(child: Text(key: UniqueKey(),"HI",style: const TextStyle(fontSize: 41),),),
)
//把文案換成hello。
AnimatedSwitcher(
duration: const Duration(seconds: 2),
child: Center(child: Text(key: UniqueKey(),"HELLO",style: const TextStyle(fontSize: 41),),),
)
當(dāng)然AnimatedSwitcher還可以選擇動畫類型,用transitionBuilder屬性即可。默認(rèn)用的是fade也就是透明度變化效果。如下:
AnimatedSwitcher(
transitionBuilder: (child,animation){
return FadeTransition(opacity: animation,child: child,)
},
duration: Duration(seconds: 2),
child: Center(child: Text(key: ValueKey("Hi"),"Hi",style: TextStyle(fontSize: 41),),),
)
我們也能改成放大縮小動畫或者選擇,如下:改成放大縮小
AnimatedSwitcher(
transitionBuilder: (child,animation){
return ScaleTransition(scale: animation,child: child,)
},
duration: Duration(seconds: 2),
child: Center(child: Text(key: ValueKey("Hi"),"Hi",style: TextStyle(fontSize: 41),),),
)
那么如果我們既需要透明動畫也需要放大縮小動畫怎么辦?那就套娃。如下:
AnimatedSwitcher(
transitionBuilder: (child,animation){
return ScaleTransition(scale: animation,
child: FadeTransition(
opacity: animation,
child: child,
),);
},
duration: Duration(seconds: 2),
child: Center(child: Text(key: ValueKey("Hi"),"Hi",style: TextStyle(fontSize: 41),),),
)
三、補(bǔ)間動畫
和安卓一樣,flutter也有補(bǔ)間動畫,其實(shí)可以理解成,一個(gè)屬性設(shè)置在一個(gè)返回變化,然后一變化就給小部件賦值,達(dá)到動畫效果。比如下面的透明度動畫。如下:
Scaffold(
body: SafeArea(
child: Center(
child: TweenAnimationBuilder(
duration: Duration(seconds: 2),
builder: (BuildContext context, value, child) {
return Opacity(
opacity: value,
child: Container(
width: 300,
height: 300,
color: Colors.blue,
),
);
},
tween: Tween(begin: 1.0,end: 0.0),
),
),
),
)
上面的代碼可以實(shí)現(xiàn)AnimatedOpacity的動畫效果,而且好掌握。就是代碼長了些。有沒有發(fā)現(xiàn)builder里面的child沒有用到?這個(gè)是優(yōu)化用的。還沒到時(shí)機(jī),暫時(shí)不記錄。
補(bǔ)間動畫經(jīng)常和Transform一起連用。下面來一個(gè)按按鈕讓小部件來回放大縮小的動畫。如下:
class Test1State extends State<Test1> {
var _begin = true;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: TweenAnimationBuilder(
duration: Duration(seconds: 2),
builder: (BuildContext context, value, child) {
return Container(
width: 300,
height: 300,
color: Colors.blue,
child: Center(
child: Transform.scale(
scale: value,
child: Text("HI",style: TextStyle(fontSize: 50),),
),
),
);
},
tween: Tween(begin: 1.0,end: _begin ? 2.0 : 1.0),
),
),
),
floatingActionButton: FloatingActionButton(onPressed: (){
setState(() {
_begin = !_begin;
});
},),
);
}
}
Transform還有旋轉(zhuǎn),平移等,都可以看看。
四、顯示動畫
顯示動畫帶控制器,能更好的方便我們停止開始等。下面做一個(gè)不停旋轉(zhuǎn)的圖標(biāo)動畫,如下:
class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
AnimationController? _controller = null;
bool _start = false;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
}
@override
void dispose() {
super.dispose();
_controller?.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Container(
width: 300,
height: 300,
color: Colors.blue,
child: Center(
child: RotationTransition(
turns: _controller!!,
child: Icon(Icons.refresh,color: Colors.black, size: 50,),
),
),
),
),
),
floatingActionButton: FloatingActionButton(onPressed: (){
if(_start){
_controller?.stop();
}else{
_controller?.repeat();
}
setState(() {
_start = !_start;
});
},),
);
}
}
AnimationController必傳的一個(gè)參數(shù)是vsync,這是個(gè)屏幕刷新時(shí)通知其他頁面的回調(diào),我們這里with了一個(gè)SingleTickerProviderStateMixin,單個(gè)的屏幕刷新回調(diào)。因?yàn)槲覀兙鸵粋€(gè)動畫需要。這樣屏幕每一次刷新都會通知我們的AnimationController,然后AnimationController根據(jù)我們傳的值和時(shí)間,計(jì)算當(dāng)前需要的旋轉(zhuǎn)角度。repeat代表0到1重復(fù)執(zhí)行。forward是0到1執(zhí)行一次。那我要是指定3到5執(zhí)行呢?那就設(shè)置區(qū)間
//設(shè)置lowerBound和upperBound
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1),lowerBound: 3.0,upperBound: 5.0);
_controller?.addListener(() {
print("value = ${_controller?.value}");
});
}
........
//forward用執(zhí)行一次
floatingActionButton: FloatingActionButton(onPressed: (){
if(_start){
_controller?.stop();
}else{
_controller?.forward();
}
setState(() {
_start = !_start;
});
},),
除了RotationTransition還有FadeTransition和ScaleTransition、SlideTransition等。下面用ScaleTransition做一個(gè)放大縮小發(fā)大縮小重復(fù)循環(huán)的動畫。
class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
AnimationController? _controller = null;
bool _start = false;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
_controller?.addListener(() {
print("value = ${_controller?.value}");
});
}
@override
void dispose() {
super.dispose();
_controller?.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: ScaleTransition(
scale: _controller!!,
child: Container(
width: 300,
height: 300,
color: Colors.blue,
),
),
),
),
floatingActionButton: FloatingActionButton(onPressed: (){
_controller?.repeat(reverse: true);
},),
);
}
}
值得一提的是reverse,如果沒有設(shè)置,那么動畫只會放大,不會從大變小。設(shè)置了reverse就會有放大縮小再放大再縮小的動畫。
再來一個(gè)來回移動的動畫:
class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
AnimationController? _controller = null;
bool _start = false;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
_controller?.addListener(() {
print("value = ${_controller?.value}");
});
}
@override
void dispose() {
super.dispose();
_controller?.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: SlideTransition(
position: _controller!.drive(Tween(begin: Offset(0,0),end: Offset(1,0))),
child: Container(
width: 300,
height: 300,
color: Colors.blue,
),
),
),
),
floatingActionButton: FloatingActionButton(onPressed: (){
_controller?.repeat(reverse: true);
},),
);
}
}
SlideTransition需要position,而position是一系列的Offset。_controller.drive加上Tween就能生成一系列Offset。當(dāng)然Tween還有另外的寫法。如下:
position: Tween(begin: Offset(0,0),end: Offset(1,0)).animate(_controller!),
這樣寫有好處,他可以疊加其他的Tween。如下:
position: Tween(begin: Offset(0, 0), end: Offset(1, 0))!
.chain(CurveTween(curve: Curves.elasticInOut))
.animate(_controller!),
這樣我們就能做一個(gè)間隔動畫,比如一個(gè)小部件執(zhí)行完動畫再執(zhí)行其他小部件,一個(gè)個(gè)得來。如下:
class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
AnimationController? _controller = null;
bool _start = false;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 4));
}
@override
void dispose() {
super.dispose();
_controller?.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SliverBox(_controller!,Colors.blue[100]!,0.0,0.2),
SliverBox(_controller!,Colors.blue[200]!,0.2,0.4),
SliverBox(_controller!,Colors.blue[300]!,0.4,0.6),
SliverBox(_controller!,Colors.blue[400]!,0.6,0.8),
SliverBox(_controller!,Colors.blue[500]!,0.8,1.0),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller?.repeat(reverse: true);
},
),
);
}
}
class SliverBox extends StatelessWidget {
AnimationController _controller;
Color color;
double startInterval;
double endInterval;
SliverBox(this._controller,this.color,this.startInterval,this.endInterval,{Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SlideTransition(
position: Tween(begin: Offset(0, 0), end: Offset(0.1, 0))
.chain(CurveTween(curve: Interval(startInterval, endInterval,curve: Curves.bounceOut)))
.animate(_controller!),
child: Container(
width: 280,
height: 100,
color: color,
),
);
}
}
五、方便擴(kuò)展的AnimatedBuilder
我們做一個(gè)透明度加高度變化的動畫,如下:
class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
AnimationController? _controller = null;
bool _start = false;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 4));
}
@override
void dispose() {
super.dispose();
_controller?.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: AnimatedBuilder(
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: _controller!.value,
child: Container(
width: 200,
height: 100 + (100 * _controller!.value),
color: Colors.blue,
child: Center(
child: Text(
"HI",
style: TextStyle(fontSize: 40),
),
),
),
);
}, animation: _controller!,
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller?.repeat();
},
),
);
}
}
可以看到我們的透明度增加,高度也隨之增加。那么上次我們提到了AnimatedBuilder里面的builder有一個(gè)child參數(shù),他是干嘛的?其實(shí)他是flutter為了增加效率做的參數(shù),你看啊,我們這代碼里面,Opacity和Container都有變化,一個(gè)跟著動畫改變透明度,提個(gè)改變高度。唯獨(dú)中間的文字沒有變。所以我們可以把這個(gè)文字放到AnimatedBuilder的child中,這樣的話,每一幀動畫回調(diào)的builder都會把這個(gè)child返回,我們有直接復(fù)用即可??梢钥闯鰂lutter對性能優(yōu)化的注重。優(yōu)化代碼如下:
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: AnimatedBuilder(
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: _controller!.value,
child: Container(
width: 200,
height: 100 + (100 * _controller!.value),
color: Colors.blue,
child: child,
),
);
},
animation: _controller!,
child: Center( //這個(gè)child將會在build回調(diào)中復(fù)用。
child: Text(
"HI",
style: TextStyle(fontSize: 40),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller?.repeat();
},
),
);
}
}
六、hero動畫
android有一個(gè)兩個(gè)頁面的跳轉(zhuǎn)動畫,那flutter也有,它就是hero動畫。如下:
class HeroAnimationRoute extends StatelessWidget {
const HeroAnimationRoute({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.only(top: 100),
alignment: Alignment.topCenter,
child: Column(
children: <Widget>[
InkWell(
child: Hero(
tag: "avatar", //唯一標(biāo)記,前后兩個(gè)路由頁Hero的tag必須相同
child: ClipOval(
child: Image.network('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F511%2F101611154647%2F111016154647-10-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1644588215&t=9a40338757751be9d2684f0d3c80ae31',
width: 100.0,
),
),
),
onTap: () {
//打開B路由
Navigator.push(context, PageRouteBuilder(
pageBuilder: (
BuildContext context,
animation,
secondaryAnimation,
) {
return FadeTransition(
opacity: animation,
child: Scaffold(
appBar: AppBar(
title: Text("原圖"),
),
body: HeroAnimationRouteB(),
),
);
},
));
},
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text("點(diǎn)擊頭像"),
)
],
),
),
);
}
}
class HeroAnimationRouteB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", //唯一標(biāo)記,前后兩個(gè)路由頁Hero的tag必須相同
child: Image.network('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F511%2F101611154647%2F111016154647-10-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1644588215&t=9a40338757751be9d2684f0d3c80ae31'),
),
);
}
}
Hero中的tag必須保持一致。這樣,跳轉(zhuǎn)頁面就不會看起來生硬了。
七、Paint與動畫結(jié)合
像android一樣,flutter也有自己的畫布、畫筆。如果要在flutter中畫畫,需要用到
Container(
width: double.infinity,
height: double.infinity,
child: CustomPaint(
painter: MyPaint(),
)
)
......
class MyPaint extends CustomPainter{
MyPaint(this.whitePaint,this._snows);
@override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(Offset(0, 0), 50, Paint());
});
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
這樣就能在屏幕上畫一個(gè)圓。
那么我們把這個(gè)圓當(dāng)做是雪花,再在底部畫一個(gè)雪人,那么結(jié)合anmation的事實(shí)刷新,就能讓雪花動起來。如下:
class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
late AnimationController _controller ;
late Paint whitePaint;//雪人和雪花都是白色,所以定義白色畫筆
late List<Snow> _snows = List.generate(100, (index) => Snow());//一共一百個(gè)雪花
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 4));
whitePaint = Paint();
whitePaint.color = Colors.white;
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(//寬高都最大
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue,Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
)
),
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return CustomPaint(//設(shè)置自定義畫布
painter: MyPaint(whitePaint,_snows),//傳入畫筆和雪花到自定義畫布
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.repeat();//點(diǎn)擊循環(huán)播放動畫
},
),
);
}
}
class MyPaint extends CustomPainter{
Paint whitePaint;
List<Snow> _snows;
MyPaint(this.whitePaint,this._snows);
@override
void paint(Canvas canvas, Size size) {
print("width = ${size.width}");
canvas.drawCircle(Offset(size.width/2, size.height - 200), 50, whitePaint);//雪人頭
canvas.drawOval(Rect.fromCenter(center: Offset(size.width/2,size.height - 75), width: 140, height: 200), whitePaint);//雪人身體
_snows.forEach((element) {//循環(huán)畫出雪花
canvas.drawCircle(Offset(element.x, element.y), element.radio, whitePaint);
element.start();//開始下落,并且下落完成,重新設(shè)置隨機(jī)數(shù)。
});
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;//這是判斷paint方法要不要執(zhí)行,如果返回false,paint方法不會執(zhí)行,返回true就會,我們這里必須讓他刷新。
}
}
class Snow {
double x = Random().nextDouble() * 360;//橫坐標(biāo)
double y = Random().nextDouble() * 720;//縱坐標(biāo)
double radio = Random().nextDouble() * 2 + 4;// 最小3最大5
double speed = Random().nextDouble() * 2 + 2;// 最小2最大4
start(){
y = y + speed;
if(y > 720){//如果雪花落地了,那么重新生成位置、大小、速度等
x = Random().nextDouble() * 360;
y = 0;
radio = Random().nextDouble() * 2 + 4;// 最小3最大5
speed = Random().nextDouble() * 2 + 2;// 最小2最大4
}
}
}
八、第三方動畫框架
和安卓一樣,flutter也支持lottie還支持Flare等。可以去插件庫看看。