Flutter佈局和繪製流程淺析

chonglingliu發表於2021-04-01

我們前面介紹了StatelessWidgetStatefulWidget,它們只是對其他Widget進行組合,不具備自定義繪製的能力。在需要繪製內容的場景下,我們要使用RenderObjectWidget,因為RenderObjectWidget建立的RenderObject負責佈局和繪製的功能。

本文將以RenderObject為起點,梳理下Flutter的佈局和繪製流程的邏輯。

RenderObject

RenderObject渲染樹Render Tree的一個節點,主要負責佈局繪製

Flutter設計了重要的三棵樹Widget Tree - Element Tree - RenderObject Tree。示例如下: 圖片引用來源

RenderObjectWidget例項化的RenderObjectElement會建立 RenderObject, 所有的RenderObject會組成一顆RenderObject Tree

abstract class RenderObject extends AbstractNode implements HitTestTarget {}
複製程式碼

RenderObject繼承自AbstractNode, AbstractNode是對樹的節點的抽象:

class AbstractNode {
    // 1
    int get depth => _depth;
    int _depth = 0;
    void redepthChild(AbstractNode child) {}
    
    // 2
    Object? get owner => _owner;
    Object? _owner;
    
    void attach(covariant Object owner) {
        _owner = owner;
    }
    void detach() {
        _owner = null;
    }
    
    // 3
    AbstractNode? get parent => _parent;
    AbstractNode? _parent;
  
    void adoptChild(covariant AbstractNode child) {
        child._parent = this;
        if (attached)
          child.attach(_owner!);
        redepthChild(child);
    }
    
    void dropChild(covariant AbstractNode child) {
        child._parent = null;
        if (attached)
          child.detach();
    }
}
複製程式碼
  • AbstractNode提供了三個屬性和幾個重要的方法:
  1. 節點深度depth屬性和計算節點深度redepthChild()方法;
  2. owner和對應的關聯attach()和取消關聯detach()方法;
  3. parent父節點;
  4. 掛載子節點adoptChild()和解除安裝子節點dropChild()方法。
abstract class RenderObject extends AbstractNode implements HitTestTarget {
    // 1
    ParentData? parentData;
    
    // 2
    Constraints _constraints;
    
    // 3
    RenderObject? _relayoutBoundary;
    
    // 眾多方法...
}
複製程式碼
  • RenderObject自身也有幾個重要的屬性:
  1. parentData父節點的插槽,父節點的一些資訊可以放置在這裡面供子節點使用;
  2. _constraints為父節點提供的約束;
  3. _relayoutBoundary是需要重新佈局的邊界。
  • RenderObject的方法和Android的View非常類似:
功能RenderObjectView
佈局performLayout()measure()/measure()
繪製paint()draw()
請求佈局markNeedsLayout() requestLayout()
請求繪製markNeedsPaint() invalidate()
父節點/ViewparentgetParent()
新增子節點/ViewadoptChild()addView()
移除子節點/ViewdropChild()removeView()
關聯owner/Windowattach()onAttachedToWindow()
取消關聯owner/Windowdetach()onDetachedFromWindow()
事件hitTest()onTouch()
螢幕旋轉rotate()onConfigurationChanged()
引數parentDatamLayoutParams
  • RenderObject還有一個特點 --- 它定義了佈局/繪製協議,但並沒定義具體佈局/繪製模型

定義了佈局/繪製協議就是指繼承RenderObject的子類必須要實現一些方法,譬如performLayoutpaint等;沒定義具體佈局/繪製模型是指沒有限定使用什麼座標系,子節點可以有0個、1個還是多個等。

  • Flutter提供了RenderBoxRenderSlive兩個子類,他們分別對應簡單的2D笛卡爾座標模型和滾動模型。
RenderObject子類ConstraintsParentData
RenderBoxBoxConstraintsBoxParentData
RenderSliveSliverConstraintsSliverLogicalParentData

一般情況下我們不需要直接使用RenderObject,使用RenderBoxRenderSlive這兩個子類就能滿足需求。

SchedulerBinding.handleDrawFrame()

