零:前言
1. 系列引言
可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint
组件,自定义 CustomPainter
对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来
的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint
组件来画的,其实 CustomPaint
组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试
、调试
及源码分析
来给出一些在绘制时被忽略
或从未知晓
的东西,而有些要点如果被忽略,就很可能出现问题。
2.前情回顾
希望在观看此篇前,你已经看过前面文章的铺垫 。上回说到与 CustomPainter
关系最为密切的是 RenderCustomPaint
这个渲染对象。我们都知道,通过 CustomPainter#paint
方法可以获取到 Canvas 对象进行绘制操作,但你有么有想过,这个 Canvas 是从何而来的?CustomPainter#paint
方法又是在哪里回调的?shouldRepaint
到底是在哪里起的作用?这些都会在本文的探索中给出答案。
一、CustomPainter#paint 方法探索
1. 测试代码
为了更方便探索 CustomPainter
的内部机制,这里使用最精简的代码,摒除其余干扰信息。如下代码直接将 CustomPaint
组件传给 runApp
方法,运行效果如下:
void main() => runApp(CustomPaint(
painter: ShapePainter(color: Colors.blue),
));
class ShapePainter extends CustomPainter {
final Color color;
ShapePainter({this.color});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = color;
canvas.drawCircle(Offset(100, 100), 50, paint);
}
@override
bool shouldRepaint(covariant ShapePainter oldDelegate) {
return oldDelegate.color != color;
}
}
复制代码
2. paint 方法的调试分析
想要进行分析,最有效的方式便是 调试
,在 paint
方法添加断点,调试信息如下。左侧是程序运行到 paint
时方法栈帧情况,当前 ShapePainter.paint
方法处于栈顶,其下的方法都是在方法栈中还未执行完毕的方法
,它们都在等着栈顶的方法退栈。所以可以从这里看出方法依次进栈
的顺序,从而很快了解 paint
是如何一步步被调用的。
RenderCustomPaint._paintWithPainter
在 ShapePainter.paint
之下,说明 ShapePainter.paint
是在该方法里被调用的。如下所示,点击栈帧中的方法时,会进行跳转。来到 RenderCustomPaint
类中的 _paintWithPainter
方法内,ShapePainter.paint
被调用的那一行,这就是 debug
的强大之处。
通过调试可以看到方法栈的调用情况,但很多方法在一块,会让人觉得很乱,有时走着走着自己就乱了,不知道在干嘛。所以在调试中有件一个很重要的事:就是认清我是谁
,我在哪里
,我要干什么
,这让你不会迷路。我们可以通过栈帧
看到当前方法所处的位置;另外,任何方法调用时,都是一个对象在调用,这个对象便是 this
,当我们迷路时,this 会成为指路明灯。通过下面计数器的图标,可以输入表达式和查看对象信息。查看 this 信息如下,当前对象为 RenderCustomPaint
类型,可以看到当前对象的成员信息。
这时我们知道了 ShapePainter.paint
是在 RenderCustomPaint._paintWithPainter
中被调用的,那么 _paintWithPainter
又是在哪调用的呢。同理,可以看下一个栈帧。它是在 RenderCustomPaint.paint
中被触发的。也就是说 RenderCustomPaint
作为一个 RenderObject
本应要处理绘制的任务,但是它将这个任务向外界暴露出去,由用户进行绘制处理。
而暴露给用户的抽象层便是 CustomPianter
,可以看出 CustomPianter#paint
回调的出去的 Canvas 是 RenderCustomPaint#paint
方法参数的 PaintingContext
中的 canvas
对象。
3.RendererBinding.drawFrame
在 runApp
方法中,会执行 WidgetsFlutterBinding#scheduleWarmUpFrame
开始调度绘制帧。
每次帧的回调会触发 RendererBinding#_handlePersistentFrameCallback
。在此方法中会执行 drawFrame
。至于 Flutter 框架层如何启动,初始化各个 Binding
,如何添加 _handlePersistentFrameCallback
回调的,本文就不详述了,着重在绘制的点。
---->[RendererBinding#_handlePersistentFrameCallback]----
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_scheduleMouseTrackerUpdate();
}
复制代码
这样我们方法 RendererBinding.drawFrame
,它的作用就是绘制帧。在这里触发了PipelineOwner.flushPaint
,从而吹响了绘制的号角。
PipelineOwner 中持有 _nodesNeedingPaint
对象,它是一个 RenderObject
列表,收集需要绘制的 RenderObject。在 PipelineOwner.flushPaint
中,会对收集到需要绘制的 RenderObject 使用 PaintingContext.repaintCompositedChild
静态方法进行绘制。可以看出当前的节点是 RenderView
,它的孩子是 RenderCustomPaint
这也就是当前 渲染树
的结构。RenderView
是在 Flutter 框架内部初始化的RenderObject, 它永远都是渲染树的根节点。
PipelineOwner
类中在允许绘制之前还有几个条件,1. 渲染对象的 _layer
属性非空;2. 渲染对象的 _needsPaint 属性为 true ;3.渲染对象持有的 PipelineOwner
为当前对象;4. 渲染对象的 _layer
成员的 _ower 非空。
---->[PipelineOwner#flushPaint]----
assert(node._layer != null);
if (node._needsPaint && node.owner == this) {
if (node._layer!.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
---->[AbstractNode#attached]----
bool get attached => _owner != null;
复制代码
可以回想一下上文中,RenderObject 对象的 markNeedsPaint
方法,就是在向 owner._nodesNeedingPaint
列表中添加渲染对象 。下面是 RenderObject#markNeedsPaint
去除断言后的所有代码。可以看出,自己 在被加入到owner 的待渲染列表
前,会有些条件。1. _needsPaint
属性为 false。 2. isRepaintBoundary
为 true。否则就让 父节点执行 markNeedsPaint
。
所以从这里可以看出:当一个 RenderObject
对象执行 markNeedsPaint
时,如果自身 isRepaintBoundary
为false,会向上寻找父级,直到有 isRepaintBoundary=true
为止。然后该父级节点被加入 _nodesNeedingPaint
列表中。
---->[RenderObject#markNeedsPaint]----
void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
if (isRepaintBoundary) {
if (owner != null) {
owner!._nodesNeedingPaint.add(this); //<--- 自己被加入 待渲染列表
owner!.requestVisualUpdate();
}
} else if (parent is RenderObject) {
final RenderObject parent = this.parent as RenderObject;
parent.markNeedsPaint();
} else {
if (owner != null)
owner!.requestVisualUpdate();
}
}
复制代码
repaintCompositedChild
是 PaintingContext
的静态方法,没有复杂的逻辑,只是调用了 _repaintCompositedChild
。
---->[PaintingContext#repaintCompositedChild]----
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
assert(child._needsPaint);
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
复制代码
4. 绘制上下文 PaintingContext 的诞生
在 _repaintCompositedChild
方法中除去断言后,所有代码如下:可以看到这里创建了 PaintingContext
,也就是 Canvas
的发源地。这里的 child
对象便是根渲染节点 RenderView
。可以看出 PaintingContext
类只是用于提供绘制的上下文,最终的绘制还是由 RenderObject
自身完成。
---->[PaintingContext#_repaintCompositedChild]----
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext? childContext,
}) {
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
child._layer = childLayer = OffsetLayer();
} else {
childLayer.removeAllChildren();
}
childContext ??= PaintingContext(child._layer!, child.paintBounds);//绘制上下文的创建
child._paintWithContext(childContext, Offset.zero); // RenderObject 绘制
childContext.stopRecordingIfNeeded();
}
复制代码
在 RenderObject#_paintWithContext
方法中做了很多断言的操作,其本身并没有什么复杂的逻辑,就调用了一下该类的 paint
方法,将上面传来的绘制上下文回调出去。
---->[RenderObject#_paintWithContext]----
void _paintWithContext(PaintingContext context, Offset offset) {
if (_needsLayout)
return;
RenderObject? debugLastActivePaint;
_needsPaint = false;
try {
paint(context, offset); // <--- 调用 paint
} catch (e, stack) {
_debugReportException('paint', e, stack);
}
}
复制代码
在 RenderView.paint
方法中,会触发 PaintingContext.paintChild
方法。然后会触发渲染树下一节点的绘制。我们知道,下一个节点就是 RenderCustomPaint
。
从这里可以看出,如果 child.isRepaintBoundary
为 true 就不会触发 child
的绘制,而是使用 _compositeChild
进行合成,将 child._layer
添加到 _containerLayer
中,这样可以避免渲染对象的绘制。如果 child.isRepaintBoundary
为 false,会执行 _paintWithContext
方法进行绘制,也就是当前的情况。
---->[RenderObject#paintChild]----
void paintChild(RenderObject child, Offset offset) {
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
}
void _compositeChild(RenderObject child, Offset offset) {
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
}
final OffsetLayer childOffsetLayer = child._layer as OffsetLayer;
childOffsetLayer.offset = offset;
appendLayer(child._layer!); // 添加到 _containerLayer 中
}
@protected
void appendLayer(Layer layer) {
layer.remove();
_containerLayer.append(layer);
}
复制代码
这样一来,一条路就畅通了,现在可以自己回味一下从 RendererBinding.drawFrame
一路过来发生的事情。多调试调试,栈帧,会为你诉说它所经历的 故事
。
当前的渲染树只有 RenderView
和 RenderCustomPaint
两个节点。在绘制时 RenderView.paint
先入栈 , RenderCustomPaint.paint
后入栈,这说明在前面的节点会一直等待后面的节点绘制完毕,自己的绘制才算结束。现在让当栈帧依次出栈,当 pipelineOwner.flushPaint()
执行完毕,屏幕上就会出现绘制的图形。这么我们就了解了一下 CustomPainter#paint
是什么时候被调用的,以及 Canvas 对象是何时被创建的。
二、 CustomPainter#shouldRepaint 方法探索
1.源码中对 shouldRepaint 的使用
遇事不决,先看源码,源码中 20 个基于 CustomPainter 绘制的组件,我们可以从其中来看到正规的适用方式。那个简单的 _GridPaperPainter
来看,它在 shouldRepaint
中进行的处理是: 只要属性成员和旧的画板对象有所不同,就返回 true 。 如果完全一致,则返回 false。这基本上是作为画板而言,刻在 DNA 里的操作了。
2. 从源码认识 shouldRepaint
CustomPainter#shouldRepaint
在整个 Flutter 框架中只有两处使用。第一个是在 CustomPaintershouldRebuildSemantics
中,会默认调用它来进行判断。
第二个就是在 RenderCustomPaint#_didUpdatePainter
中 ,这个方法的触发,是在为 RenderCustomPaint 设置新画板
时。这里的 oldPainter
也就是之前的画板。
set painter(CustomPainter? value) {
if (_painter == value)
return;
final CustomPainter? oldPainter = _painter;
_painter = value;
_didUpdatePainter(_painter, oldPainter);
}
复制代码
我们来仔细看一下 _didUpdatePainter
这个方法,入参是新旧两个画板。[1]. 如果新画板为 null ,重新绘制来清除旧画。[2]. 如果新画板为 null 、新旧画板运行时类型不一致、shouldRepaint
返回值为 true ,这三个条件满足其一,就可以通过 markNeedsPaint
让 RenderCustomPaint
加入重绘渲染列表。
void _didUpdatePainter(CustomPainter? newPainter, CustomPainter? oldPainter) {
if (newPainter == null) {
markNeedsPaint();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRepaint(oldPainter)) {
markNeedsPaint();
}
if (attached) {
oldPainter?.removeListener(markNeedsPaint);
newPainter?.addListener(markNeedsPaint);
}
if (newPainter == null) {
if (attached)
markNeedsSemanticsUpdate();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRebuildSemantics(oldPainter)) {
markNeedsSemanticsUpdate();
}
}
复制代码
到这里再来回答,shouldRepaint
返回 false,就一定不会重绘当前画板吗?答案以及很明显了。并非全然,一者 oldPainter == null
和 newPainter.runtimeType != oldPainter.runtimeType
两个条件如果满足也是可以的。但不要忽略一个要点,这个方法只是在 set painter
时被触发。还有别的情况可能引起绘制对象重绘,比如父级渲染对象的刷新、_painter
基于监听器的刷新,这些是 shouldRepaint
无法控制的。
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_painter?.addListener(markNeedsPaint);
_foregroundPainter?.addListener(markNeedsPaint);
}
复制代码
所以 shouldRepaint
并非是一个控制画板刷新的万金油。我们需要根据情况进一步处理,至于怎么处理,在上面我们讲到过 RenderObject 中有一个属性可以控制重绘,它就是 isRepaintBoundary
。现在对于 CustomPainter
最核心的两个方法已经介绍完毕,你应该可以回答出本篇一开始的那几个问题了。在下一篇我们将进一步去探索 Flutter 绘制的奥秘,在什么情况下会触发 shouldRepaint
无法控制的刷新,我们又该如何去控制。
@张风捷特烈 2021.01.11 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~
本文同步分享在 博客“”(JueJin)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。