本文是『 深刻淺出 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) 提供了場所或者說環境 (上下文)。 其主要職責包括:markdown
如上圖:app
PaintingContext
繼承自ClipContext
,ClipContext
是抽象類,主要提供了幾個與裁剪 (Clip) 有關的輔助方法;PictureLayer _currentLayer
、ui.PictureRecorder _recorder
以及Canvas _canvas
用於具體的繪製操做;ContainerLayer _containerLayer
,「Layer Subtree」的根節點,由PaintingContext
構造函數傳入,通常傳入的是RenderObject._layer
。RenderObject 與 Layer 是多對一的關係,即多個 RenderObject 繪製在一個 Layer 上。函數
在上一小節中說起一些基礎的概念,本小節對它們逐一進行簡要介紹。oop
Canvas
是 Engine(C++) 層到 Framework(Dart) 層的橋接,真正的功能在 Engine 層實現。post
下文將要出現的
Picture
、PictureRecorder
、SceneBuilder
以及SceneBuilder
都屬於Engine(C++) 層到 Framework(Dart) 層的橋接。ui
Canvas 向 Framework 層曝露了與繪製相關的基礎接口,如:draw*
、clip*
、transform
以及scale
等,RenderObject 正是經過這些基礎接口完成繪製任務的。
經過這套接口進行的全部操做都將被
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();
}
複製代碼
經過直接操做 Canvas,咱們在屏幕上畫了一個⭕️。
僅僅是爲了演示,在平常開發中並不須要直接操做這些基礎 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 有很大的幫助。