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 Tree – Element Tree – RenderObject 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:
- The depth of the node
depth
Property and compute node depthredepthChild()
Methods; owner
And the corresponding associationattach()
And unassociatedetach()
Methods;parent
The parent node;- Mount child nodes
adoptChild()
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:
parentData
A slot for the parent node in which some information about the parent node can be placed for use by the child node;_constraints
Constraints provided for the parent node;_relayoutBoundary
Are 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:
buildOwner! .buildScope(renderViewElement!)
Execution isWidgetthebuildMission, and that includesStatelessWidgetandStatefulWidgetandRenderObjectWidget;- callWidgetsBindingthe
drawFrame
Methods; - 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:
- Through the first
createElement
Methods according to theWidgetCreate the correspondingElement; - And then the new oneElementcall
mount
Method to mount itself toElement TreeUp, position is the parentElementthenewSlot
The 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
super.mount
The main function of is to recordparent
.slot
anddepth
Equivalent;widget.createRenderObject
Created arenderObject;attachRenderObject
Mount 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:
- If it’s already marked
_needsLayout
, return directly; - if
_relayoutBoundary
Layout boundaries are not themselves, allowing the parent node to recursively callmarkNeedsLayout
Methods; - if
_relayoutBoundary
Layout boundaries are themselves, marked_needsLayout
And add itself toPipelineOwnerthe_nodesNeedingLayout
List, waitPipelineOwnerTo rearrange; - Request PipelineOwner for an update.
You may be wondering when _relayoutBoundary was assigned? There are two places to assign:
- When I first laid it out,
_relayoutBoundary
It’s going to be labeledRenderView, itself, and then layout from the root node;
void scheduleInitialLayout() { _relayoutBoundary = this; owner! ._nodesNeedingLayout.add(this); }Copy the code
layout()
In the methodRenderObjectIt will also be relabeled_relayoutBoundary
In 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:
- If it’s already marked
_needsCompositingBitsUpdate
, return directly; - If not marked
_needsCompositingBitsUpdate
Tag, then tag the parent node or recursively call the parent classmarkNeedsCompositingBitsUpdate
Until the tag succeeds; - Add yourself toPipelineOwnerthe
_nodesNeedingCompositingBitsUpdate
In 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
- First of all, according to the
! parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject
On the condition that_relayoutBoundary
The 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.
- According to the
! _needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary
Determine whether the layout needs to be rearranged, without returning directly; - Record the
_constraints
; - If it depends on the size of the parent node, the
_constraints
To calculate thesize
Size,; performLayout
According to according to_constraints
To calculate thesize
Size, and then call the subclass’slayout
Methods.
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
- ifisRepaintBoundaryfortrue, then join to
_nodesNeedingPaint
Array, then request interface update; - If isRepaintBoundary is false, the parent node is traversed;
- 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.