We introduced statelessWidgets and StatefulWidgets earlier, which are simply combinations of other widgets and do not have the ability to customize drawing. We use RenderObjectWidget in scenarios where we need to draw content, because the RenderObject created by RenderObjectWidget is responsible for layout and drawing.

This paper will take RenderObject as a starting point to sort out the layout of Flutter and the logic of its drawing process.

RenderObject

RenderObject is a node in the Render Tree responsible for layout and rendering.

FlutterThree important trees were designedWidget TreeElement TreeRenderObject Tree. The following is an example: Image source

RenderObjectWidget instantiates The RenderObjectElement that creates the RenderObjects, all of which form a RenderObject Tree.

abstract class RenderObject extends AbstractNode implements HitTestTarget {}
Copy the code

RenderObject is derived from AbstractNode, which is an abstraction of nodes in a tree:

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();
    }
}
Copy the code
  • AbstractNode provides three properties and several important methods:
  1. The depth of the nodedepthProperty and compute node depthredepthChild()Methods;
  2. ownerAnd the corresponding associationattach()And unassociatedetach()Methods;
  3. parentThe parent node;
  4. Mount child nodesadoptChild()And unload child nodesdropChild()Methods.
abstract class RenderObject extends AbstractNode implements HitTestTarget { // 1 ParentData? parentData; // 2 Constraints _constraints; // 3 RenderObject? _relayoutBoundary; // Many ways... }Copy the code
  • The RenderObject itself also has several important properties:
  1. parentDataA slot for the parent node in which some information about the parent node can be placed for use by the child node;
  2. _constraintsConstraints provided for the parent node;
  3. _relayoutBoundaryAre the boundaries that need to be rearranged.
  • RenderObject works very much like an Android View:
function RenderObject View
layout performLayout() measure()/measure()
draw paint() draw()
Request the layout markNeedsLayout() requestLayout()
Request drawing markNeedsPaint() invalidate()
The parent node/View parent getParent()
Add child node /View adoptChild() addView()
Remove child node /View dropChild() removeView()
The owner/the Window attach() onAttachedToWindow()
Disassociate owner/Window detach() onDetachedFromWindow()
The event hitTest() onTouch()
Screen rotation rotate() onConfigurationChanged()
parameter parentData mLayoutParams
  • RenderObject also has a feature — it defines layout/rendering protocols, but not layout/rendering models.

Defining a layout/draw protocol means that subclasses that inherit RenderObject must implement methods such as performLayout, Paint, etc. Undefined layout/drawing model means that there is no limitation on which coordinate system to use, and the child nodes can be zero, one, or multiple, etc.

  • Flutter provides RenderBox and RenderSlive subclasses, which correspond to a simple 2D Cartesian coordinate model and a scroll model, respectively.
RenderObject subclass Constraints ParentData
RenderBox BoxConstraints BoxParentData
RenderSlive SliverConstraints SliverLogicalParentData

Normally we don’t need to use RenderObject directly. RenderBox and RenderSlive subclasses do the trick.

SchedulerBinding.handleDrawFrame()

We introduce this method to describe the workflow of each refresh, which will help us understand RenderObject better.

In the Flutter startup process analysis article, we mentioned that window.scheduleFrame() sends a request to the Native platform to refresh its view, The Flutter Engine calls the _handleDrawFrame method of a SchedulerBinding when appropriate.

void handleDrawFrame() { try { // PERSISTENT FRAME CALLBACKS _schedulerPhase = SchedulerPhase.persistentCallbacks; for (final FrameCallback callback in _persistentCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!) ; } finally { } }Copy the code

All callbacks in the persistentCallbacks array are executed in handleDrawFrame. Including the _handlePersistentFrameCallback RendererBinding method:

<! -- RendererBinding --> void _handlePersistentFrameCallback(Duration timeStamp) { drawFrame(); _scheduleMouseTrackerUpdate(); }Copy the code

Here the drawFrame method is the parent of the WidgetsBinding method called:

<! -- WidgetsBinding --> void drawFrame() { try { // 1 if (renderViewElement ! = null) buildOwner! .buildScope(renderViewElement!) ; // 2 super.drawFrame(); // 3 buildOwner! .finalizeTree(); } finally { } }Copy the code

What this method represents:

  1. buildOwner! .buildScope(renderViewElement!)Execution isWidgetthebuildMission, and that includesStatelessWidgetandStatefulWidgetandRenderObjectWidget;
  2. callWidgetsBindingthedrawFrameMethods;
  3. Unload the inactive Element.

The WidgetsBinding drawFrame method performs layout and drawing operations.

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;
    }
}
Copy the code

Buid /layout/paint are all related to RenderObject, which will be covered in the following sections.

Build

Let’s look at the RenderObjectWidget build process that is triggered by the buildScope method.

Element inflateWidget(Widget newWidget, dynamic newSlot) {
    // 1
    final Element newChild = newWidget.createElement();
    // 2
    newChild.mount(this, newSlot);
    return newChild;
}
Copy the code

The inflateWidget method has the following functions:

  1. Through the firstcreateElementMethods according to theWidgetCreate the correspondingElement;
  2. And then the new oneElementcallmountMethod to mount itself toElement TreeUp, position is the parentElementthenewSlotThe slot.

createElement

abstract class RenderObjectWidget extends Widget {
    @factory
    RenderObjectElement createElement();
}
Copy the code

The createElement method of RenderObjectWidget is a factory method, and the real implementation method is in the subclass.

RenderObjectWidget subclass Element

classification Widget Element
The root node RenderObjectToWidgetAdapter RootRenderObjectElement
Has multiple child nodes MultiChildRenderObjectWidget MultiChildRenderObjectElement
Has a child node point SingleChildRenderObjectWidget SingleChildRenderObjectElement
A leaf node LeafRenderObjectWidget LeafRenderObjectElement

The code is as follows:

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;
}
Copy the code

mount

RenderObjectElement mount method

void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
}
Copy the code
  1. super.mountThe main function of is to recordparent.slotanddepthEquivalent;
  2. widget.createRenderObjectCreated arenderObject;
  3. attachRenderObjectMount parentData toRenderObject TreeAnd updateRenderObjecttheparentData.
void attachRenderObject(dynamic newSlot) { _slot = newSlot; _ancestorRenderObjectElement = _findAncestorRenderObjectElement(); _ancestorRenderObjectElement? .insertRenderObjectChild(renderObject, newSlot); final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement(); if (parentDataElement ! = null) _updateParentData(parentDataElement.widget); }Copy the code

insertRenderObjectChild

RenderObject is mounted to the renderObject Tree using the insertRenderObjectChild method. How is this implemented?

Can realize the mount RenderObject only SingleChildRenderObjectElement and MultiChildRenderObjectElement. Let’s take a look at each:

SingleChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, dynamic slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
    renderObject.child = child;
}
Copy the code

