本文是『 深刻浅出 Flutter Framework 』系列文章的第五篇,主要目的是为后面介绍 RenderObject 做准备。 文章对 PaintingContext 进行了较详细的分析,主要包括在 Rendering Pipeline 中 PaintingContext 是如何配合 RenderObject 进行绘制的,同时对一些基础概念进行了简要的介绍(如:Canvas、Picture、PictureRecorder、SceneBuilder 以及 Scene 等)。git
本文同时发表于个人我的博客github
本系列文章将深刻 Flutter Framework 内部逐步去分析其核心概念和流程,主要包括:canvas
『 Widget 』—『 Element 』—『 RenderObject 』可称之为 Flutter Framework『三剑客』,其中 Widget、Element 都已介绍过,而 RenderObject 在这三者中属于最核心、最复杂的,涉及 Layout、Paint 等核心流程。 为了更好、更流畅地去理解 RenderObject,在正式介绍以前,须要作些准备工做,本文介绍的 PaintingContext 在 RenderObject 的绘制流程上扮演了重要角色。api
『Painting Context』,其名称已说明了一些事情:绘制上下文,最简单的理解就是为绘制操做 (Paint) 提供了场所或者说环境 (上下文)。 其主要职责包括:app
PaintingContext
继承自ClipContext
,ClipContext
是抽象类,主要提供了几个与裁剪 (Clip) 有关的辅助方法;PictureLayer _currentLayer
、ui.PictureRecorder _recorder
以及Canvas _canvas
用于具体的绘制操做;ContainerLayer _containerLayer
,「Layer Subtree」的根节点,由PaintingContext
构造函数传入,通常传入的是RenderObject._layer
。RenderObject 与 Layer 是多对一的关系,即多个 RenderObject 绘制在一个 Layer 上。函数
在上一小节中说起一些基础的概念,本小节对它们逐一进行简要介绍。post
Canvas
是 Engine(C++) 层到 Framework(Dart) 层的桥接,真正的功能在 Engine 层实现。ui
下文将要出现的
Picture
、PictureRecorder
、SceneBuilder
以及SceneBuilder
都属于Engine(C++) 层到 Framework(Dart) 层的桥接。this
Canvas 向 Framework 层曝露了与绘制相关的基础接口,如:draw*
、clip*
、transform
以及scale
等,RenderObject 正是经过这些基础接口完成绘制任务的。spa
经过这套接口进行的全部操做都将被
PictureRecorder
记录下来。
Canvas(PictureRecorder recorder, [ Rect cullRect ]){}
复制代码
如上,在Canvas
初始化时须要指定PictureRecorder
,用于记录全部的「graphical operations」。
除了正常的绘制操做(draw*
),Canvas 还支持矩阵变换(transformation matrix)、区域裁剪(clip region),它们将做用于其后在该 Canvas 上进行的全部绘制操做。 下面列举部分方法,以便有更直观的感觉:
void scale(double sx, [double sy]);
void rotate(double radians) native;
void transform(Float64List matrix4);
void clipRect(Rect rect, { ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true });
void clipPath(Path path, {bool doAntiAlias = true});
void drawColor(Color color, BlendMode blendMode);
void drawLine(Offset p1, Offset p2, Paint paint);
void drawRect(Rect rect, Paint paint);
void drawCircle(Offset c, double radius, Paint paint);
void drawImage(Image image, Offset p, Paint paint);
void drawParagraph(Paragraph paragraph, Offset offset);
复制代码
其本质是一系列「graphical operations」的集合,对 Framework 层透明。 Future<Image> toImage(int width, int height)
,经过toImage
方法能够将其记录的全部操做经光栅化后生成Image
对象。
其主要做用是记录在Canvas
上执行的「graphical operations」,经过Picture#endRecording
最终生成Picture
。
一样对 Framework 层透明,是一系列 Picture、Texture 合成的结果。
An opaque object representing a composited scene.
UI 帧刷新时,在 Rendering Pipeline 中 Flutter UI 经 build、layout、paint 等步骤后最终生成 Scene。 其后经过window.render
将该 Scene 送入 Engine 层,最终经 GPU 光栅化后显示在屏幕上。
用于将多个图层(Layer)、Picture、Texture 合成为 Scene。
void addPicture(Offset offset, Picture picture, { bool isComplexHint = false, bool willChangeHint = false });
void addTexture(int textureId, { Offset offset = Offset.zero, double width = 0.0, double height = 0.0 , bool freeze = false});
复制代码
经过addPicture
、addTexture
能够引入要合成的 Picture、Texture。
同时,SceneBuilder 还会维护一个图形操做 stack:
pushTransform
pushOffset
pushClipRect
...
pop
复制代码
这些操做主要用于OffsetLayer
、ClipRectLayer
等。
是否是以为很抽象,晕乎乎的! 下面经过一个小例子将它们串起来,真实感觉一下。
void main() {
PictureRecorder recorder = PictureRecorder();
// 初始化 Canvas 时,传入 PictureRecorder 实例
// 用于记录发生在该 canvas 上的全部操做
//
Canvas canvas = Canvas(recorder);
Paint circlePaint= Paint();
circlePaint.color = Colors.blueAccent;
// 调用 Canvas 的绘制接口,画一个圆形
//
canvas.drawCircle(Offset(400, 400), 300, circlePaint);
// 绘制结束,生成Picture
//
Picture picture = recorder.endRecording();
SceneBuilder sceneBuilder = SceneBuilder();
sceneBuilder.pushOffset(0, 0);
// 将 picture 送入 SceneBuilder
//
sceneBuilder.addPicture(Offset(0, 0), picture);
sceneBuilder.pop();
// 生成 Scene
//
Scene scene = sceneBuilder.build();
window.onDrawFrame = () {
// 将 scene 送入 Engine 层进行渲染显示
//
window.render(scene);
};
window.scheduleFrame();
}
复制代码
仅仅是为了演示,在平常开发中并不须要直接操做这些基础 API。
本小节介绍的绘制流程,仅局限于 PaintingContext 周围,更完整的流程将在介绍 RenderObject 时进行分析。
PaintingContext 与 RenderObject 是什么关系? 从『类间关系』角度看,它们之间是依赖关系,即 RenderObject 依赖于 PaintingContext —— PaintingContext 做为参数出如今 RenderObject 的绘制方法中。 也就是说,PaintingContext 是一次性的,每次执行 Paint 时都会生成对应的 PaintingContext,当绘制完成时其生命周期也随之结束。 PaintingContext 在 RenderObject 的绘制过程当中的做用以下图所示:
RendererBinding#drawFrame
->PipelineOwner#flushPaint
触发RenderObject#paint
;RenderObject#paint
调用PaintingContext.canvas
提供的图形操做接口(draw*
、clip*
、transform
等)完成绘制任务;PaintingContext#paintChild
递归地绘制子节点(child renderobject,若有);PaintingContext.canvas
上,即 RenderObject 与 Layer 是多对一的关系。window.render
送入 Engine 层,最终 GPU 对其进行光栅化处理,显示在屏幕上。Repaint Boundary 的概念将在介绍 RenderObject 时重点分析。
上述流程中,起到关键做用的几个方法:
Canvas get canvas {
if (_canvas == null)
_startRecording();
return _canvas;
}
void _startRecording() {
// 在当前 Canvas 上进行的图形操做生成的 Picture 将添加到该 layer 上
//
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
// 初始化 Canvas,传入_recorder
//
_canvas = Canvas(_recorder);
// 将_currentLayer插入以_containerLayer为根节点的子树上
//
_containerLayer.append(_currentLayer);
}
void stopRecordingIfNeeded() {
// 在中止记录时,将结果 picture 加到 _currentLayer 上
//
_currentLayer.picture = _recorder.endRecording();
// 注意!
// 此时,_currentLayer、_recorder、_canvas 被释放,
// 此后,若还要经过当前 PaintingContext 进行绘制,则会生成新的 _currentLayer、_recorder、_canvas
// 即在 PaintingContext 的生命周期内 _canvas 可能会变
//
_currentLayer = null;
_recorder = null;
_canvas = null;
}
复制代码
Compositing,合成,属于 Rendering Pipeline 中的一环,表示是否要生成新的 Layer 来实现某些特定的图形效果。
RenderObject.needCompositing
表示该 RenderObject 是否须要合成,即在paint
方法中是否须要生成新的 Layer。 更详细的信息将在介绍 RenderObject 是进行分析。
一般 RenderObject 会经过PaintingContext#push*
来处理 Compositing:
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
// 注意!
// 在 append sub layer 前先终止现有的绘制操做
// stopRecordingIfNeeded 所执行的操做见上文
//
stopRecordingIfNeeded();
appendLayer(childLayer);
// 为 childLayer 建立新的 PaintingContext,以便独立进行绘制操做
//
final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset);
childContext.stopRecordingIfNeeded();
}
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
return PaintingContext(childLayer, bounds);
}
// needsCompositing 参数通常来自 RenderObject.needCompositing
//
ClipRectLayer pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge, ClipRectLayer oldLayer }) {
final Rect offsetClipRect = clipRect.shift(offset);
if (needsCompositing) {
// 在须要合成时,建立新 Layer
//
final ClipRectLayer layer = oldLayer ?? ClipRectLayer();
layer
..clipRect = offsetClipRect
..clipBehavior = clipBehavior;
// 将新 layer 添加到 layer tree 上,并在其上完成绘制
//
pushLayer(layer, painter, offset, childPaintBounds: offsetClipRect);
return layer;
} else {
// 不然在当前 Canvas 上进行裁剪、绘制
//
clipRectAndPaint(offsetClipRect, clipBehavior, offsetClipRect, () => painter(this, offset));
return null;
}
}
复制代码
如上,pushClipRect
在needsCompositing
为true
时,建立了新 Layer 并在其上进行裁剪、绘制,不然在当前 Canvas 上进行裁剪、绘制。
下面,咱们再经过一个简单的例子将上面的内容串一下:
void main() {
ContainerLayer containerLayer = ContainerLayer();
PaintingContext paintingContext = PaintingContext(containerLayer, Rect.zero);
Paint circle1Paint= Paint();
circle1Paint.color = Colors.blue;
// 注释1
// paintingContext.canvas.save();
// 对画布进行裁剪
//
paintingContext.canvas.clipRect(Rect.fromCenter(center: Offset(400, 400), width: 280, height: 600));
// 在裁剪后的画布上画一个⭕️
//
paintingContext.canvas.drawCircle(Offset(400, 400), 300, circle1Paint);
// 注释2
// paintingContext.canvas.restore();
void _painter(PaintingContext context, Offset offset) {
Paint circle2Paint = Paint();
circle2Paint.color = Colors.red;
context.canvas.drawCircle(Offset(400, 400), 250, circle2Paint);
}
// 经过 pushClipRect 方法再次执行裁剪
// 注意此处 needsCompositing 参数为 true
//
paintingContext.pushClipRect(true, Offset.zero, Rect.fromCenter(center: Offset(500, 400), width: 200, height: 200), _painter,);
Paint circle3Paint= Paint();
circle3Paint.color = Colors.yellow;
// 再次画一个⭕️
//
paintingContext.canvas.drawCircle(Offset(400, 800), 300, circle3Paint);
paintingContext.stopRecordingIfNeeded();
// 为了减小篇幅,生成 Scene 相关的代码已省略
}
复制代码
绘制结果以下图所示:
paintingContext.pushClipRect
时,
needsCompositing
参数为
false
,则结果以下:
needsCompositing
参数为
false
时,如何实现图1的效果呢? 很简单,将代码中一、2处的注释去掉便可。 过程就不分析了,兴趣的同窗能够本身分析一下。
PaintingContext 在协助 RenderObject 绘制过程当中起到重要做用,如:对 Layer Tree 的管理、对 Repaint Boundary、need Compositing 的处理、对基础 api 的封装等。了解了这些对后面理解 RenderObject 有很大的帮助。