跳至主要內容

Flutter 动画分析之 CustomPaint

JI,XIAOYONG...大约 9 分钟

本文讨论的 Flutter 动画主要限定在:随着每一帧的变化,修改 Flutter Widget 的大小、颜色、位置等属性,使之看起来从一种状态渐变为另外一种状态 这一范围。

根据之前的分析,关于 Flutter 中的 Widget 动画,大体可以分为三大类:

  • 隐式动画,以 ImplicitlyAnimatedWidget 及其子类为代表。特点是当涉及到的属性变化后,这些 Widget 会 自动渐变到新的属性,使用者只能设置动画的 Duration、Tween、Curve 等,而无法主动终止、反向执行动画。

    涉及到的类主要有 TweenAnimationBuilder 以及一系列以 AnimatedFoo 命名的类。

  • 显式动画,以 AnimatedWidget 及其子类为代表,需要配合 AnimationController 使用。特点是 当 AnimationController 的值变化时,Widget 中对应的属性也会随之变化

    涉及到的类主要有 AnimationBuilder/AnimatedWidget 以及一系列 FooTransition 命名的类。

  • 自定义动画,如果上述两种方式还无法满足需求,则可以使用 CustomPaint + CustomPainter + Listenable(比如 AnimationController)实现动画,特点是实现方式灵活,但同时也比上述两者难度高一些。

Flutter 中这些与动画有关的类如何选择,Flutter 官方给了一张图以供参考:

如何实现 Flutter 中的动画
如何实现 Flutter 中的动画

本文将着重分析使用 CustomPaint 实现自定义动画,涉及到的类以及他们的关系图如下:

CustomPaint 自定义动画
CustomPaint 自定义动画

源码分析

先来看一个使用 CustomPaint 实现的动画(动画的内容是一个反复变大又缩小的蓝色小球):

// 1. 创建 AnimationController 用来触发 CustomPaint 绘制
late final AnimationController _controller =
      AnimationController(vsync: this, duration: const Duration(seconds: 5))
        ..repeat(reverse: true);

// 2. 创建自定义的 CustomPainter 类
class _SampleCustomPainter extends CustomPainter {
  // 注意这里给父类传入了 Listenable 类型的 repaint
  _SampleCustomPainter(this.progress) : super(repaint: progress) {
    _paint = ui.Paint()..color = Colors.blue;
  }

  Animation<double> progress;
  late Paint _paint;

  
  void paint(Canvas canvas, Size size) {
    // 在这里实现真正的绘制逻辑
    var minSize = size.width > size.height ? size.height : size.width;
    var radius = minSize * (0.2 + 0.8 * progress.value) / 2;
    canvas.drawCircle(size.center(Offset.zero), radius, _paint);
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

// 3. 使用 CustomPaint 和 CustomPainter 等实现动画
CustomPaint(
    painter: _SampleCustomPainter(_controller),
    size: const Size(100, 100),
    )

上述代码是我们使用 CustomPaint 实现自定义动画的常见用法,接下来我们逐一分析上述涉及到的类的作用。

CustomPaint

A widget that provides a canvas on which to draw during the paint phase.

CustomPaint 继承自 SingleChildRenderObjectWidget,其内部持有 CustomPainter 并将其传入创建 RenderCustomPaint 以获取 Canvas 用于绘制内容。

class CustomPaint extends SingleChildRenderObjectWidget {

  const CustomPaint({
    Key? key,
    this.painter,// 绘制背景
    this.foregroundPainter,// 绘制前景
    this.size = Size.zero,// 优先取 child.Size,其为 null 才取这里的 size
    this.isComplex = false,// 动画是否复杂到需要合成系统设置缓存
    this.willChange = false,// 告诉光栅 raster 此 painting 是否会在下一帧变化
    Widget? child,// 可选
  }) : super(key: key, child: child);

  RenderCustomPaint createRenderObject(BuildContext context) {...}
}

可以看到 CustomPaint 内部并没有太多逻辑,控制 CustomPainter 绘制的主要逻辑都在 RenderCustomPaint 中。

RenderCustomPaint

Provides a canvas on which to draw during the paint phase.

作为 RenderObject 的 RenderCustomPaint,负责实际计算 size、在 Flutter 框架 paint 阶段安排 painter、foregroundPainter 以及 child 的绘制。

以 CustomPaint 传入的 CustomPainter? painter 为例,在 RenderCustomPaint 的构造方法中它被赋值给_painter,随后当 attach 到 RenderObject Tree 时,对 CustomPainter 添加了监听:

class RenderCustomPaint extends RenderProxyBox {

  CustomPainter? get painter => _painter;

  
  void attach(PipelineOwner owner) {
    super.attach(owner);
    // 这里对 CustomPainter 添加监听,只要值变化就 markNeedsPaint
    _painter?.addListener(markNeedsPaint);
    _foregroundPainter?.addListener(markNeedsPaint);
  }

  
  void detach() {
    _painter?.removeListener(markNeedsPaint);
    _foregroundPainter?.removeListener(markNeedsPaint);
    super.detach();
  }


}

从上述代码可以看出,RenderCustomPaint 对 CustomPainter 添加监听,只要值变化就执行 markNeedsPaint;

而如果是更新了 CustomPainter,则会执行 RenderCustomPainter.painter 方法,最终在_didUpdatePainter 方法中重写对新的 CustomPainter 添加监听以触发 markNeedsPaint:

set painter(CustomPainter? value) {
    if (_painter == value)
      return;
    final CustomPainter? oldPainter = _painter;
    _painter = value;
    _didUpdatePainter(_painter, oldPainter);
  }

  void _didUpdatePainter(CustomPainter? newPainter, CustomPainter? oldPainter) {
    // Check if we need to repaint.
    if (newPainter == null) {
      assert(oldPainter != null); // We should be called only for changes.
      markNeedsPaint();
    } else if (oldPainter == null ||
        newPainter.runtimeType != oldPainter.runtimeType ||
        newPainter.shouldRepaint(oldPainter)) {
      markNeedsPaint();
    }
    if (attached) {
      oldPainter?.removeListener(markNeedsPaint);
      newPainter?.addListener(markNeedsPaint);
    }

    // Check if we need to rebuild semantics.
    if (newPainter == null) {
      assert(oldPainter != null); // We should be called only for changes.
      if (attached)
        markNeedsSemanticsUpdate();
    } else if (oldPainter == null ||
        newPainter.runtimeType != oldPainter.runtimeType ||
        newPainter.shouldRebuildSemantics(oldPainter)) {
      markNeedsSemanticsUpdate();
    }
  }

上述两种方法最终都会直接触发 paint 阶段(跳过了 build 和 layout 阶段),最终由 Flutter Framework 调用 RenderCustomPainter.paint 方法:

  
  void paint(PaintingContext context, Offset offset) {
    // 1. 先绘制背景
    if (_painter != null) {
      _paintWithPainter(context.canvas, offset, _painter!);
      _setRasterCacheHints(context);
    }
    // 2. 再通过父类绘制 child
    super.paint(context, offset);
    // 3. 最后绘制前景
    if (_foregroundPainter != null) {
      _paintWithPainter(context.canvas, offset, _foregroundPainter!);
      _setRasterCacheHints(context);
    }
  }

  void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
    late int debugPreviousCanvasSaveCount;
    canvas.save();
    if (offset != Offset.zero)
      canvas.translate(offset.dx, offset.dy);
    // 调用 CustomPainter.paint 方法绘制内容
    painter.paint(canvas, size);
    canvas.restore();
  }

以上就是 RenderCustomPaint 使用 CustomPainter 实现绘制的过程,大体分为 3 部分:

