# 【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 画任意图形 | --- ## 二、怎么运行 / 演示内容 ```dart runApp(const MaterialApp(home: AnimationDemoPage())); ``` 1. 点方块:宽/圆角/颜色三属性同时变(AnimatedContainer); 2. 答题互动:选对"弹跳放大",选错"水平抖动"(TweenSequence 分段动画); 3. 点封面:共享元素飞到详情页(Hero); 4. 拖滑块:进度环实时重绘(CustomPainter drawArc)。 --- ## 三、核心知识点(新手向) ### 1. 隐式 vs 显式(判断口诀) - 只需要"A 变成 B 的过渡" → **隐式**(写一行 `duration` 完事); - 需要"按我的节奏动起来"(回弹、抖动、连续) → **显式 controller**。 ```dart // 隐式:属性变了自动补过渡(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. 显式动画五件套 ```dart // ① 页面必须 with SingleTickerProviderStateMixin class _State extends State with SingleTickerProviderStateMixin { // ② controller:驱动一切(vsync 必传,duration 定义时长) late final AnimationController _answerCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 500), ); // ③ TweenSequence:分段动画(先放大再回弹 = 两个 Tween 按权重拼) late final Animation _correctScale = TweenSequence([ 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 缓存 ```dart AnimatedBuilder( animation: _correctScale, child: body, // 不变的子树用 child 传进来缓存,避免 builder 反复重建 builder: (context, child) => Transform.scale(scale: _correctScale.value, child: child), ) ``` ### 4. Hero 共享元素 ```dart // 页面 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 自绘进度环 ```dart 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 拼出"抖三下") ```dart late final Animation _wrongShake = TweenSequence([ 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) ```dart 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), ); } ``` --- ## 五、踩坑记录 1. **AnimationController 必须 dispose**,且页面必须 `with SingleTickerProviderStateMixin`(否则报 Ticker 错误); 2. **隐式动画一定要写 `duration`**,漏了就瞬变没有过渡; 3. **AnimatedBuilder 的 builder 里别放不变子树**,用 `child` 参数缓存(否则每帧重建,性能浪费); 4. **shouldRepaint 要正确比较参数**:不重绘或一直重绘都是错; 5. 多个 controller 用 `TickerProviderStateMixin`(多 Ticker),单个用 Single 版; 6. 同屏多个 FloatingActionButton 要 `heroTag: null`(见 list_demo),Hero tag 全局必须唯一,重了会断言崩溃。 --- ## 六、扩展思考 - 本项目 exam 答题页的"选对弹跳/选错抖动"就是本页思路的真实落地; - 显式动画进阶:`AnimationController.repeat()` 无限循环(loading 旋转)、`addStatusListener` 监听完成回调; - 隐式动画家族:AnimatedOpacity / AnimatedSwitcher(切换动画)/ AnimatedPositioned / AnimatedAlign —— 都是"属性变了自动补帧"; - 性能纪律:动画组件尽量用 Transform(不触发 layout),别动 width/height 频繁重建。