我們介紹這個方法是為了介紹每次重新整理的工作流程,這樣有助於我們更好的理解RenderObject的相關內容。

Flutter啟動流程分析那篇文章中,我們提到過window.scheduleFrame()Native platform發起一個重新整理檢視的請求後,Flutter Engine會在適當的時候呼叫SchedulerBinding_handleDrawFrame方法。

void handleDrawFrame() {
    try {
      // PERSISTENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (final FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp!);
    } finally {
    }
}
複製程式碼

handleDrawFrame中執行了回撥函式陣列persistentCallbacks中所有的回撥函式。其中就包括RendererBinding中的_handlePersistentFrameCallback方法:

<!-- RendererBinding -->
void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
    _scheduleMouseTrackerUpdate();
}
複製程式碼

這裡的drawFrame方法是呼叫的父類WidgetsBinding的方法:

<!-- WidgetsBinding -->
void drawFrame() {
    try {
      // 1
      if (renderViewElement != null)
        buildOwner!.buildScope(renderViewElement!);
      // 2
      super.drawFrame();
      // 3
      buildOwner!.finalizeTree();
    } finally {
     
    }
}
複製程式碼

此方法代表的含義:

  1. buildOwner!.buildScope(renderViewElement!)執行的是Widgetbuild任務,這其中就包括StatelessWidgetStatefulWidgetRenderObjectWidget
  2. 呼叫WidgetsBindingdrawFrame方法;
  3. 解除安裝非啟用狀態的Element

WidgetsBindingdrawFrame方法中則執行了佈局和繪製等操作。

void drawFrame() {
    assert(renderView != null);
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    if (sendFramesToEngine) {
      renderView.compositeFrame(); // this sends the bits to the GPU
      pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
      _firstFrameSent = true;
    }
}
複製程式碼

flow

上面的buid/layout/paint等都和RenderObject息息相關,我們將在接下來的章節中詳細介紹。

Build

接下來我們就來看看buildScope方法觸發的RenderObjectWidget的構建過程。

Element inflateWidget(Widget newWidget, dynamic newSlot) {
    // 1
    final Element newChild = newWidget.createElement();
    // 2
    newChild.mount(this, newSlot);
    return newChild;
}
複製程式碼

inflateWidget方法主要作用:

  1. 先通過createElement方法根據Widget建立對應的Element
  2. 然後新建的Element呼叫mount方法,將自己掛載到Element Tree上,位置是父ElementnewSlot這個插槽。

createElement

abstract class RenderObjectWidget extends Widget {
    @factory
    RenderObjectElement createElement();
}
複製程式碼

RenderObjectWidgetcreateElement方法是工廠方法,真正的實現方法在子類裡面。

RenderObjectWidget的子類對應的Element總結:

分類WidgetElement
根節點RenderObjectToWidgetAdapterRootRenderObjectElement
具有多個子節點MultiChildRenderObjectWidgetMultiChildRenderObjectElement
具有一個子節點點SingleChildRenderObjectWidgetSingleChildRenderObjectElement
葉子節點LeafRenderObjectWidgetLeafRenderObjectElement

程式碼如下:

abstract class LeafRenderObjectWidget extends RenderObjectWidget {
  @override
  LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}

abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
  @override
  SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
}

abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
  @override
  MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}

class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
  @override
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);

  @override
  RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
}
複製程式碼

mount

我們來看RenderObjectElementmount方法實現:

void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
}
複製程式碼
  1. super.mount的作用主要是記錄下parent,slotdepth等值;
  2. widget.createRenderObject建立了一個renderObject
  3. attachRenderObject就是將這個parentData掛載到RenderObject Tree上,並且更新RenderObjectparentData
void attachRenderObject(dynamic newSlot) {
    _slot = newSlot;
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
    final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
    if (parentDataElement != null)
      _updateParentData(parentDataElement.widget);
}
複製程式碼

insertRenderObjectChild

renderObject通過insertRenderObjectChild方法掛載到RenderObject Tree上,那具體的實現是如何實現的呢?

能實現掛載RenderObject的只能是SingleChildRenderObjectElementMultiChildRenderObjectElement。我們分別來看看:

SingleChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, dynamic slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
    renderObject.child = child;
}
複製程式碼

child進行賦值:

set child(ChildType? value) {
    if (_child != null)
      dropChild(_child!);
    _child = value;
    if (_child != null)
      adoptChild(_child!);
}
複製程式碼

如果已經有_child先將其從解除安裝,然後將新的Child掛載上。

void dropChild(RenderObject child) {
    child._cleanRelayoutBoundary();
    child.parentData!.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
}
複製程式碼
void adoptChild(RenderObject child) {
    setupParentData(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
}
複製程式碼

這兩個方法主要是對_childparentData重新賦值,然後通過markNeedsLayoutmarkNeedsCompositingBitsUpdatemarkNeedsSemanticsUpdate標記需要重新佈局,需要合成和語義的更新。

MultiChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
    final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;
    renderObject.insert(child, after: slot.value?.renderObject);
}
複製程式碼
void insert(ChildType child, { ChildType? after }) {
    adoptChild(child);
    _insertIntoChildList(child, after: after);
}
複製程式碼

MultiChildRenderObjectElement中的實現方式類似,只是這次不是簡單的賦值,而是將child新增到Render Tree中去,然後進行各種標記。

_insertIntoChildList方法新增的邏輯如下:

  • 依附的兄弟節點為空,插入在第一個子節點;
  • 依附的兄弟節點沒有相關聯的下一個兄弟節點,插入在兄弟節點隊尾;
  • 依附的兄弟節點有相關聯的下一個兄弟節點,插入在兄弟節點中間。

inflateWidget遞迴

由於SingleChildRenderObjectWidgetMultiChildRenderObjectWidget含有子節點,所以需要對子Widget進行構建。

<!-- SingleChildRenderObjectWidget -->
void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _child = updateChild(_child, widget.child, null);
}

Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) {
    final Element newChild;
    // ... 省略Widget更新的邏輯
    newChild = inflateWidget(newWidget, newSlot);
    return newChild;
}
複製程式碼
<!-- MultiChildRenderObjectElement -->
void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
    Element? previousChild;
    for (int i = 0; i < children.length; i += 1) {
      final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
      children[i] = newChild;
      previousChild = newChild;
    }
    _children = children;
}
複製程式碼

這樣,接下來的操作就進入了遞迴流程了,和上面介紹的流程內容一模一樣了。

流程示意圖:

RenderObjectWidget Build

markNeedsLayout

我們上面看到了,adoptChildadoptChild的方法中都呼叫了markNeedsLayout的相關內容:

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {

    bool _needsLayout = true;

    RenderObject? _relayoutBoundary;
    
    void markNeedsLayout() {
        // 1
        if (_needsLayout) {
          return;
        }
        // 2
        if (_relayoutBoundary != this) {
          markParentNeedsLayout();
        } else {
          // 3
          _needsLayout = true;
          if (owner != null) {
            owner!._nodesNeedingLayout.add(this);
            owner!.requestVisualUpdate();
          }
        }
    }
}
複製程式碼

RenderObject_needsLayout屬性,標記是否需要重新佈局,還有一個_relayoutBoundary佈局邊界屬性,表示開始重新佈局的節點,這樣就不需要每次整個渲染樹的節點都進行重新佈局。

markNeedsLayout代表的含義:

  1. 如果已經標記_needsLayout,直接返回;
  2. 如果_relayoutBoundary佈局邊界不是自身,讓父節點遞迴呼叫markNeedsLayout方法;
  3. 如果_relayoutBoundary佈局邊界是自身,標記_needsLayout, 並將自身加入到PipelineOwner_nodesNeedingLayout列表中,等待PipelineOwner進行重新佈局;
  4. 請求PipelineOwner進行更新。

您可能會有疑問_relayoutBoundary是在什麼時候賦值的?有兩個地方賦值:

  1. 第一次佈局的時候,_relayoutBoundary會被標記為RenderView,即自身,然後從根節點進行佈局;
void scheduleInitialLayout() {
    _relayoutBoundary = this;
    owner!._nodesNeedingLayout.add(this);
}
複製程式碼
  1. layout()方法中RenderObject也會重新標記_relayoutBoundary,一般情況下也是自身。