  • 在 attach 方法中监听从 CustomPaint 传入的 CustomPainter,一旦其有变化就执行 markNeedsPaint 方法,引导 Flutter 框架重新绘制内容。
  • 如果中间通过 RenderCustomPainter.painter/foregroundPaint 更改 CustomPainter,则会重新监听以触发 markNeedsPaint。
  • 当 Flutter 框架执行重绘时,会通过 RenderCustomPaint.paint 方法最终调用 CustomPainter.paint 方法绘制内容。

除此之外,RenderCustomPainter 还通过 computeMinIntrinsicWidth/computeMaxIntrinsicHeight 等方法计算合适的 Size,以及使用 hitTestChildren/hitTestSelf 等处理点击事件等等。

CustomPainter

The interface used by CustomPaint (in the widgets library) and RenderCustomPaint (in the rendering library).

CustomPainter 提供 paint 方法让使用者使用 Canvas 绘制内容;继承自 Listenable,所以可以通过 addListener/removeListener 方法监听。

abstract class CustomPainter extends Listenable {
  const CustomPainter({ Listenable? repaint }) : _repaint = repaint;

  // 可以看到,如果 repaint 不为 null,则会监听其变化
  final Listenable? _repaint;
  void addListener(VoidCallback listener) => _repaint?.addListener(listener);
  void removeListener(VoidCallback listener) => _repaint?.removeListener(listener);

  // 点击事件以及语义等等,可以根据需要实现
  bool? hitTest(Offset position) => null;
  SemanticsBuilderCallback? get semanticsBuilder => null;
  bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => shouldRepaint(oldDelegate);

  // 子类必须实现的方法
  bool shouldRepaint(covariant CustomPainter oldDelegate);
  void paint(Canvas canvas, Size size);
}

CustomPainter 有两种使用方式:

  • 继承自 CustomPainter(推荐),可以传入 Listenable,这样 RenderCustomPaint 监听的便是这个 Listenable 对象。

  • 继承 Listenable 或其子类,并实现 CustomPainter 接口Dart 语言特性,会要求实现其所有方法属性),这样监听的便是这个实现类对象本身。

当通知 listener 时会通过 RenderCustomPaint 执行 markNeedsPaint,触发 Flutter Framework 重新安排绘制,最终执行 CustomPainter.paint 方法重新绘制内容。

