零:前言
可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint
组件,自定义 CustomPainter
对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来
的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint
组件来画的,其实 CustomPaint
组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试
、调试
及源码分析
来给出一些在绘制时被忽略
或从未知晓
的东西,而有些要点如果被忽略,就很可能出现问题。
希望在观看此篇前,你已经看过前面文章的铺垫 。上回说到与 CustomPainter
关系最为密切的是 RenderCustomPaint
这个渲染对象。我们都知道,通过 CustomPainter#paint
方法可以获取到 Canvas 对象进行绘制操作,但你有么有想过,这个 Canvas 是从何而来的?CustomPainter#paint
方法又是在哪里回调的?shouldRepaint
到底是在哪里起的作用?这些都会在本文的探索中给出答案。
为了更方便探索 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;
}
}
复制代码
想要进行分析,最有效的方式便是 调试
,在 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
对象。
在 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,
);
}
在 _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 对象是何时被创建的。
遇事不决,先看源码,源码中 20 个基于 CustomPainter 绘制的组件,我们可以从其中来看到正规的适用方式。那个简单的 _GridPaperPainter
来看,它在 shouldRepaint
中进行的处理是: 只要属性成员和旧的画板对象有所不同,就返回 true 。 如果完全一致,则返回 false。这基本上是作为画板而言,刻在 DNA 里的操作了。
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 ~