You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.26 05:06:08