无论哪种实现方式,都必须实现 shouldRepaint 和 paint 两个方法。

  • bool shouldRepaint(covariant CustomPainter oldDelegate) 每当新的 CustomPainter 提供给 RenderCustomPaint 时都会调用此方法,再判断是否调用 paint 方法。

    如果前后 CustomPainter 的信息不同影响到绘制,则应该返回 true;否则返回 false 则可能导致 paint 方法被省略。

    即使此方法返回 false,paint 也可能会被回调(Listenable 调用 listener 触发);如果 size 变化等情况下会直接调用 paint 方法。

  • void paint(Canvas canvas, Size size) 每当对象需要 paint 的时候都会调用。

    传入的 Canvas 的坐标系以左上角为原点,范围无限大,但 box 的大小为 size,绘制的内容应当在 size 范围内,否则可能有性能问题,可以在最开始使用 Canvas.clipRect 限制绘制范围。

Canvas

An interface for recording graphical operations.

当使用 PaintingContext.canvas 方法获取 Canvas 时,(如果 canvas 为 null)会创建 ui.PictureRecorder() 并以此创建 Canvas。

class PaintingContext extends ClipContext {
  
  Canvas get canvas {
    if (_canvas == null)
      _startRecording();
    assert(_currentLayer != null);
    return _canvas!;
  }

  void _startRecording() {
    assert(!_isRecording);
    _currentLayer = PictureLayer(estimatedBounds);
    _recorder = ui.PictureRecorder();
    _canvas = Canvas(_recorder!);
    _containerLayer.append(_currentLayer!);
  }
}

PictureRecorder 使用 Canvas 记录图形操作的接口,在PictureRecorder.endRecording方法中使用其创建 Picture,后者被 SceneBuilder 用来在SceneBuilder.build()方法中创建 Scene。
最终在FlutterView.render方法中,Scene 被 GPU 绘制在屏幕上。

Canvas 有常用的方法:

  • save() 将当前的 transform 和 clip 等的复制保存在 save stack 上面,必须使用 restore()方法恢复

  • saveLayer(),与 save()方法类似,但是执行以后,后续操作都是在一个新的 layer 上面进行,相当于一个独立的图层。

    可以通过 Paint.colorFilter、Paint.blendMode 等属性配置其与之前图层的叠加方式。
    比较耗性能,好处是可以对后续操作绘制的内容统一处理(比如,单独画两个圆,然后抗锯齿会分别执行两次,可能会出现毛刺,而使用 saveLayer 后这两个圆相当于一个整体,使用抗锯齿效果会很好)。

  • restore(),pop 掉当前的 save stack,save 和 saveLayer 方法与此方法一一对应。saveLayer 创建的 layer 会和之前的 layer 合并。

  • translate/scale/rotate 对画布进行平移、缩放、旋转操作

  • clipRect/clipRRect/clipPath 裁剪画布的绘制范围

  • drawColor/drawLine/drawRRect/drawRect... 等等绘制各种形状

    绘制文字应该使用 TextPainter,不推荐使用 drawParagraph

  • skew/transform 倾斜/变换画布

小结

到这里我们差不过分析了刚开始的 demo 涉及到的类,总结一下他们各自的角色:

  • AnimationController 对象(也即 Listenable 对象),用来监听 Flutter Framework 的帧刷新,根据设定的时长、曲线等通知 listener,以达到控制动画的效果。

  • CustomPaint,作为 SingleChildRenderObjectWidget,主要作用是将 CustomPainter、Widget child 等各类参数传递给 RenderCustomPaint。

  • RenderCustomPainter,作为 RenderProxyBox,优先以 Widget child 的大小为准,会监听传入的 CustomPainter 对象,并在其变化时触发 Flutter Framework 安排重新绘制(跳过 build 和 layout 阶段,直接进入 paint 阶段);

    当需要绘制时,会依次安排背景、child、前景的绘制。而这里的背景和前景都由传入的 CustomPainter 负责实际绘制。

  • CustomPainter,本身是 Listenable 的子类,可以被 RenderCustomPaint 监听,其 paint 方法实际负责绘制内容。

    使用是可以继承此类(推荐),也可以当做接口实现所有方法。

  • Canvas,Paint 则是负责实际绘制。

总结

到本文为止,我们分析了 Flutter 动画实现的各种方式及其原理:

Flutter Widget 动画,是AnimationControlleropen in new window通过 Ticker 监听 Flutter 屏幕刷新,然后根据Tween/Curveopen in new window/Duration 等一系列参数计算出实时的值,Flutter 的 Widget 则根据这些值计算、修改对应属性从而实现动画效果。

Flutter 内置的动画分为隐式动画和显式动画open in new window两种,如果这些动画还无法满足需求,可以使用 CustomPaint 实现自定义动画(本文内容)。

Flutter 中的Hero 动画open in new window便是使用显式动画中的 AnimationBuilder、Tween 等实现的。

参考资料

CustomPaint api.flutter.devopen in new window

Canvas api.flutter.devopen in new window

What is Canvas.save and Canvas.restore? stackoverflow.comopen in new window

文章标题:《Flutter 动画分析之 CustomPaint》
本文作者: JI,XIAOYONG
发布时间: 2022/08/29 20:57:16 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/adf541c0.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8