外观
动画与自定义绘制
先选成本合适的方案
| 场景 | 方案 | 需要维护 |
|---|---|---|
| 样式随状态平滑变化 | AnimatedContainer、AnimatedOpacity | 目标值、时长与曲线 |
| 内容切换 | AnimatedSwitcher | 子组件身份与过渡 |
| 页面间共享元素 | Hero | 唯一 tag 与两侧布局 |
| 播放、反向、循环、多个阶段 | AnimationController + 过渡组件 | Ticker、控制器和释放 |
| 图表、轨迹、进度弧等绘制 | CustomPaint + CustomPainter | 尺寸、画笔、重绘与语义 |
| 已有布局组件难以表达的布局协议 | 自定义 RenderObject | 约束、布局、命中测试、绘制和语义 |
优先组合现有 Widget;只有确认缺少布局或绘制能力,再进入更底层。动画概念可对照 官方 API 概览。
显式动画的生命周期
下面是可放入 MaterialApp.home 的独立页面,文件顶部需导入 package:flutter/material.dart。
dart
class FadeNotePage extends StatefulWidget {
const FadeNotePage({super.key});
@override
State<FadeNotePage> createState() => _FadeNotePageState();
}
class _FadeNotePageState extends State<FadeNotePage>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
)..forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('笔记动画')),
body: Center(
child: FadeTransition(
opacity: _controller,
child: const Card(
child: Padding(
padding: EdgeInsets.all(24),
child: Text('从状态到画面'),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _controller.forward(from: 0),
child: const Icon(Icons.replay),
),
);
}
}AnimationController 产生动画值,Tween 映射值域,Curve 调整变化节奏,过渡组件消费动画。这里只有透明度变化,直接使用控制器的 0~1 值即可。控制器不能在每次 build 时重新创建;多个控制器的场景需要合适的 TickerProvider。
用 Canvas 画一个进度弧
把下面的 Painter 加到同一文件,另增加 import 'dart:math' as math;:
dart
class ProgressPainter extends CustomPainter {
ProgressPainter({required this.progress, required this.color});
final double progress;
final Color color;
@override
void paint(Canvas canvas, Size size) {
final rect = (Offset.zero & size).deflate(6);
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round;
canvas.drawArc(
rect,
-math.pi / 2,
math.pi * 2 * progress.clamp(0.0, 1.0),
false,
paint,
);
}
@override
bool shouldRepaint(covariant ProgressPainter oldDelegate) {
return oldDelegate.progress != progress || oldDelegate.color != color;
}
}在组件树中用 SizedBox(width: 120, height: 120, child: CustomPaint(painter: ProgressPainter(progress: 0.65, color: Colors.teal))) 展示静态进度,宽高应大于描边留白。加入语义说明时,可在外层使用 Semantics(label: '完成进度', value: '65%')。
shouldRepaint 用于比较新旧绘制委托的输入,并不是「返回 false 后永远不绘制」。动画驱动的绘制还可以通过 Painter 的 repaint 参数监听变化,减少不必要的构建和布局。参见 CustomPainter API。
什么时候才需要 RenderObject
现有 Widget 的组合可以表达布局时,组合通常更容易维护。确实需要自定义布局协议时,再研究 RenderBox.performLayout,确保最终尺寸满足约束;属性变化后,根据影响调用 markNeedsLayout 或 markNeedsPaint。
不要把绘制、交互与无障碍拆开考虑:Canvas 画出来的按钮不会自动拥有标准按钮的焦点、点击和语义行为。绘制案例进入业务页面前,要补上这些能力。
练习与验收
让进度从 0 动画到 0.65,检查起点、终点和主题切换后的颜色。重复进入和退出页面不应产生 Ticker 泄漏;再用 性能工具 检查重建与重绘范围。