6.4 KiB
6.4 KiB
【Demo】动画体系(隐式 / 显式 / Hero / CustomPaint)
源文件:
lib/pages/demo/animation_demo.dart| 分类:基础布局 一句话:对 CSS 学 Flutter 动画 —— 隐式=transition,显式=keyframes,加上 Hero 转场和 Canvas 自绘。
一、这个 Demo 解决什么问题
新手面对 Flutter 动画容易懵:组件太多、概念太多。本页把动画体系按"对 CSS"的方式梳理成 4 块:
| 动画类型 | 对标 CSS | 一句话 |
|---|---|---|
| ① 隐式动画 | transition | 属性变了自己补过渡帧,零 controller |
| ② 显式动画 | keyframes | controller 手动驱动,精确控制时序/曲线 |
| ③ Hero 转场 | 无(原生独有) | 页面跳转时元素"飞过去" |
| ④ CustomPaint | canvas | 用 Canvas 画任意图形 |
二、怎么运行 / 演示内容
runApp(const MaterialApp(home: AnimationDemoPage()));
- 点方块:宽/圆角/颜色三属性同时变(AnimatedContainer);
- 答题互动:选对"弹跳放大",选错"水平抖动"(TweenSequence 分段动画);
- 点封面:共享元素飞到详情页(Hero);
- 拖滑块:进度环实时重绘(CustomPainter drawArc)。
三、核心知识点(新手向)
1. 隐式 vs 显式(判断口诀)
- 只需要"A 变成 B 的过渡" → 隐式(写一行
duration完事); - 需要"按我的节奏动起来"(回弹、抖动、连续) → 显式 controller。
// 隐式:属性变了自动补过渡(duration 必写!漏了就瞬变)
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _boxExpanded ? 200.w : 120.w,
decoration: BoxDecoration(
color: _boxExpanded ? Color(0xFF3B82F6) : Color(0xFFFF9800),
borderRadius: BorderRadius.circular(_boxExpanded ? 45.r : 8.r),
),
)
2. 显式动画五件套
// ① 页面必须 with SingleTickerProviderStateMixin
class _State extends State<Page> with SingleTickerProviderStateMixin {
// ② controller:驱动一切(vsync 必传,duration 定义时长)
late final AnimationController _answerCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
// ③ TweenSequence:分段动画(先放大再回弹 = 两个 Tween 按权重拼)
late final Animation<double> _correctScale = TweenSequence<double>([
TweenSequenceItem(
tween: Tween(begin: 1.0, end: 1.25).chain(CurveTween(curve: Curves.easeOut)),
weight: 40,
),
TweenSequenceItem(
tween: Tween(begin: 1.25, end: 1.0).chain(CurveTween(curve: Curves.bounceOut)),
weight: 60,
),
]).animate(_answerCtrl);
// ④ 触发:每次从 0 重新播放
_answerCtrl.forward(from: 0);
// ⑤ 红线:必须 dispose
@override
void dispose() {
_answerCtrl.dispose();
super.dispose();
}
}
3. AnimatedBuilder 的 child 缓存
AnimatedBuilder(
animation: _correctScale,
child: body, // 不变的子树用 child 传进来缓存,避免 builder 反复重建
builder: (context, child) => Transform.scale(scale: _correctScale.value, child: child),
)
4. Hero 共享元素
// 页面 A
Hero(tag: 'hero-demo-cover', child: Container(width: 120.w, height: 72.h, ...));
// 页面 B(同一个 tag!)
Hero(tag: 'hero-demo-cover', child: Container(width: 280.w, height: 168.h, ...));
两个页面里 tag 相同的 Hero,转场时自动共享动画(封面从小图"飞"成大图)。
5. CustomPainter 自绘进度环
class _ProgressRingPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final rect = (Offset.zero & size).deflate(stroke / 2); // 内缩一个线宽
// 背景环(画满 360°)
canvas.drawArc(rect, -0.5 * 3.14159, 2 * 3.14159, false, bgPaint);
// 前景环:起点 -90° = 12 点方向,顺时针画 progress 比例
canvas.drawArc(rect, -0.5 * 3.14159, 2 * 3.14159 * progress, false, fgPaint);
}
@override
bool shouldRepaint(_ProgressRingPainter oldDelegate) {
// 参数变了才重绘(滑块拖动时)
return oldDelegate.progress != progress || oldDelegate.color != color;
}
}
要点:drawArc 的角度单位是弧度,-π/2 才是 12 点方向(画布 0° 在 3 点方向)。
四、关键代码剖析
抖动动画(4 段 Tween 拼出"抖三下")
late final Animation<double> _wrongShake = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0.0, end: -8.0).chain(CurveTween(curve: Curves.easeOut)), weight: 20),
TweenSequenceItem(tween: Tween(begin: -8.0, end: 8.0), weight: 30),
TweenSequenceItem(tween: Tween(begin: 8.0, end: -6.0), weight: 30),
TweenSequenceItem(tween: Tween(begin: -6.0, end: 0.0), weight: 20),
]).animate(_answerCtrl);
对错反馈的应用(按选择状态包 Transform)
if (isRight) {
body = AnimatedBuilder(
animation: _correctScale,
child: body,
builder: (context, child) => Transform.scale(scale: _correctScale.value, child: child),
);
}
if (isWrong) {
body = AnimatedBuilder(
animation: _wrongShake,
child: body,
builder: (context, child) =>
Transform.translate(offset: Offset(_wrongShake.value, 0), child: child),
);
}
五、踩坑记录
- AnimationController 必须 dispose,且页面必须
with SingleTickerProviderStateMixin(否则报 Ticker 错误); - 隐式动画一定要写
duration,漏了就瞬变没有过渡; - AnimatedBuilder 的 builder 里别放不变子树,用
child参数缓存(否则每帧重建,性能浪费); - shouldRepaint 要正确比较参数:不重绘或一直重绘都是错;
- 多个 controller 用
TickerProviderStateMixin(多 Ticker),单个用 Single 版; - 同屏多个 FloatingActionButton 要
heroTag: null(见 list_demo),Hero tag 全局必须唯一,重了会断言崩溃。
六、扩展思考
- 本项目 exam 答题页的"选对弹跳/选错抖动"就是本页思路的真实落地;
- 显式动画进阶:
AnimationController.repeat()无限循环(loading 旋转)、addStatusListener监听完成回调; - 隐式动画家族:AnimatedOpacity / AnimatedSwitcher(切换动画)/ AnimatedPositioned / AnimatedAlign —— 都是"属性变了自动补帧";
- 性能纪律:动画组件尽量用 Transform(不触发 layout),别动 width/height 频繁重建。