flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
# 【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<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 缓存
```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<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)
```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 频繁重建。