RenderObject was mentioned in Widget. What is RenderObject? Let’s play a game!
The official explanation
An object in the render tree.
An object in the Render tree.
There are four trees in front of Flutter: the Widget tree, the Element tree, the Render tree and the Layer tree.
If the Widget tree is a drawing, then the Render tree is the assembly line for that drawing. RenderObject is an operator on this assembly line.
After we configure the Widget tree at the top level, the Render tree is responsible for rendering what you want on the phone screen.
Read the source code
A good day should start with reading the source code!
abstract class RenderObject extends AbstractNode
with DiagnosticableTreeMixin
implements HitTestTarget {}
Copy the code
As you can see, the RenderObject is also a linked list structure, incorporating the DiagnosticableTreeMixin tree feature and implementing a hit test abstract class.
Remove debug and Assert code, and the structural functionality of the RenderObject is clear.
Layout module
abstract class RenderObject {
/ / LAYOUT module
// The storage container that passes information to child is usually Offset
ParentData parentData;
// This method passes parentData to the child before it is added to the child list
void setupParentData(covariant RenderObject child) {
if (child.parentData is! ParentData)
child.parentData = ParentData();
}
// Add a Render Object as your child
@override
void adoptChild(RenderObject child) {
/ / set parentData
setupParentData(child);
// Mark the update
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
super.adoptChild(child);
}
// Delete a Render Object as its own child
@override
void dropChild(RenderObject child) {
// Clear boundaries
child._cleanRelayoutBoundary();
// The parentData access permission is lost
child.parentData.detach();
child.parentData = null;
super.dropChild(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
/ / traverse the children
void visitChildren(RenderObjectVisitor visitor) { }
// Render Tree manager
@override
PipelineOwner get owner => super.owner;
// Tell its owner to insert it into the Render Tree and the first markup needs to compute the layout and redraw it
@override
void attach(PipelineOwner owner) {
super.attach(owner);
if(_needsLayout && _relayoutBoundary ! =null) {
_needsLayout = false;
markNeedsLayout();
}
if (_needsCompositingBitsUpdate) {
_needsCompositingBitsUpdate = false;
markNeedsCompositingBitsUpdate();
}
if(_needsPaint && _layer ! =null) {
_needsPaint = false;
markNeedsPaint();
}
if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
_needsSemanticsUpdate = false; markNeedsSemanticsUpdate(); }}// Whether layout is required
bool _needsLayout = true;
// Where is the record layout boundary
RenderObject _relayoutBoundary;
// Parent constraints
@protected
Constraints get constraints => _constraints;
Constraints _constraints;
// The markup needs to be rearranged
void markNeedsLayout() {
// If it is not a boundary, let parent. MarkNeedsLayout handle push all the way to the boundary
if(_relayoutBoundary ! =this) {
markParentNeedsLayout();
} else {
// The owner adds the dirty table to the layout and asks for a refresh
_needsLayout = true;
if(owner ! =null) {
owner._nodesNeedingLayout.add(this); owner.requestVisualUpdate(); }}}This method is called to clear the boundary cache when the boundary changes
void _cleanRelayoutBoundary() {
if(_relayoutBoundary ! =this) {
_relayoutBoundary = null;
_needsLayout = true; visitChildren((RenderObject child) { child._cleanRelayoutBoundary(); }); }}// Only layout does not remeasure
void _layoutWithoutResize() {
performLayout();
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject relayoutBoundary;
// Determine the boundary
// If one of the following four conditions is met, the boundary is itself
// parentUsesSize parent Is concerned about its own size
// sizedByParent specifies the size by parent
// constraints. IsTight is constrained
// parent is not the root node of RenderObject
if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject) {
relayoutBoundary = this;
} else {
// Otherwise use the parent boundary
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
_constraints = constraints;
// If the boundary changes, the traversal clears all recorded boundaries to reset
if(_relayoutBoundary ! =null&& relayoutBoundary ! = _relayoutBoundary) { visitChildren((RenderObject child) { child._cleanRelayoutBoundary(); }); } _relayoutBoundary = relayoutBoundary;// performResize is called when sizedByParent is true
// Otherwise, set the size in performLayout
if (sizedByParent) {
performResize();
}
/ / layout
performLayout();
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
// size is determined only by parent constraints. Default to false is never wrong
// When true, the work of determining size is computed in performResize
@protected
bool get sizedByParent => false;
// Subclass overrides this method to calculate its own size based on constraints
// You can't call this method directly, you can call the Layout method indirectly only if sizedByParent is true
@protected
void performResize();
// Subclasses override this method
// This method cannot be called directly, but indirectly by calling the Layout method
Function 1: When sizedByParent is false, calculate its size according to the adaptive child
Function 2: Iterate over child.layout to determine the size and offsets of the child
@protected
void performLayout();
}
Copy the code
Lay down the key points:
RenderObject
As aRender Tree
But you can’t directly change the tree yourself. Every move needs to be communicated toPipelineOwner
The manager executes.PipelineOwner
Manage several dirty notebooks, record nodes that need to be changed, and actually update according to these notebooksRender Tree
. whenRenderObject
When changes need to be made, notify the owner to write himself to the small book (marked as dirty). It can be understood that in the process of military training, any problems should be reported to the drillmaster, and the drillmaster will make unified arrangements.parent
throughsetupParentData
Method, transferparentData
, usuallylayout offset
.performResize
Methods need to be subclassed. Calculate its own sizeperformLayout
Methods need to be subclassed.
- Calculate size when sizedByParent is false.
- 2. Iterate over child.layout to determine the size and offsets of the child.
- The core approach is
layout
.
Determine '_relayoutBoundary' layout boundary -> Call the 'performResize' and 'performLayout' methods to calculate size and position -> redrawCopy the code
Since this process works for most scenarios, when we actually develop, we only care about rewriting the implementation of performResize and performLayout, not the Layout method.
Paint module
abstract class RenderObject {
/ / PAINTING module
RenderObject() {
Whether you need / / mixed layer = is redrawing borders | | always mixed layer
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
}
// Whether to redraw the boundaries currently
bool get isRepaintBoundary => false;
// Always create a new layer and merge it to the original layer
@protected
bool get alwaysNeedsCompositing => false;
// Cache layer
ContainerLayer _layer;
// Whether the value of _needsCompositing needs to be set
bool _needsCompositingBitsUpdate = false;
// The flag _needsCompositing needs to be reset
void markNeedsCompositingBitsUpdate() {
if(owner ! =null)
owner._nodesNeedingCompositingBitsUpdate.add(this);
}
// Whether to draw on a new layer and merge to the ancestor layer
// true: Draw on a new layer but the new layer will preferentially use the cache layer to improve performance
// false: do not use the new layer
// currently repaintBoundary _needsCompositing=true and the cache layer is automatically assigned to the new OffsetLayer after drawing on this layer to merge into the parent layer
bool _needsCompositing;
// Go through the Settings using layer blending
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;
}
// Whether to draw
bool _needsPaint = true;
// The mark needs to be redrawn
void markNeedsPaint() {
_needsPaint = true;
// If it is currently repaintBoundary inform the owner that it needs to be redrawn
if (isRepaintBoundary) {
if(owner ! =null) {
owner._nodesNeedingPaint.add(this); owner.requestVisualUpdate(); }}// If the node is not root, let parent determine
else if (parent is RenderObject) {
final RenderObject parent = this.parent;
parent.markNeedsPaint();
}
// If the current node is root, directly notify the owner to redraw
else {
if(owner ! =null) owner.requestVisualUpdate(); }}// See [RenderView] to initialize the root node.
void scheduleInitialPaint(ContainerLayer rootLayer) {
_layer = rootLayer;
owner._nodesNeedingPaint.add(this);
}
// Replace rootLayer which is called only by root
// This method may be called when the pixel ratio of the device changes
void replaceRootLayer(OffsetLayer rootLayer) {
_layer.detach();
_layer = rootLayer;
markNeedsPaint();
}
// Subclasses override this method to do the actual drawing at the corresponding offset
// Instead of calling this method directly, use the markNeedsPaint flag and let the owner handle it
/ / if you just want to draw a child with PaintingContext. PaintChild interface to operate Avoid direct manipulation render the object
void paint(PaintingContext context, Offset offset) { }
}
Copy the code
The most important point in this module is the variable needsCompositing. This variable determines whether to draw on the new layer. As you can see in the constructor, it is determined by isRepaintBoundary and alwaysNeedsCompositing. IsRepaintBoundary This can be changed to true with the RepaintBoundary wrap. AlwaysNeedsCompositing can be modified by subclasses.
Therefore we can use the RepaintBoundary control to partially redraw to improve performance.
The SEMANTICS module and the HIT TESTING module
Semantic Semantics is mainly the interface provided to screen reading software, and it is also the basis to realize auxiliary functions. Through semantic interface, the machine can understand the content on the page, and users with visual impairment can use screen reading software to understand the UI content. Unless there is a special need, generally not accessible.
The result of the hit test is processed in the following way.
// Override this method to handle events
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }
Copy the code
There are two more important methods in RenderObject that I’ll mention as well.
Void reassemble() {// Mark for relayout markNeedsLayout(); / / tag markNeedsCompositingBitsUpdate reset whether to use a new layer (); // Mark needs to be redrawn markNeedsPaint(); // Tag semantics need to update markNeedsSemanticsUpdate(); VisitChildren ((RenderObject child) {child-.reassemble (); }); Void showOnScreen({RenderObject descendant, Rect Rect, Duration Duration = duration.zero, Curve curve = Curves.ease, }) { if (parent is RenderObject) { final RenderObject renderParent = parent; renderParent.showOnScreen( descendant: descendant ?? this, rect: rect, duration: duration, curve: curve, ); }}Copy the code
PipelineOwner
The pipeline owner manages the rendering pipeline.
The manager of the entire rendering process is embodied in the following methods:
- flushLayout
To iterate over the Render Object node that needs Relayout, call _layoutWithoutResize() to rewrite the computed layout
- flushCompositingBits
Iterate over the nodes that require CompositingBitsUpdate and call the _updateCompositingBits() method to update needsCompositing. This is usually executed between flushLayout and flushPaint.
- flushPaint
Iterate over the node that needs repaint, triggering the paint painting by calling the _paintWithContext method of the child Render Object with PaintingContext.
- flushSemantics
Update semantics
RenderBox
RenderBox is a subclass of RenderObject, which is a further encapsulation of RenderObject in 2D Cartesian coordinates. It mainly encapsulates the following function points:
- Parentdata is
BoxParentData
onlyoffset
Property defaults toOffset.zero
- use
BoxConstraints
As its constraint - use
size
Record its size - Measure its maximum, minimum width and height
- The measurement baseline
- Implement the default hit test scheme
- Mixed with
RenderObjectWithChildMixin
Implementation of single child ->SingleChildRenderObjectWidget
- Mixed with
ContainerRenderObjectMixin
Implementation of multiple children ->MultiChildRenderObjectWidget
The development App draws in Cartesian coordinates, so inheriting RenderBox will suffice for most scenarios.
conclusion
This section describes the main functions and methods of RenderObject. Understanding these will help us better understand the underlying principles of Flutter UI and help us to implement our own custom Flutter controls. For details on how to draw the layout to the screen, I will use examples below.
It hurts my hair to understand how it works!