一、带动画刷新的小部件
比如AnimatedContainer,这一类小部件都以Animated开头,示例如下:
AnimatedContainer(
duration: const Duration(seconds: 3),
width: 300,
height: 300,
color: Colors.blue,
)
将上面的颜色换成其他颜色然后热更新一下,就会有颜色过渡动画。这一类的动画小部件只对自己的属性起到动画效果,他不能控制自己的child也执行动画。
差值器:curve
默认是Curves.linear。他是水平匀速运动。Curves里面还有很多,可以都看看。做一个匀速下落盒子的动画,如下:
@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切换时,没有动画,那么我们要做到这样的功能需要AnimatedSwitcher来实现。如下:
AnimatedSwitcher(
duration: Duration(seconds: 2),
child: Center(child: CircularProgressIndicator(),),//Image.network("https://www.baidu.com/img/flexible/logo/pc/result.png")
)
当吧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),),),
)
为什么没有动画?因为只是改文案,flutter不会认为是换了小部件,所以AnimatedSwitcher也不会执行动画,那么要想执行动画就要让flutter觉得小部件改变了。就要用到key。
在Flutter中,当重新build时,会更新widget和element。
Flutter的UI是通过构建widget树来实现的,widget是不可变的,当需要更新UI时,会创建一个新的widget,并使用diff算法比较新旧widget的差异,然后更新element树。如果widget树中的某个widget的属性发生了变化,或者父级widget调用了setState方法,那么Flutter会重新build该widget及其子树。
在重新build时,Flutter会根据widget的差异更新element树。如果新旧widget的类型相同,Flutter会复用element,只更新相关的属性值。如果新旧widget的类型不同,则会销毁旧的element,并创建一个新的element。如果当类型相同,key不同,flutter也会重新构建widget。
所以把上面的代码改成下面的加个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),),),
)
当然AnimatedSwitcher还可以选择动画类型,用transitionBuilder属性即可。默认用的是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),),),
)
三、补间动画
和安卓一样,flutter也有补间动画,其实可以理解成,一个属性设置在一个返回变化,然后一变化就给小部件赋值,达到动画效果。比如下面的透明度动画。如下:
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),
),
),
),
)
上面的代码可以实现AnimatedOpacity的动画效果,而且好掌握。就是代码长了些。有没有发现builder里面的child没有用到?这个是优化用的。还没到时机,暂时不记录。
补间动画经常和Transform一起连用。下面来一个按按钮让小部件来回放大缩小的动画。如下:
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还有旋转,平移等,都可以看看。
四、显示动画
显示动画带控制器,能更好的方便我们停止开始等。下面做一个不停旋转的图标动画,如下:
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必传的一个参数是vsync,这是个屏幕刷新时通知其他页面的回调,我们这里with了一个SingleTickerProviderStateMixin,单个的屏幕刷新回调。因为我们就一个动画需要。这样屏幕每一次刷新都会通知我们的AnimationController,然后AnimationController根据我们传的值和时间,计算当前需要的旋转角度。repeat代表0到1重复执行。forward是0到1执行一次。那我要是指定3到5执行呢?那就设置区间
//设置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用执行一次
floatingActionButton: FloatingActionButton(onPressed: (){
if(_start){
_controller?.stop();
}else{
_controller?.forward();
}
setState(() {
_start = !_start;
});
},),
除了RotationTransition还有FadeTransition和ScaleTransition、SlideTransition等。下面用ScaleTransition做一个放大缩小发大缩小重复循环的动画。
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,如果没有设置,那么动画只会放大,不会从大变小。设置了reverse就会有放大缩小再放大再缩小的动画。
再来一个来回移动的动画:
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。当然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!),
这样我们就能做一个间隔动画,比如一个小部件执行完动画再执行其他小部件,一个个得来。如下:
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,
),
);
}
}
五、方便扩展的AnimatedBuilder
我们做一个透明度加高度变化的动画,如下:
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有一个child参数,他是干嘛的?其实他是flutter为了增加效率做的参数,你看啊,我们这代码里面,Opacity和Container都有变化,一个跟着动画改变透明度,提个改变高度。唯独中间的文字没有变。所以我们可以把这个文字放到AnimatedBuilder的child中,这样的话,每一帧动画回调的builder都会把这个child返回,我们有直接复用即可。可以看出flutter对性能优化的注重。优化代码如下:
@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( //这个child将会在build回调中复用。
child: Text(
"HI",
style: TextStyle(fontSize: 40),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller?.repeat();
},
),
);
}
}
六、hero动画
android有一个两个页面的跳转动画,那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", //唯一标记,前后两个路由页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("点击头像"),
)
],
),
),
);
}
}
class HeroAnimationRouteB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页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必须保持一致。这样,跳转页面就不会看起来生硬了。
七、Paint与动画结合
像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;
}
}
这样就能在屏幕上画一个圆。
那么我们把这个圆当做是雪花,再在底部画一个雪人,那么结合anmation的事实刷新,就能让雪花动起来。如下:
class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
late AnimationController _controller ;
late Paint whitePaint;//雪人和雪花都是白色,所以定义白色画笔
late List<Snow> _snows = List.generate(100, (index) => Snow());//一共一百个雪花
@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(//设置自定义画布
painter: MyPaint(whitePaint,_snows),//传入画笔和雪花到自定义画布
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.repeat();//点击循环播放动画
},
),
);
}
}
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) {//循环画出雪花
canvas.drawCircle(Offset(element.x, element.y), element.radio, whitePaint);
element.start();//开始下落,并且下落完成,重新设置随机数。
});
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;//这是判断paint方法要不要执行,如果返回false,paint方法不会执行,返回true就会,我们这里必须让他刷新。
}
}
class Snow {
double x = Random().nextDouble() * 360;//横坐标
double y = Random().nextDouble() * 720;//纵坐标
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等。可以去插件库看看。