简介
Flutter 提供了强大的动画系统,包括隐式动画和显式动画。通过动画可以让应用界面更加生动,提升用户体验。
隐式动画
AnimatedContainer
class AnimatedContainerDemo extends StatefulWidget {
@override
_AnimatedContainerDemoState createState() => _AnimatedContainerDemoState();
}
class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_expanded = !_expanded;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
width: _expanded ? 200.0 : 100.0,
height: _expanded ? 200.0 : 100.0,
color: _expanded ? Colors.blue : Colors.red,
curve: Curves.fastOutSlowIn,
),
);
}
}
其他隐式动画组件
// 透明度动画
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
)
// 位置动画
AnimatedPositioned(
duration: Duration(milliseconds: 500),
left: _left,
top: _top,
child: Container(
width: 50,
height: 50,
color: Colors.red,
),
)
// 大小动画
AnimatedSize(
duration: Duration(milliseconds: 300),
child: Container(
width: _size,
height: _size,
color: Colors.green,
),
)
显式动画
基本用法
class AnimationDemo extends StatefulWidget {
@override
_AnimationDemoState createState() => _AnimationDemoState();
}
class _AnimationDemoState extends State<AnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
);
},
);
}
}
动画控制器
// 启动动画
_controller.forward();
// 反向运行
_controller.reverse();
// 重置动画
_controller.reset();
// 重复动画
_controller.repeat();
// 停止动画
_controller.stop();
完整示例
class AnimatedCard extends StatefulWidget {
@override
_AnimatedCardState createState() => _AnimatedCardState();
}
class _AnimatedCardState extends State<AnimatedCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _rotateAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 800),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeOut),
));
_rotateAnimation = Tween<double>(
begin: 0.0,
end: 2 * pi,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0, curve: Curves.easeOut),
));
_slideAnimation = Tween<Offset>(
begin: Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animated Card')),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotateAnimation.value,
child: SlideTransition(
position: _slideAnimation,
child: Container(
width: 200,
height: 300,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black26,
offset: Offset(0, 4),
blurRadius: 10,
),
],
),
child: Center(
child: Text(
'Animated Card',
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
),
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Icon(Icons.play_arrow),
),
);
}
}
自定义动画
Tween
// 自定义 Tween
class ColorTween extends Tween<Color?> {
ColorTween({Color? begin, Color? end}) : super(begin: begin, end: end);
@override
Color? lerp(double t) {
return Color.lerp(begin, end, t);
}
}
// 使用自定义 Tween
final colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
AnimatedWidget
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key? key, required Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Transform.rotate(
angle: animation.value,
child: FlutterLogo(size: 100),
);
}
}
最佳实践
- 选型顺序固定:200~400ms 的一次性小交互(显隐、展开收起、缩放反馈)优先隐式动画(AnimatedContainer/AnimatedOpacity/AnimatedScale),setState 改值就行;多阶段编排(先放大、再旋转、最后滑走)才上 AnimationController + Interval——隐式覆盖约 80% 场景,别为简单的显隐付显式动画的管理成本。
- 时长经验值:按钮按反馈 100
200ms,页面元素入场 200400ms,页面转场 300~500ms;超过 1s 的常驻动画用户会觉得拖沓(刻意的加载/氛围动画除外)。 - 显式动画优先 AnimatedBuilder 只重建动画子树,别在 controller 的 listener 里调整页 setState——复杂页面每帧全量重建,直接掉帧。
- AnimationController 建完就写 dispose:State 的 dispose 里必须 dispose controller;常驻 repeat 的动画在页面不可见时 stop,否则后台每帧空转烧 CPU——这是最常见的 Ticker 泄漏。
注意事项
- 别在 listener 里做重型计算:60fps 下 listener 每 16.6ms 触发一次,里面做 JSON 解析、排序、大 list 遍历必然掉帧;listener 里只做赋值,计算放外面或提前缓存。
- controller.forward() 在已 completed 的动画上调用会直接跳过不播——“再播一次”没反应时,先 reset() 再 forward;反向同理,检查 dismissed 状态。
- 隐式动画不耐高频改值:快速连续 setState 改 AnimatedContainer 的属性,动画会不断打断重起、看起来发抖;跟手拖拽这种高频场景用显式动画直接设 controller.value。
- 一个 State 里要跑多个 controller 时,用 TickerProviderStateMixin 而不是 SingleTickerProviderStateMixin——Single 下创建第二个 controller 直接抛异常。
实战:隐式动画先行,是我的默认策略
做电商 App 时,商品卡片的收藏按钮点一下缩放、筛选面板的展开收起、提示条的显隐——这类 200~300ms 的小交互,我一律先用隐式动画(AnimatedContainer、AnimatedOpacity、AnimatedScale)解决:setState 改个值就行,不用管 controller 的创建和 dispose,心智负担小得多。
只有当一个动画需要精确编排(比如”先放大、再旋转、最后滑走”这种多阶段),我才上 AnimationController + Interval。取舍逻辑很简单:隐式动画解决 80% 的场景,剩下的 20% 才值得为显式动画付管理成本。反例就是别把 AnimationController 用在简单的显隐上——代码量翻倍,还容易漏写 dispose,白白埋个内存泄漏的坑。
参考:Flutter 官方文档《Animations》中 implicit vs explicit 的划分 (docs.flutter.dev/ui/animations)
总结
Flutter 的动画系统提供了丰富的功能,通过合理使用可以创建出流畅、生动的用户界面。理解并掌握动画的使用对于开发高质量的 Flutter 应用至关重要。
