简介
组合动画是指将多个简单的动画组合在一起,实现更加复杂和丰富的动画效果。本文介绍如何在 Flutter 中实现动画组合。
基本组合
顺序动画
class SequentialAnimationDemo extends StatefulWidget {
@override
_SequentialAnimationDemoState createState() => _SequentialAnimationDemoState();
}
class _SequentialAnimationDemoState extends State<SequentialAnimationDemo>
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(seconds: 3),
vsync: this,
);
// 缩放动画 (0-1秒)
_scaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.33, curve: Curves.easeOut),
));
// 旋转动画 (1-2秒)
_rotateAnimation = Tween<double>(
begin: 0.0,
end: 2 * pi,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.33, 0.66, curve: Curves.easeInOut),
));
// 滑动动画 (2-3秒)
_slideAnimation = Tween<Offset>(
begin: Offset.zero,
end: Offset(1.0, 0.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.66, 1.0, curve: Curves.easeIn),
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: _slideAnimation.value * 100,
child: Transform.rotate(
angle: _rotateAnimation.value,
child: Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
),
),
),
),
);
},
);
}
}
并行动画
class ParallelAnimationDemo extends StatefulWidget {
@override
_ParallelAnimationDemoState createState() => _ParallelAnimationDemoState();
}
class _ParallelAnimationDemoState extends State<ParallelAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<Color?> _colorAnimation;
late Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
// 所有动画同时进行
_sizeAnimation = Tween<double>(
begin: 50.0,
end: 200.0,
).animate(_controller);
_colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
_opacityAnimation = Tween<double>(
begin: 0.5,
end: 1.0,
).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _opacityAnimation.value,
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: _colorAnimation.value,
borderRadius: BorderRadius.circular(10),
),
),
);
},
);
}
}
复杂组合
交叉动画
class CrossFadeAnimationDemo extends StatefulWidget {
@override
_CrossFadeAnimationDemoState createState() => _CrossFadeAnimationDemoState();
}
class _CrossFadeAnimationDemoState extends State<CrossFadeAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeOutAnimation;
late Animation<double> _fadeInAnimation;
late Animation<double> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_fadeOutAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeOut),
));
_fadeInAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0, curve: Curves.easeIn),
));
_slideAnimation = Tween<double>(
begin: 0.0,
end: 200.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
AnimatedBuilder(
animation: _fadeOutAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeOutAnimation.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
},
),
AnimatedBuilder(
animation: _fadeInAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_slideAnimation.value, 0),
child: Opacity(
opacity: _fadeInAnimation.value,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
);
},
),
],
);
}
}
动画管理
动画状态管理
class AnimationStateManager {
final List<AnimationController> controllers;
AnimationStateManager(this.controllers);
void startAll() {
for (var controller in controllers) {
controller.forward();
}
}
void stopAll() {
for (var controller in controllers) {
controller.stop();
}
}
void resetAll() {
for (var controller in controllers) {
controller.reset();
}
}
void dispose() {
for (var controller in controllers) {
controller.dispose();
}
}
}
动画队列
class AnimationQueue {
final List<AnimationController> _queue = [];
int _currentIndex = 0;
void addAnimation(AnimationController controller) {
_queue.add(controller);
controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_playNext();
}
});
}
void start() {
if (_queue.isNotEmpty) {
_queue[0].forward();
}
}
void _playNext() {
_currentIndex++;
if (_currentIndex < _queue.length) {
_queue[_currentIndex].forward();
}
}
void dispose() {
for (var controller in _queue) {
controller.dispose();
}
}
}
最佳实践
- 一时间轴一 controller:顺序/交叉动画能用一个 controller + Interval 分段就别开多个——多个 controller 要自己写状态机串行,还多 N 个 dispose 风险;只有各段需要独立触发、独立循环(一个常驻转、一个点击才播)时才拆。
- 同一属性的多段变化用 TweenSequence:“先放大到 1.2 再回弹到 1.0”用它比两个 Interval 拼 Tween 更直白,拼接点也不容易跳变。
- 阶段时长按”视觉重量”分配:入场(淡入+位移)给足 60% 时间轴,装饰性收尾(颜色微调)20% 就够;平均分配会让重点动作显得仓促、次要动作显得拖沓。
- 三段以上的组合先画时间轴:纸上或注释里写清每段的 Interval 区间和时长再写代码——靠脑子记顺序返工率极高,Interval 写错一段整个节奏全乱。
注意事项
- Interval 的 begin 必须小于 end,写反了动画直接不播且不报错;多段首尾相接时留 0.01~0.02 的重叠,避免拼接帧肉眼可见的”顿一下”。
- 状态转换先查 status:completed 状态下调 forward() 无效果、dismissed 下调 reverse() 无效果——弹窗”再播一次”没反应,十有八九是没 reset 就直接 forward。
- 别把”并行”写成”串行”:三个动画都直接 animate(_controller) 不带 Interval 就是并行;想错峰必须显式给 Interval——漏写 Interval 是组合动画里最常见的”为什么它们一起动”。
- 动画队列(如文中的 AnimationQueue)里 addStatusListener 要配好移除:页面重建后旧 listener 残留会导致 _playNext 被多次触发,动画乱序——要么 dispose 时 removeListener,要么把 listener 的管理收进统一的 dispose。
实战:能用 Interval,就别开多个 controller
组合动画的需求我遇到过:营销弹窗”先弹出来再轻微回缩”,或者页面转场里标题和内容错峰出现。我的选型很固定:同一时间轴上的多段动画,一个 AnimationController + Interval 分段——比开三四个 controller 再写状态机串起来省事得多,也少了漏写 dispose 的风险。
多个 controller 只在一种情况下值得:各段动画需要独立触发、独立重复(比如一个常驻循环转、一个点击才播)。另外 TweenSequence 也值得知道:“先放大到 1.2 再缩回 1.0”这种同一个属性内的多段变化,用它比 Interval 更直白。这篇的顺序/并行示例思路是对的,落地时先问一句:时间轴能不能统一?能,就别拆。
总结
组合动画可以创建出更加复杂和丰富的动画效果。通过合理组织和管理多个动画,可以实现流畅自然的用户界面交互。理解并掌握动画组合的技巧对于开发高质量的 Flutter 应用很有帮助。
