Flutter 基于CustomPainter的生成式静态图如何实现无限动画
实现无限动画最直接简便的方案
完全复用现有CustomPainter的绘制逻辑,用Flutter SDK内置的AnimationController做驱动即可,不需要引入任何第三方依赖,改造成本极低。
核心实现逻辑
- 把原来的无状态页面组件改造为支持动画驱动的有状态组件,利用
SingleTickerProviderStateMixin获取跟屏幕刷新率同步的动画时钟 - 创建可无限循环的
AnimationController,通过repeat()方法自动持续运行,不需要手动重置动画进度 - 把动画运行中持续变化的进度值作为参数传入自定义Painter,替换原代码中写死的路径坐标计算参数,实现路径的动态形变
- 优化
shouldRepaint逻辑,仅当传入的动画值发生变化时才触发重绘,减少不必要的绘制开销
关键代码改动
1. 页面组件改造
替换原来的StatelessWidget,通过AnimatedBuilder自动把动画值传递给CustomPaint,不需要手动绑定监听器和调用setState:
class PathExample extends StatefulWidget { @override _PathExampleState createState() => _PathExampleState(); } class _PathExampleState extends State<PathExample> with SingleTickerProviderStateMixin { late final AnimationController _animController; @override void initState() { super.initState(); // 动画时长可自行调整,数值越大动画流速越慢 _animController = AnimationController( vsync: this, duration: const Duration(seconds: 8), )..repeat(); // 直接实现无限循环运行 } @override void dispose() { _animController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animController, builder: (context, child) { return CustomPaint( painter: PathPainter(animValue: _animController.value), ); }, ); } }
2. 自定义Painter改造
给Painter增加动画值入参,把固定的坐标计算替换为跟动画值联动的动态计算,同时优化原有代码的性能问题:
import 'dart:math'; class PathPainter extends CustomPainter { // 动态传入的动画进度值,取值范围0~1 final double animValue; // 把固定对象提到类成员,避免每帧绘制时重复创建造成GC卡顿 final Paint _basePaint = Paint(); final Random _fixedRandom = Random(42); PathPainter({required this.animValue}); @override void paint(Canvas canvas, Size size) { _basePaint ..color = Colors.grey[200] ..style = PaintingStyle.fill ..strokeWidth = 0.0; canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), _basePaint); Path path2 = Path(); for (double i = 0; i < 200; i++) { // 叠加动画值到正弦函数的相位,实现无跳变的波浪滚动效果 final waveOffset = sin(i / 2.14 + animValue * 2 * pi) * 45 + 200; final lineOffset = sin(i / 2.14 + animValue * 2 * pi) * 50 + 100; path2.moveTo(waveOffset, i * 12); path2.lineTo(lineOffset, i * 10); _basePaint ..style = PaintingStyle.stroke ..color = Colors.red; canvas.drawPath(path2, _basePaint); } Path path = Path(); _basePaint ..color = Colors.blue ..style = PaintingStyle.stroke; for (double i = 0; i < 30; i++) { path.moveTo(100, 50); // 叠加动画值到贝塞尔曲线的控制点,实现曲线的动态形变 path.cubicTo( -220, 300, 500, 600 - i * 20 + animValue * 120, size.width / 2 + 50, size.height - 50 ); canvas.drawPath(path, _basePaint); } } // 仅当动画值变化时才触发重绘,避免无意义的绘制 @override bool shouldRepaint(covariant PathPainter oldDelegate) { return oldDelegate.animValue != animValue; } }
注意事项
- 不要在
paint方法内部创建新的Paint、Random、Path实例,把固定不变的对象提到类成员位置,降低每帧的内存分配开销 - 把正弦类的周期动画计算映射到
0~2*pi的相位区间,保证动画循环时不会出现位置跳变 - 动画运行的流畅度可以通过调整AnimationController的duration参数优化,不需要改动现有绘制逻辑
内容的提问来源于stack exchange,提问作者RonH
相关产品推荐
相关产品推荐