void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // ...
    _relayoutBoundary = relayoutBoundary;
}
複製程式碼

markNeedsCompositingBitsUpdate

bool _needsCompositingBitsUpdate = false;

void markNeedsCompositingBitsUpdate() {
    if (_needsCompositingBitsUpdate)
      return;
    _needsCompositingBitsUpdate = true;
    if (parent is RenderObject) {
      final RenderObject parent = this.parent! as RenderObject;
      if (parent._needsCompositingBitsUpdate)
        return;
      if (!isRepaintBoundary && !parent.isRepaintBoundary) {
        parent.markNeedsCompositingBitsUpdate();
        return;
      }
    }
    if (owner != null)
      owner!._nodesNeedingCompositingBitsUpdate.add(this);
}
複製程式碼

RenderObject_needsCompositingBitsUpdate屬性,標記是否需要合成。

markNeedsCompositingBitsUpdate的邏輯如下:

  1. 如果已經標記_needsCompositingBitsUpdate,直接返回;
  2. 如果未標記_needsCompositingBitsUpdate先標記,然後標記父節點或者向父類遞迴呼叫markNeedsCompositingBitsUpdate直到標記成功為止;
  3. 將自身加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表中。

結果就是將isRepaintBoundary這個節點下的所有節點都標記為_needsCompositingBitsUpdate,然後加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表中。

flushLayout

前面所有的邏輯只能算是buildScope方法觸發的Build階段。接下來我們就進入了Layout階段了。

<!-- PipelineOwner -->
void flushLayout() {
    try {
      while (_nodesNeedingLayout.isNotEmpty) {
        final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
        _nodesNeedingLayout = <RenderObject>[];
        for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
          if (node._needsLayout && node.owner == this)
            node._layoutWithoutResize();
        }
      }
    } finally {
    }
}
複製程式碼

PipelineOwnerflushLayout其實很簡單,讓_nodesNeedingLayout中的所有RenderObject按照廣度優先遍歷呼叫_layoutWithoutResize方法。

<!-- RenderObject -->
void _layoutWithoutResize() {
    try {
      performLayout();
    } catch (e, stack) {
    }
    _needsLayout = false;
    markNeedsPaint();
}
複製程式碼

我們來看看_ScaffoldLayout中的實現:

void performLayout() {
    size = _getSize(constraints);
    delegate._callPerformLayout(size, firstChild);
}

void _callPerformLayout(Size size, RenderBox? firstChild) {
    performLayout(size);
}

void performLayout(Size size) {
    layoutChild(_ScaffoldSlot.body, bodyConstraints);
    positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
}

Size layoutChild(Object childId, BoxConstraints constraints) {
    child!.layout(constraints, parentUsesSize: true);
    return child.size;
}

void positionChild(Object childId, Offset offset) {
    final MultiChildLayoutParentData childParentData = child!.parentData! as MultiChildLayoutParentData;
    childParentData.offset = offset;
}
複製程式碼

根據一系列呼叫,生成一個BoxConstraints傳遞給每個子節點,子節點呼叫layout() 進行測量和佈局。

void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // 1
    RenderObject? relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
    }
    
    // 2
    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
      return;
    }
    // 3
    _constraints = constraints;
    if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
      visitChildren(_cleanChildRelayoutBoundary);
    }
    _relayoutBoundary = relayoutBoundary;
    
    // 4
    if (sizedByParent) {
      try {
        performResize();
        
      } catch (e, stack) {
      }
      
    }
    try {
      // MultiChildLayoutDelegate --- performLayout & _callPerformLayout & performLayout & child!.layout
      // 5 
      performLayout();
      markNeedsSemanticsUpdate();
      
    } catch (e, stack) {
    }
 
    _needsLayout = false;
    markNeedsPaint();
}
複製程式碼
  1. 首先根據!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject的條件進行_relayoutBoundary的計算,一般情況下會指向自身;

