Flutter 动画系统
dart class _MyWidgetState extends State with SingleTickerProviderStateMixin { late AnimationController _controller;
@override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, // 提供 Ticker )..forward(); // 启动动画 }
@override void dispose() { _controller.dispose(); super.dispose(); } }
#### 2. Animation:可聆听的“值流”
`AnimationController` 本身继承自 `Animation<double>`,所以它直接提供线性变化的 double 值。但大多数时候我们需要曲线、颜色、大小等非线性的值。这时就要用到 `Animation<T>` 的其他子类或通过 `Tween` 与控制器组合得到。
- `Animation` 对象可以添加监听器 `addListener`,值变化时触发,我们通常在该回调中调用 `setState` 触发界面重绘。
- 可以添加状态监听 `addStatusListener`,在动画完成或恢复时执行特定逻辑。
#### 3. Tween:定义值的变化范围
`Tween<T>` 定义了动画的起点和终点,它本身不是一个 `Animation`,需要通过 `animate()` 方法作用于一个 `AnimationController` 来生成一个 `Animation<T>`。例如:
```dart
final Animation<Color?> colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue)
.animate(_controller);
final Animation<double> sizeAnimation = Tween<double>(begin: 0, end: 300)
.animate(_controller);
animate() 方法还可以接受一个 Curve 参数,改变动画的节奏。例如 Curves.easeInOut 会让动画缓入缓出。
常用曲线与节奏控制
Curve 类定义了动画值随时间变化的速率。Flutter 内置了大量曲线,从常见的 Curves.easeIn、Curves.easeOut、Curves.bounceOut 到 Curves.elasticOut 等。只需在动画对象上指定即可:
final Animation<double> curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
如果需要复合曲线(例如前半段使用一种曲线,后半段使用另一种),可以使用 Interval 配合 Curve 使用。
从头开始构建一个动画(显式动画)
初学者最容易混淆的是隐式动画和显式动画。我们先学习显式动画,因为它能让你完全掌控动画过程,并深入理解底层机制。
步骤拆解:
- 创建
AnimationController:绑定vsync并设置时长。 - 创建 Tween 并生成 Animation 对象:可以结合曲线。
- 添加监听器,在
setState中更新 UI:根据动画当前值构建组件。 - 在
dispose中释放控制器。 - 通过按钮或生命周期触发动画播放。
下面是一个让方块大小从 0 变化到 200,颜色从红变蓝的完整示例:
class ExplicitAnimationDemo extends StatefulWidget {
@override
_ExplicitAnimationDemoState createState() => _ExplicitAnimationDemoState();
}
class _ExplicitAnimationDemoState extends State<ExplicitAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_sizeAnimation = Tween<double>(begin: 0, end: 200).animate(
CurvedAnimation(parent: _controller, curve: Curves.bounceOut),
);
_colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller);
// 动画值变化时刷新界面
_controller.addListener(() {
setState(() {});
});
// 启动动画
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: _colorAnimation.value,
),
);
}
}
虽然在监听器中调用 setState 能实现动画,但代码较为繁琐。Flutter 提供了 AnimatedBuilder 小部件来简化这种模式。
使用 AnimatedBuilder 简化监听
AnimatedBuilder 是一个通用动画构建器,它接受一个 animation 和一个 builder 方法,当动画值变化时自动调用 builder,且只会重建 builder 内部的部分,无需手动管理 addListener 和 setState。
用 AnimatedBuilder 重写上述示例:
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Center(
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: _colorAnimation.value,
),
);
},
);
}
这样代码更加清晰,并且能将性能敏感的 child 提取出来(如果它不依赖于动画)以避免不必要的重建。
隐式动画:开箱即用的便捷方案
如果你只需要让某个属性从 A 动到 B,而不需要复杂的控制(如暂停、反向、重复等),隐式动画 是最佳选择。Flutter 提供了一系列 AnimatedFoo 小部件,它们内部已经封装了控制器、补间和构建逻辑,你只需指定目标值和时长。
常见隐式动画组件:
AnimatedContainer:动画改变容器的大小、颜色、边距等。AnimatedOpacity:淡入淡出。AnimatedAlign:平滑改变对齐方式。AnimatedPadding:动画改变内边距。AnimatedDefaultTextStyle:文本样式过渡。
示例:动态改变容器大小和颜色
class ImplicitAnimationDemo extends StatefulWidget {
@override
_ImplicitAnimationDemoState createState() => _ImplicitAnimationDemoState();
}
class _ImplicitAnimationDemoState extends State<ImplicitAnimationDemo> {
bool _expanded = false;
void _toggle() {
setState(() {
_expanded = !_expanded;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _toggle,
child: AnimatedContainer(
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
color: _expanded ? Colors.blue : Colors.red,
),
);
}
}
只需修改状态并触发 setState,AnimatedContainer 就会自动插值过渡到新值。
交错动画:协调多个动画的时间线
实际项目中经常需要多个动画按顺序或叠加播放,这就是交错动画(Staggered Animation)。核心思想是让同一个 AnimationController 驱动多个补间,但每个补间使用不同的 Interval 曲线(通过 Interval 指定开始和结束的百分比)。
例如:先旋转,再逐渐放大,最后改变颜色。
late Animation<double> rotation;
late Animation<double> scale;
late Animation<Color?> color;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
);
rotation = Tween<double>(begin: 0, end: 2 * pi).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.3, curve: Curves.easeOut),
),
);
scale = Tween<double>(begin: 1.0, end: 2.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.3, 0.7, curve: Curves.elasticOut),
),
);
color = ColorTween(begin: Colors.blue, end: Colors.orange).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.7, 1.0, curve: Curves.linear),
),
);
_controller.forward();
}
构建时同时应用多个动画值即可。Interval 确保了每个动画只在控制器进度的一段起作用。
英雄动画与路由过渡
体验性极强的动画之一是 Hero 动画(共享元素过渡)。当从页面 A 跳转到页面 B 时,两个页面中具有相同 tag 的 Hero 小部件会执行平滑的飞行过渡。
使用方式:在两个页面的相似元素外包裹 Hero,并赋予相同的 tag。
// 页面 A
Hero(
tag: 'avatar',
child: Image.asset('avatar.png', width: 100),
)
// 页面 B
Hero(
tag: 'avatar',
child: Image.asset('avatar.png', width: 300),
)