Assign to child:

set child(ChildType? value) { if (_child ! = null) dropChild(_child!) ; _child = value; if (_child ! = null) adoptChild(_child!) ; }Copy the code

If there is already a _child, unmount it and then mount the new Child.

void dropChild(RenderObject child) { child._cleanRelayoutBoundary(); child.parentData! .detach(); child.parentData = null; super.dropChild(child); markNeedsLayout(); markNeedsCompositingBitsUpdate(); markNeedsSemanticsUpdate(); }Copy the code
void adoptChild(RenderObject child) {
    setupParentData(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
}
Copy the code

These two methods are mainly the _child and parentData assignment again, and then through markNeedsLayout, markNeedsCompositingBitsUpdate and markNeedsSemanticsUpdate markers need to layout, Composition and semantic updates are required.

MultiChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element? > slot) { final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject; renderObject.insert(child, after: slot.value? .renderObject); }Copy the code
void insert(ChildType child, { ChildType? after }) {
    adoptChild(child);
    _insertIntoChildList(child, after: after);
}
Copy the code

MultiChildRenderObjectElement the implementation in the similar way, just this is not a simple assignment, but add the child to the Render Tree, and do all kinds of tag.

The _insertIntoChildList method adds the following logic:

  • The attached sibling node is empty and inserted into the first child node;
  • The attached sibling node has no associated next sibling node and is inserted at the end of the sibling node queue.
  • The attached sibling has an associated next sibling inserted between the siblings.

InflateWidget recursive

Because SingleChildRenderObjectWidget and MultiChildRenderObjectWidget contains child nodes, so need to subsidiary to build the 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; / /... Omit Widget update logic newChild = inflateWidget(newWidget, newSlot); return newChild; }Copy the code
<! -- 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; }Copy the code

So, the next step is a recursive flow, exactly the same as the flow described above.

Process diagram:

markNeedsLayout

The adoptChild and adoptChild methods call 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(); }}}}Copy the code

The RenderObject has a _needsLayout attribute to indicate whether or not it needs to be rearranged, and a _relayoutBoundary layout boundary to indicate which node to begin rearranging, so that the entire render tree does not need to be rearranged each time.

What markNeedsLayout stands for:

  1. If it’s already marked_needsLayout, return directly;
  2. if_relayoutBoundaryLayout boundaries are not themselves, allowing the parent node to recursively callmarkNeedsLayoutMethods;
  3. if_relayoutBoundaryLayout boundaries are themselves, marked_needsLayoutAnd add itself toPipelineOwnerthe_nodesNeedingLayoutList, waitPipelineOwnerTo rearrange;
  4. Request PipelineOwner for an update.