parentUsesSize表示是否父節點的大小依賴子節點,sizedByParent表示大小由父類決定,constraints.isTight表示大小是固定的。

  1. 根據!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary確實定是否需要重新佈局,不需要直接返回;
  2. 記錄下_constraints;
  3. 如果依賴父節點的大小,則根據_constraints計算出size尺寸, ;
  4. performLayout根據根據_constraints計算出size尺寸,然後呼叫子類的layout方法。
總結:

performLayout的邏輯就是通過layout方法將Constraints逐步往下傳遞,得到Size逐步向上傳遞,然後父節點通過給parentData賦值確定對子節點的位置擺放。

flushLayout

flushCompositingBits

void flushCompositingBits() {

    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
    for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
      if (node._needsCompositingBitsUpdate && node.owner == this)
        node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
  }
複製程式碼

遍歷_nodesNeedingCompositingBitsUpdate中的每個RenderObject,然後呼叫_updateCompositingBits方法。

void _updateCompositingBits() {
    if (!_needsCompositingBitsUpdate)
      return;
    final bool oldNeedsCompositing = _needsCompositing;
    _needsCompositing = false;
    visitChildren((RenderObject child) {
      child._updateCompositingBits();
      if (child.needsCompositing)
        _needsCompositing = true;
    });
    if (isRepaintBoundary || alwaysNeedsCompositing)
      _needsCompositing = true;
    if (oldNeedsCompositing != _needsCompositing)
      markNeedsPaint();
    _needsCompositingBitsUpdate = false;
}
複製程式碼

這個方法就是找到isRepaintBoundarytrue的節點及其父節點,將它們的_needsCompositing為true設定為true;

isRepaintBoundary

上面提到的isRepaintBoundaryRenderObject的一個屬性,預設是false。表示的是否需要獨立渲染。

<!-- RenderObject -->
bool get isRepaintBoundary => false;
複製程式碼

如果需要獨立渲染則需要覆蓋這個值為true,例如RenderView的值就為true

<!-- RenderView -->
@override
bool get isRepaintBoundary => true;
複製程式碼

flushPaint

按照邏輯flushPaint之前應該先會呼叫markNeedsPaint,我們回過頭來看看發現確實如此,有很多地方都頻繁的呼叫的markNeedsPaint,譬如_layoutWithoutResize,layout,_updateCompositingBits等方法中都有出現,只是前面我們特意忽略了這個邏輯。

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();
    }
}
複製程式碼
  1. 如果isRepaintBoundarytrue, 則加入到_nodesNeedingPaint陣列中,然後請求介面更新;
  2. 如果isRepaintBoundaryfalse,則向父節點遍歷;
  3. 如果到了根節點,就直接請求介面更新;

我們接下來看看flushPaint的程式碼:

void flushPaint() {
    try {
      final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = <RenderObject>[];
      for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
        if (node._needsPaint && node.owner == this) {
          if (node._layer!.attached) {
            PaintingContext.repaintCompositedChild(node);
          } else {
            node._skippedPaintingOnLayer();
          }
        }
      }
    } finally {
    }
}
複製程式碼

從下往上遍歷_nodesNeedingPaint陣列,然後從上往下進行繪製。

接下來我們看看是如何繪製的:

static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
    _repaintCompositedChild(
      child,
      debugAlsoPaintedParent: debugAlsoPaintedParent,
    );
}

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);

    childContext.stopRecordingIfNeeded();
}
複製程式碼

PaintingContext的類方法repaintCompositedChild接收了RenderObject物件,最後結果是這個RenderObject物件呼叫_paintWithContext方法,引數是PaintingContext物件和偏移量Offset

void _paintWithContext(PaintingContext context, Offset offset) {
    
    if (_needsLayout)
      return;

    _needsPaint = false;
    try {
      paint(context, offset);
    } catch (e, stack) {
      
    }
}
複製程式碼
<!-- PaintingContext -->
Canvas? _canvas;
複製程式碼

_paintWithContext方法呼叫的是RenderObject子類物件的paint(PaintingContext context, Offset offset)進行繪製,繪製在PaintingContextCanvas上。

總結

本文主要分析了RendObjectBuildLayoutPaint等相關內容,後續繼續分析其他相關內容。

相關文章