Flutter framework analysis (four)-RenderObject

Flutter framework analysis (four)-RenderObject

1 Introduction

In Flutter , the main responsibility of RenderObject is layout and drawing. Through the Element Tree introduced in the previous article , Flutter Framework will generate a RenderObject Tree . Its main functions are as follows:

  • Layout, starting from RenderBox , lay out the RenderObject Tree from top to bottom.
  • Drawing, through the Canvas object, RenderObject can draw itself and its child nodes in the RenderObject Tree .
  • Click test, RenderObject passes the click event from top to bottom, and controls whether to respond to the click event by its position and behavior .

RenderObject Tree is the underlying layout and drawing system. Most Flutter developers do not need to directly interact with the RenderObject Tree , but use Widget , and then the Flutter Framework will automatically build the RenderObject Tree .
RenderObject has a parent and a ParentData slot ( Slot ), the so-called slot refers to a reserved interface or position, this interface and position are accessed or occupied by other objects, this interface or position is usually in the software It is represented by a reserved variable , and ParentData is a reserved variable, which is assigned by the parent . The parent usually stores some data related to the child elements through the ParentData of the child RenderObject . For example, in the Stack layout, RenderStack will Store the offset data of the child element in the ParentData of the child element (seePositioned implementation). The related principles of ParentData will be detailed in the article "Flutter Framework Analysis (6)-Parent Data", and interested readers can read this article.

2. RenderObject classification

As shown in the figure above, RenderObject is mainly divided into four categories:

  • RenderView

RenderView is the root node of the entire RenderObject Tree and represents the entire output interface.

  • RenderAbstractViewport

RenderAbstractViewport is a type of interface designed for RenderObject that only displays part of its content.

  • RenderSliver

RenderSliver is the base class of RenderObject that implements the sliding effect, and its commonly used subclasses include RenderSliverSingleBoxAdapter and so on.

  • RenderBox

RenderBox is a base class of RenderObject that uses a 2D Cartesian coordinate system. The general RenderOBject is inherited from RenderBox, such as RenderStack, etc. It is also the base class of custom RenderObject.

3. Core Process

RenderObject is mainly responsible for layout, drawing, and hit testing. These core processes will be explained separately below.

  • layout

The function corresponding to the layout is layout . The main function of this function is to lay out this node and its child nodes through control parameters such as Constraints and parentUsesSize passed from the upper node . Constraints are constraints on the layout of nodes. The principle is that Constraints are down, Sizes are up, and the parent node sets the position of this node. which is:

  1. A Widget gets Constraints from its parent node and passes it to the child nodes.
  2. The Widget lays out its child nodes.
  3. Finally, the node tells its parent node its Sizes .

In the next article, we will introduce the process in detail, for now we only need to remember the principle.

When the layout of this node depends on the layout of its child nodes, the value of parentUsesSize is true. At this time, when the child node is marked as needing layout, this node will also be marked as needing layout. In this way, when the next frame is drawn, both the current node and the child nodes will be re-layout. Conversely, if the value of parentUsesSize is false, there is no need to notify this node when the child nodes are re-layout.

Subclasses of RenderObject should not directly override the layout function of RenderObject , but override the performResize and performLayout functions. These two functions are really responsible for the specific layout.

The source code of the layout function in RenderObject is as follows:

void layout(Constraints constraints, { bool parentUsesSize = false }) { //1. Determine whether you need to re-layout according to relayoutBoundary RenderObject relayoutBoundary; if (!parentUsesSize || sizedByParent || constraints.isTight || parent is ! RenderObject) { relayoutBoundary = this ; } else { relayoutBoundary = (parent as RenderObject)._relayoutBoundary; } if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) { return ; } _constraints = constraints; //2. Update the relayout boundary of the child node if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) { //The local relayout boundary has changed, must notify children in case //they also need updating. Otherwise, they will be confused about what //their actual relayout boundary is later. visitChildren(_cleanChildRelayoutBoundary); } _relayoutBoundary = relayoutBoundary; //3. Recalculate the size and re-layout if (sizedByParent) { try { performResize(); } catch (e, stack) { _debugReportException( 'performResize' , e, stack); } } try { performLayout(); markNeedsSemanticsUpdate(); } catch (e, stack) { _debugReportException( 'performLayout' , e, stack); } _needsLayout = false ; markNeedsPaint(); } Copy code

As you can see from the source code, relayoutBoundary is an important parameter in the layout function. When the size of a component is changed, the size of its parent may also be affected, so the parent node needs to be notified. If you iterate up in this way, you need to notify the entire RenderObject Tree to re-layout, which will inevitably affect the layout efficiency. Therefore, Flutter divides the RenderObject Tree by the relayBoundary , and if it encounters the relayBoundary , it does not notify its parent node to re-layout, because its size does not affect the size of the parent node. In this way, you only need to re-layout a section of the RenderObject Tree , which improves the layout efficiency. About relayoutBoundary will be explained in detail in a later article. At present, only need to understand that relayoutBoundary will segment the RenderObject Tree to improve layout efficiency.

  • draw

The corresponding function for drawing is paint , and its main function is to draw the RenderObject and sub- RenderObject on the Canvas . Subclasses of RenderObject should override this function and add drawing logic to this function.

The paint function source code of RenderFlex, a subclass of RenderObject , is as follows:

void paint(PaintingContext context, Offset offset) { //1. No overflow, draw directly if (!_hasOverflow) { defaultPaint(context, offset); return ; } //2. Empty, no need to draw //There's no point in drawing the children if we're empty. if (size.isEmpty) return ; //3. Determine whether the overflow boundary part needs to be clipped according to clipBehavior if (clipBehavior == Clip.none) { defaultPaint(context, offset); } else { //We have overflow and the clipBehavior isn't none. Clip it. context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint, clipBehavior: clipBehavior); } //4. Drawing overflow error prompt assert (() { //Only set this if it's null to save work. It gets reset to null if the //_direction changes. final List <DiagnosticsNode> debugOverflowHints = <DiagnosticsNode>[ ErrorDescription( 'The overflowing $runtimeType has an orientation of $_direction .' ), ErrorDescription( 'The edge of the $runtimeType that is overflowing has been marked ' 'in the rendering with a yellow and black striped pattern. This is''usually caused by the contents being too big for the $runtimeType .' ), ErrorHint( 'Consider applying a flex factor (eg using an Expanded widget) to ' 'force the children of the $runtimeType to fit within the available''space instead of being sized to their natural size.' ), ErrorHint( 'This is considered an error condition because it indicates that there ' 'is content that cannot be seen. If the content is legitimately bigger''than the available space, consider clipping it with a ClipRect widget ' 'before putting it in the flex, or using a scrollable container rather ' 'than a Flex, like a ListView.' ), ]; //Simulate a child rect that overflows by the right amount. This child //rect is never used for drawing, just for determining the overflow //location and amount. Rect overflowChildRect; switch (_direction) { case Axis.horizontal: overflowChildRect = Rect.fromLTWH( 0.0 , 0.0 , size.width + _overflow, 0.0 ); break ; case Axis.vertical: overflowChildRect = Rect.fromLTWH( 0.0 , 0.0 , 0.0 , size.height + _overflow); break ; } paintOverflowIndicator(context, offset, Offset.zero & size, overflowChildRect, overflowHints: debugOverflowHints); return true ; }()); } Copy code

The logic of this part of the code is to first judge whether there is overflow, call defaultPaint to complete the drawing if there is no overflow, and then see whether it is empty, return directly if the size is empty, and finally draw the overflow information.

Wherein defaultPaint source as follows:

void defaultPaint(PaintingContext context, Offset offset) { ChildType child = firstChild; while (child != null ) { final ParentDataType childParentData = child.parentData as ParentDataType; context.paintChild(child, childParentData.offset + offset); child = childParentData.nextSibling; } } Copy code

It can be seen that defaultPaint will call paintChild to draw child nodes, and if the child node has child nodes, paintChild will eventually call its paint and then call defaultPaint , thus forming a recursive call to draw the entire RenderObject Tree .

  • Hit test

The hit test is to determine whether a component needs to respond to a click event, and its entrance is the hitTest function of RenderView , the root node of the RenderObject Tree . The following is the source code of the function:

bool hitTest(HitTestResult result, {Offset position }) { if (child != null ) child.hitTest(BoxHitTestResult.wrap(result), position: position); result.add(HitTestEntry( this )); return true ; } Copy code

As can be seen from the constructor of RenderView , child is the RenderBox class, so let's look at the hitTest function of RenderBox .

bool hitTest(BoxHitTestResult result, { @required Offset position }) { if (_size.contains(position)) { if (hitTestChildren(result, position: position) || hitTestSelf(position)) { result.add(BoxHitTestEntry( this , position)); return true ; } } return false ; } Copy code

The code logic is very simple. If the click event location is within the RenderObject , and if it is within it, and hitTestSelf or hitTestChildren returns true, it means that the RenderObject has passed the hit test and needs to respond to the event. At this time, the clicked RenderObject needs to be added to the BoxHitTestResult list. , And the click event will no longer be passed down. Otherwise, it is considered that the hit test is not passed, and the event continues to pass down. Among them, the hitTestSelf function indicates whether the node passes the hit test, and hitTestChildren indicates whether the child node passes the hit test.

4. Core functions

There are many core functions of RenderObject , and it is difficult to list them one by one . The three core functions of RenderObject have been explained in detail in the core process . In order to facilitate the understanding of the role of each core function, the core function of RenderObject and the core function of Android View are compared here. The following is a comparison table.

effectFlutter RenderObjectAndroid View
drawpaint()draw()/onDraw()
layoutperformLayout()/layout()measure()/onMeasure(), layout()/onLayout()
Layout constraintsConstraintsMeasureSpec
Layout Agreement 1The Constraints parameter of performLayout() indicates the layout restriction of the parent node to the child nodeThe two parameters of measure() indicate the layout limit of the parent node to the child node
Layout agreement 2performLayout() should call the layout() of each child nodeonLayout() should call the layout() of each child node
Layout parametersparentDatamLayoutParams
Request layoutmarkNeedsLayout()requestLayout()
Request drawingmarkNeedsPaint()invalidate()
Add childadoptChild()addView()
Remove childdropChild()removeView()
Associate to window/treeattach()onAttachedToWindow()
Disassociate from window/treedetach()onDetachedFromWindow()
Get parentparentgetParent()
Touch eventhitTest()onTouch()
User input eventhandleEvent()onKey()
Rotation eventrotate()onConfigurationChanged()

Visible, RenderObject and the Android View plethora of functions corresponding up, RenderObject with respect to the Android View the layout rendering functions demolition separate out, simplified View logic.

5. Summary

This article mainly introduces RenderObject related knowledge, focusing on its classification, core process, and core functions. The key points are as follows:

  • RenderObject is mainly responsible for drawing, layout, hit testing, etc.
  • The principle of RenderObject layout is that Constraints are down, Sizes are up, and the parent node sets the position of this node.
  • RenderView is the root node of the entire RenderObject Tree , and its child is a RenderObject of type RenderBox .

6. Reference documents

Analysis of Flutter actual combat
Flutter RenderObject

7. Related articles

Flutter analysis framework (a) - Architecture Overview
Flutter framework analysis (two) - Widget
Flutter framework analysis (three) - Element
(five) -Widget, Element, RenderObject tree Flutter frame analysis
Flutter framework analysis (six) -Constraint
Flutter Frame analysis (7)-relayBoundary
Flutter frame analysis-Parent Data
Flutter frame analysis-InheritedWidget