深入淺出 Flutter Framework 之 PaintingContext

峰之巔發表於2020-05-30

本文是『 深入淺出 Flutter Framework 』系列文章的第五篇,主要目的是為後面介紹 RenderObject 作準備。 文章對 PaintingContext 進行了較詳細的分析,主要包括在 Rendering Pipeline 中 PaintingContext 是如何配合 RenderObject 進行繪製的,同時對一些基礎概念進行了簡要的介紹(如:Canvas、Picture、PictureRecorder、SceneBuilder 以及 Scene 等)。

本文同時發表於我的個人部落格

本系列文章將深入 Flutter Framework 內部逐步去分析其核心概念和流程,主要包括:

Overview


『 Widget 』—『 Element 』—『 RenderObject 』可稱之為 Flutter Framework『三劍客』,其中 WidgetElement 都已介紹過,而 RenderObject 在這三者中屬於最核心、最複雜的,涉及 Layout、Paint 等核心流程。 為了更好、更流暢地去理解 RenderObject,在正式介紹之前,需要做些準備工作,本文介紹的 PaintingContext 在 RenderObject 的繪製流程上扮演了重要角色。

『Painting Context』,其名稱已說明了一些事情:繪製上下文,最簡單的理解就是為繪製操作 (Paint) 提供了場所或者說環境 (上下文)。 其主要職責包括:

  • 在繪製流程中按需引入新的 Layer(主要依據 Repaint Boundary、need compositing);
  • 維護「Layer Tree」,每個 PaintingContext 例項都會生成一棵 Layer Sub Tree;
  • 管理 Canvas,對底層細節進行抽象、封裝。

深入淺出 Flutter Framework 之 PaintingContext
如上圖:

  • PaintingContext繼承自ClipContextClipContext是抽象類,主要提供了幾個與裁剪 (Clip) 有關的輔助方法;
  • PictureLayer _currentLayerui.PictureRecorder _recorder以及Canvas _canvas用於具體的繪製操作;
  • ContainerLayer _containerLayer,「Layer Subtree」的根節點,由PaintingContext建構函式傳入,一般傳入的是RenderObject._layer

RenderObject 與 Layer 是多對一的關係,即多個 RenderObject 繪製在一個 Layer 上。

基礎概念


在上一小節中提及一些基礎的概念,本小節對它們逐一進行簡要介紹。

Canvas

Canvas是 Engine(C++) 層到 Framework(Dart) 層的橋接,真正的功能在 Engine 層實現。

下文將要出現的PicturePictureRecorderSceneBuilder以及SceneBuilder都屬於Engine(C++) 層到 Framework(Dart) 層的橋接。

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);
複製程式碼

Picture

其本質是一系列「graphical operations」的集合,對 Framework 層透明。 Future<Image> toImage(int width, int height),通過toImage方法可以將其記錄的所有操作經光柵化後生成Image物件。

PictureRecorder

其主要作用是記錄在Canvas上執行的「graphical operations」,通過Picture#endRecording最終生成Picture

Scene

同樣對 Framework 層透明,是一系列 Picture、Texture 合成的結果。

An opaque object representing a composited scene.

UI 幀重新整理時,在 Rendering Pipeline 中 Flutter UI 經 build、layout、paint 等步驟後最終生成 Scene。 其後通過window.render將該 Scene 送入 Engine 層,最終經 GPU 光柵化後顯示在螢幕上。

SceneBuilder

用於將多個圖層(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});
複製程式碼

通過addPictureaddTexture可以引入要合成的 Picture、Texture。

同時,SceneBuilder 還會維護一個圖形操作 stack:

pushTransform
pushOffset
pushClipRect
...
pop
複製程式碼

這些操作主要用於OffsetLayerClipRectLayer等。

是不是覺得很抽象,暈乎乎的! 下面通過一個小例子將它們串起來,真實感受一下。

小例子

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();
}
複製程式碼

深入淺出 Flutter Framework 之 PaintingContext
通過直接操作 Canvas,我們在螢幕上畫了一個⭕️。

僅僅是為了演示,在日常開發中並不需要直接操作這些基礎 API。

繪製流程


本小節介紹的繪製流程,僅侷限於 PaintingContext 周圍,更完整的流程將在介紹 RenderObject 時進行分析。

PaintingContext 與 RenderObject 是什麼關係? 從『類間關係』角度看,它們之間是依賴關係,即 RenderObject 依賴於 PaintingContext —— PaintingContext 作為引數出現在 RenderObject 的繪製方法中。 也就是說,PaintingContext 是一次性的,每次執行 Paint 時都會生成對應的 PaintingContext,當繪製完成時其生命週期也隨之結束。 PaintingContext 在 RenderObject 的繪製過程中的作用如下圖所示:

深入淺出 Flutter Framework 之 PaintingContext

  • 在 UI Frame 重新整理時,通過RendererBinding#drawFrame->PipelineOwner#flushPaint觸發RenderObject#paint
  • RenderObject#paint呼叫PaintingContext.canvas提供的圖形操作介面(draw*clip*transform等)完成繪製任務;
  • 上述繪製操作被 PictureRecorder 記錄下來,在繪製結束時生成 picture,並被新增到 PictureLayer (_currentLayer)上;
  • 隨後,RenderObject 通過PaintingContext#paintChild遞迴地繪製子節點(child renderobject,如有);
  • 在繪製子節點時,根據子節點是否是「Repaint Boundary」而採用不同的策略:
    • 是「Repaint Boundary」— 為子節點生成新的 PaintingContext,從而子節點可以獨立進行繪製,繪製結果就是一顆「Layer subTree」,最後將該子樹 append 到父節點生成的「Layer Tree」上;
    • 不是「Repaint Boundary」— 子節點直接繪製在當前PaintingContext.canvas上,即 RenderObject 與 Layer 是多對一的關係。
  • 整個繪製流程結束時就得到了一棵「Layer Tree」,其後通過 SceneBuilder 生成 Scene,再經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


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;
    }
  }
複製程式碼

如上,pushClipRectneedsCompositingtrue時,建立了新 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 相關的程式碼已省略
}
複製程式碼

繪製結果如下圖所示:

深入淺出 Flutter Framework 之 PaintingContext
若上述程式碼中在呼叫paintingContext.pushClipRect時,needsCompositing引數為false,則結果如下:
深入淺出 Flutter Framework 之 PaintingContext
那麼,在needsCompositing引數為false時,如何實現圖1的效果呢? 很簡單,將程式碼中1、2處的註釋去掉即可。 過程就不分析了,興趣的同學可以自己分析一下。

總結

PaintingContext 在協助 RenderObject 繪製過程中起到重要作用,如:對 Layer Tree 的管理、對 Repaint Boundary、need Compositing 的處理、對基礎 api 的封裝等。瞭解了這些對後面理解 RenderObject 有很大的幫助。

參考資料

Flutter Internals

相關文章