Flutter 提供了从基础 Canvas 绘制到复杂物理动画的完整动画系统。掌握动画层次结构,按需选择合适的方案,是流畅 UI 的关键。
一、Canvas 绘制与 CustomPainter
当标准 Widget 无法满足需求时,可以通过 CustomPainter 直接操作 Canvas:
dart
class WaveformPainter extends CustomPainter {
final List<double> amplitudes;
final Color waveColor;
final double progress; // 0.0 - 1.0
WaveformPainter({
required this.amplitudes,
required this.waveColor,
required this.progress,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = waveColor
..strokeWidth = 3
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
final barWidth = size.width / amplitudes.length;
final playedPaint = Paint()..color = waveColor;
final unplayedPaint = Paint()..color = waveColor.withOpacity(0.3);
for (int i = 0; i < amplitudes.length; i++) {
final x = i * barWidth + barWidth / 2;
final barHeight = amplitudes[i] * size.height;
final y1 = (size.height - barHeight) / 2;
final y2 = y1 + barHeight;
final isPlayed = i / amplitudes.length < progress;
canvas.drawLine(
Offset(x, y1),
Offset(x, y2),
isPlayed ? playedPaint : unplayedPaint,
);
}
}
@override
bool shouldRepaint(WaveformPainter oldDelegate) =>
oldDelegate.progress != progress || oldDelegate.amplitudes != amplitudes;
}
// 使用
CustomPaint(
size: const Size(double.infinity, 80),
painter: WaveformPainter(
amplitudes: waveData,
waveColor: Colors.blue,
progress: playProgress,
),
)
常用 Canvas API:
| 方法 | 功能 |
|---|---|
canvas.drawLine() |
画线 |
canvas.drawRect() |
画矩形 |
canvas.drawCircle() |
画圆 |
canvas.drawPath() |
绘制自定义路径 |
canvas.drawImage() |
绘制图片 |
canvas.drawText() / TextPainter |
绘制文字 |
canvas.clipPath() |
裁剪画布 |
canvas.save() / restore() |
保存/恢复画布状态 |
二、隐式动画(Implicit Animations)
隐式动画是最简单的动画方式,只需设置目标值,Flutter 自动插值过渡。
2.1 AnimatedContainer
dart
class AnimatedBox extends StatefulWidget {
@override
State<AnimatedBox> createState() => _AnimatedBoxState();
}
class _AnimatedBoxState extends State<AnimatedBox> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
decoration: BoxDecoration(
color: _expanded ? Colors.purple : Colors.blue,
borderRadius: BorderRadius.circular(_expanded ? 40 : 8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(_expanded ? 0.3 : 0.1),
blurRadius: _expanded ? 20 : 5,
),
],
),
),
);
}
}
2.2 常用隐式动画 Widget
dart
// 透明度渐变
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: content,
)
// 位置偏移
AnimatedSlide(
offset: _show ? Offset.zero : const Offset(0, 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
child: bottomPanel,
)
// 尺寸自适应
AnimatedSize(
duration: const Duration(milliseconds: 200),
child: showDetail ? DetailWidget() : SizedBox.shrink(),
)
// 交叉淡入淡出
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: Text('$_count', key: ValueKey(_count)), // Key 变化触发动画
)
// 对齐方式变化
AnimatedAlign(
alignment: _isTop ? Alignment.topCenter : Alignment.bottomCenter,
duration: const Duration(milliseconds: 300),
child: const FloatingActionButton(onPressed: null, child: Icon(Icons.add)),
)
三、显式动画(Explicit Animations)
显式动画提供完整的动画控制,适合复杂、精确控制的场景。
3.1 AnimationController
dart
class PulseAnimation extends StatefulWidget {
final Widget child;
const PulseAnimation({super.key, required this.child});
@override
State<PulseAnimation> createState() => _PulseAnimationState();
}
class _PulseAnimationState extends State<PulseAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this, // TickerProvider,防止后台操作
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.6).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_controller.repeat(reverse: true); // 往复循环
}
@override
void dispose() {
_controller.dispose(); // ⚠️ 必须释放
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: child,
),
);
},
child: widget.child, // 不随动画变化的子 Widget,避免重复构建
);
}
}
3.2 Tween 与 Curve
dart
// Tween 定义起止值
Tween<double>(begin: 0, end: 1)
Tween<Color?>(begin: Colors.red, end: Colors.blue)
Tween<Offset>(begin: Offset.zero, end: const Offset(1, 0))
// ColorTween
ColorTween(begin: Colors.white, end: Colors.black)
// 自定义 Tween
class SizeTween extends Tween<Size> {
SizeTween({required super.begin, required super.end});
@override
Size lerp(double t) => Size.lerp(begin, end, t)!;
}
// 常用 Curve(缓动曲线)
Curves.linear // 匀速
Curves.easeIn // 加速
Curves.easeOut // 减速
Curves.easeInOut // 先加速后减速(最常用)
Curves.bounceOut // 弹跳效果
Curves.elasticOut // 弹性效果
Curves.decelerate // 急减速
3.3 AnimationController 常用方法
dart
_controller.forward(); // 正向播放
_controller.reverse(); // 反向播放
_controller.repeat(); // 循环播放
_controller.repeat(reverse: true); // 往复循环
_controller.reset(); // 重置到起始状态
_controller.stop(); // 停止
_controller.animateTo(0.5); // 动画到指定进度
四、过渡动画
4.1 Hero 动画
Hero 动画实现页面间的共享元素过渡:
dart
// 列表页
Hero(
tag: 'product_image_${product.id}', // 唯一标识符
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(product.imageUrl, fit: BoxFit.cover),
),
)
// 详情页
Hero(
tag: 'product_image_${product.id}', // 相同 tag
child: Image.network(product.imageUrl, fit: BoxFit.cover),
)
// 自定义 Hero 动画效果
Hero(
tag: heroTag,
flightShuttleBuilder: (
flightContext, animation, direction, fromContext, toContext,
) {
return ScaleTransition(
scale: animation,
child: toContext.widget,
);
},
child: image,
)
4.2 PageRouteBuilder(自定义页面过渡)
dart
// 滑入过渡
class SlidePageRoute<T> extends PageRouteBuilder<T> {
final Widget page;
final Offset beginOffset;
SlidePageRoute({
required this.page,
this.beginOffset = const Offset(1, 0),
}) : super(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: beginOffset,
end: Offset.zero,
).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)),
child: child,
);
},
transitionDuration: const Duration(milliseconds: 300),
);
}
// 使用
Navigator.push(
context,
SlidePageRoute(page: const DetailPage()),
)
// 淡入淡出
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => const NextPage(),
transitionsBuilder: (context, animation, _, child) {
return FadeTransition(opacity: animation, child: child);
},
),
)
4.3 AnimatedBuilder 与 AnimatedWidget
dart
// AnimatedBuilder:适合在 build 方法中使用动画值
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * pi,
child: child,
);
},
child: const Icon(Icons.refresh, size: 32), // 静态子 Widget
)
// AnimatedWidget:自定义动画 Widget(继承方式)
class RotatingIcon extends AnimatedWidget {
const RotatingIcon({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Transform.rotate(
angle: animation.value * 2 * pi,
child: const Icon(Icons.refresh),
);
}
}
五、高级动画
5.1 Rive 动画集成
yaml
# pubspec.yaml
dependencies:
rive: ^0.12.0
dart
import 'package:rive/rive.dart';
class RiveAnimationWidget extends StatefulWidget {
@override
State<RiveAnimationWidget> createState() => _RiveAnimationWidgetState();
}
class _RiveAnimationWidgetState extends State<RiveAnimationWidget> {
SMIBool? _isHovered;
void _onRiveInit(Artboard artboard) {
final controller = StateMachineController.fromArtboard(
artboard, 'State Machine',
);
if (controller != null) {
artboard.addController(controller);
_isHovered = controller.findInput<bool>('isHovered') as SMIBool;
}
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => _isHovered?.change(true),
onExit: (_) => _isHovered?.change(false),
child: RiveAnimation.asset(
'assets/animations/button.riv',
onInit: _onRiveInit,
),
);
}
}
5.2 Lottie 动画集成
yaml
dependencies:
lottie: ^3.0.0
dart
import 'package:lottie/lottie.dart';
// 简单播放
Lottie.asset(
'assets/animations/loading.json',
width: 200,
height: 200,
fit: BoxFit.contain,
repeat: true,
)
// 控制播放
class SuccessAnimation extends StatefulWidget {
@override
State<SuccessAnimation> createState() => _SuccessAnimationState();
}
class _SuccessAnimationState extends State<SuccessAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
@override
Widget build(BuildContext context) {
return Lottie.asset(
'assets/animations/success.json',
controller: _controller,
onLoaded: (composition) {
_controller
..duration = composition.duration
..forward().then((_) => _controller.stop()); // 播放一次停止
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
小结
| 类型 | Widget/API | 适用场景 |
|---|---|---|
| CustomPainter | CustomPaint |
自定义图形绘制 |
| 隐式动画 | AnimatedContainer, AnimatedOpacity |
简单属性过渡 |
| AnimatedSwitcher | AnimatedSwitcher |
切换不同子组件 |
| 显式动画 | AnimationController + Tween |
精确控制动画 |
| Hero | Hero |
页面间共享元素 |
| 路由过渡 | PageRouteBuilder |
自定义页面切换 |
| AnimatedBuilder | AnimatedBuilder |
在 build 中使用动画值 |
| Rive | RiveAnimation |
交互式矢量动画 |
| Lottie | Lottie.asset |
After Effects 导出动画 |
👉 下一章:三、状态管理(State Management)