1、Flutter动画基础:动画核心概念、AnimationController、Ticker、动画生命周期
动画,说白了就是让UI「动起来」。但Flutter的动画机制,跟传统的前端动画不太一样。我第一次接触时也有点懵——怎么还要管什么Ticker、Controller?后来做多了才发现,这套设计其实非常优雅。
咱们今天就把动画的底裤扒干净。从最核心的概念讲起,再到Controller怎么用,最后聊聊生命周期。嗯,保证你听完就能上手。
1.1 动画的核心三要素
Flutter动画其实就三个东西:值的变化、时间的驱动、界面的刷新。
- Animation对象:它本身不干活,只是存一个「当前值」。比如从0变到1,它告诉你现在是多少。
- AnimationController:真正的「发动机」。控制动画开始、暂停、反向、结束。
- Ticker:每秒60次的「心跳」。每次心跳通知Controller:该更新值了。
一句话总结:Ticker负责喊「到点了」,Controller负责算「现在值是多少」,Animation负责存「当前值」,最后Widget根据这个值重新绘制。
我在项目中遇到过不少新手,直接new了一个Animation就以为能跑。结果界面一动不动。为什么?因为没有Controller驱动,Animation就是个死数据。
1.2 AnimationController——动画的指挥官
Controller是动画的核心。它继承自Animation
- 控制能力:forward()、reverse()、repeat()、stop()、reset()
- 时间管理:duration属性决定动画时长
- 值范围:默认从0.0到1.0,但可以自定义
看个最简单的例子:
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this, // 这个vsync就是TickerProvider
);
}
void _startAnimation() {
_controller.forward(); // 从0到1,跑2秒
}
@override
void dispose() {
_controller.dispose(); // 必须释放!不然内存泄漏
super.dispose();
}
}
个人习惯:我一般把Controller的dispose放在dispose方法的第一行。因为曾经有一次我忘了写,结果页面切走再回来,动画还在后台跑——CPU直接拉满。嗯,血的教训。
1.3 Ticker——动画的「心跳」
Ticker是什么?说白了就是一个回调函数,每帧调用一次。Flutter的屏幕刷新率是60Hz,所以Ticker每秒会触发60次回调。
但Ticker不能直接用。你需要通过TickerProvider来获取。最常见的做法是让State混入SingleTickerProviderStateMixin:
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
// 现在这个State就是一个TickerProvider了
// 可以传给AnimationController的vsync参数
}
为什么非要这么麻烦?因为Ticker需要跟Widget的生命周期绑定。页面不可见时,Ticker应该暂停,避免浪费性能。Flutter帮你做了这件事——当Widget树被移除时,Ticker自动停止。
注意:如果你需要多个Controller,就用TickerProviderStateMixin(复数)。但大多数场景下,一个Controller就够了。我见过有人为了省事,一个页面混入了两个TickerProvider,结果把自己绕晕了。
1.4 动画生命周期——从生到死
一个完整的动画生命周期,大概分这么几步:
- 创建:在initState中初始化Controller
- 启动:调用forward()或repeat()
- 运行:每帧更新值,触发addListener回调
- 结束:到达目标值,触发statusListener(状态变为completed或dismissed)
- 销毁:在dispose中释放Controller
这里有个容易被忽略的点:动画状态监听。你可以通过addStatusListener来监听动画的状态变化:
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 动画跑完了
// 我曾经在这里做了一件蠢事:直接调用了dispose()
// 结果后续想反向播放,Controller已经没了
// 正确做法:用reverse()或者reset()
} else if (status == AnimationStatus.dismissed) {
// 动画回到了起点
}
});
避坑指南:我曾经在动画completed时直接dispose了Controller,然后想再播放一次,发现报错了。后来我改成用forward()配合repeat(),或者用reverse()做往返动画。记住:Controller的生命周期应该由Widget的dispose管理,而不是由动画状态管理。
1.5 实战小技巧
讲几个我常用的套路:
- 延迟启动:用Future.delayed配合forward(),实现入场动画的错峰
- 曲线控制:给Controller加CurvedAnimation,让动画更自然
- 值映射:用Tween<double>().animate(controller)把0~1映射到任意范围
举个例子,让一个按钮从透明到完全显示,同时从底部滑上来:
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
final Animation<double> _opacity = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);
final Animation<Offset> _slide = Tween(
begin: Offset(0, 0.5),
end: Offset(0, 0),
).animate(_controller);
这样写的好处是:一个Controller驱动多个动画,同步性好,代码也干净。
1.6 总结
动画基础就这些。记住三个关键词:Controller驱动、Ticker心跳、生命周期绑定。你想想看,Flutter这套设计其实很聪明——把动画的控制权交给开发者,但把性能优化的脏活(比如页面不可见时暂停动画)交给框架。
下一章咱们会讲Tween和Curve,到时候你会看到,动画的「值变化」可以玩出多少花样。嗯,先消化今天的内容,动手写个简单的旋转动画试试?