You may be wondering when _relayoutBoundary was assigned? There are two places to assign:

  1. When I first laid it out,_relayoutBoundaryIt’s going to be labeledRenderView, itself, and then layout from the root node;
void scheduleInitialLayout() { _relayoutBoundary = this; owner! ._nodesNeedingLayout.add(this); }Copy the code
  1. layout()In the methodRenderObjectIt will also be relabeled_relayoutBoundaryIn general, it is itself.
void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // ...
    _relayoutBoundary = relayoutBoundary;
}
Copy the code

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); }Copy the code

Whether you need RenderObject _needsCompositingBitsUpdate attribute, tag synthesis.

MarkNeedsCompositingBitsUpdate logic is as follows:

  1. If it’s already marked_needsCompositingBitsUpdate, return directly;
  2. If not marked_needsCompositingBitsUpdateTag, then tag the parent node or recursively call the parent classmarkNeedsCompositingBitsUpdateUntil the tag succeeds;
  3. Add yourself toPipelineOwnerthe_nodesNeedingCompositingBitsUpdateIn the list.

Result will be isRepaintBoundary all nodes of the node is marked as _needsCompositingBitsUpdate, Then add to the list of the PipelineOwner _nodesNeedingCompositingBitsUpdate.

flushLayout

All of the previous logic counts only as the Build phase triggered by the buildScope method. Then we entered the Layout stage.

<! -- 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 { } }Copy the code

PipelineOwner’s flushLayout is simple enough to make all renderObjects in _nodesNeedingLayout go breadth-first by calling the _layoutWithoutResize method.

<! -- RenderObject --> void _layoutWithoutResize() { try { performLayout(); } catch (e, stack) { } _needsLayout = false; markNeedsPaint(); }Copy the code

Let’s look at the implementation of _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; }Copy the code

Based on a series of calls, a BoxConstraints is generated and passed to each child node, which calls Layout () for measurement and 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(); }Copy the code
  1. First of all, according to the! parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObjectOn the condition that_relayoutBoundaryThe calculation of, in general, refers to itself;

ParentUsesSize indicates whether the size of the parent node depends on the child node, sizedByParent indicates that the size is determined by the parent class, and constraints. IsTight indicates that the size is fixed.

  1. According to the! _needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundaryDetermine whether the layout needs to be rearranged, without returning directly;
  2. Record the_constraints;
  3. If it depends on the size of the parent node, the_constraintsTo calculate thesizeSize,;
  4. performLayoutAccording to according to_constraintsTo calculate thesizeSize, and then call the subclass’slayoutMethods.
Conclusion:

The logic of performLayout is to pass the Constraints progressively down through the Layout method, resulting in Size progressively up, and then assign parentData to the parent node to determine the position of the child node.

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(); }Copy the code

Each of the traverse _nodesNeedingCompositingBitsUpdate RenderObject then call _updateCompositingBits method.

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; }Copy the code

The method is to find the nodes whose isRepaintBoundary is true and their parents and set their _needsCompositing to true;

isRepaintBoundary

The isRepaintBoundary mentioned above is a property of the RenderObject and defaults to false. Indicates whether rendering is required independently.

<! -- RenderObject --> bool get isRepaintBoundary => false;Copy the code

Override this value to true if standalone rendering is required, such as true for RenderView.

<! -- RenderView --> @override bool get isRepaintBoundary => true;Copy the code

flushPaint

It’s logical to call markNeedsPaint before flushPaint, and we’ll go back and see that it’s true, there are lots of places where markNeedsPaint is called frequently, Such as _layoutWithoutResize, layout, _updateCompositingBits methods have appeared, such as only we deliberately ignored the above logic.

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(); }}Copy the code
  1. ifisRepaintBoundaryfortrue, then join to_nodesNeedingPaintArray, then request interface update;
  2. If isRepaintBoundary is false, the parent node is traversed;
  3. If the root node is reached, the interface update is requested directly.

Let’s look at the code for 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 { } }Copy the code

Iterate through the _nodesNeedingPaint array from the bottom up and draw from the top down.

Let’s see how it’s drawn:

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); // focus child._paintwithContext (childContext, offset.zero); childContext.stopRecordingIfNeeded(); }Copy the code

The PaintingContext class method repaintCompositedChild receives the RenderObject object. The result is that the RenderObject calls the _paintWithContext method, The parameters are the PaintingContext object and the Offset.

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

    _needsPaint = false;
    try {
      paint(context, offset);
    } catch (e, stack) {
      
    }
}
Copy the code
<! -- PaintingContext --> Canvas? _canvas;Copy the code

The _paintWithContext method calls paint(PaintingContext context, Offset Offset) of the RenderObject subclass to draw on the Canvas of the PaintingContext.

conclusion

This article mainly analyzes RendObject, Build, Layout, Paint and other related content, and will continue to analyze other related content.