Flutter 动画分析之 CustomPaint
本文讨论的 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 官方给了一张图以供参考:
本文将着重分析使用 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;
@override
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);
}
@override
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;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
// 这里对 CustomPainter 添加监听,只要值变化就 markNeedsPaint
_painter?.addListener(markNeedsPaint);
_foregroundPainter?.addListener(markNeedsPaint);
}
@override
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 方法:
@override
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 {
@override
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 动画,是AnimationController通过 Ticker 监听 Flutter 屏幕刷新,然后根据Tween/Curve/Duration 等一系列参数计算出实时的值,Flutter 的 Widget 则根据这些值计算、修改对应属性从而实现动画效果。
Flutter 内置的动画分为隐式动画和显式动画两种,如果这些动画还无法满足需求,可以使用 CustomPaint 实现自定义动画(本文内容)。
Flutter 中的Hero 动画便是使用显式动画中的 AnimationBuilder、Tween 等实现的。