基础动画
class ScaleAnimationRoute extends StatefulWidget {
const ScaleAnimationRoute({Key? key}) : super(key: key);
@override
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
//使用弹性曲线
animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//匀速
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(() => {});
});
//启动动画(正向执行)
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}
@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
代码中addListener函数调用了setState,所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build方法再次被调用,在build中,改变Image的宽高,因为它的高度和宽度使用的是animation.value,所以会逐渐放大。需要注意的是,动画完成时要释放控制器(调用dispose()函数)防止内存泄漏。
AnimatedWidget简化
通过addListener和setState来更新UI比较繁琐,AnimatedWidget类封装了调用setState的细节,允许我们将widget分离出来。
import 'package:flutter/material.dart';
class AnimatedImage extends AnimatedWidget {
const AnimatedImage({
Key? key,
required Animation<double> animation,
}) : super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}
}
class ScaleAnimationRoute1 extends StatefulWidget {
const ScaleAnimationRoute1({Key? key}) : super(key: key);
@override
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2), vsync: this);
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller);
//启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedImage(
animation: animation,
);
}
@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
用AnimatedBuilder重构
用AnimatedWidget可以从动画中分离出widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget中,假设需要再添加一个widget透明度变化的动画,那么需要再实现一个AnimatedWidget,这样很繁琐,如果能把渲染过程也抽离出来,就会很优雅。AnimatedBuilder正式将渲染逻辑分离出来:
@override
Widget build(BuildContext context) {
//return AnimatedImage(animation: animation,);
return AnimatedBuilder(
animation: animation,
child: Image.asset("imgs/avatar.png"),
builder: (BuildContext ctx, child) {
return Center(
child: SizedBox(
height: animation.value,
width: animation.value,
child: child,
),
);
},
);
}
上面的代码似乎child看起来被指定了两次,实际上是:将外部引用的child传递给AnimatedBuilder后,AnimtedBuilder再将其传递给匿名构造器,然后将该对象作为其子对象。最终结果是AnimatedBuilder返回的对象插入到widget中。
好处:
- 不需要显示的去添加帧监听器,然后再调用setState。
- 更好的性能,因为每一帧需要构建的widget的范围缩小了,如果没有builder,setState将会在父组件上下文中调用,这会导致父组件的build方法重新调用,而builder之后,只会导致动画widget自身builder重新调用,避免不必要的rebuilder。
- 通过AnimatedBuilder可以封装常见的过渡效果来复用动画。
class GrowTransition extends StatelessWidget {
const GrowTransition({Key? key,
required this.animation,
this.child,
}) : super(key: key);
final Widget? child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
动画状态监听
Animation的addStatusListener方法添加动画状态改变监听器。Flutter中有四种动画状态:
- dismissed:动画在起始点停止
- forward:动画正在正向执行
- reverse:动画正在反向执行
- completed:动画在终点停止
示例:
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
//动画执行结束时反向执行动画
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//动画恢复到初始状态时执行动画(正向)
controller.forward();
}
});
//启动动画(正向)
controller.forward();
}