Hello everyone, I am Guo Shuyu, the author of “Detailed Description of Flutter Development”. From the title, I know that today I want to share with you some topics related to Flutter. The content is relatively straightforward and simple, that is, knowledge about the layout of Flutter.

I’m sure you’ve all heard of or used Flutter and may have some idea about it. But as the title suggests, the theme of this time is to introduce you to a different kind of Flutter, or something that is often overlooked by Moxin. So this time we will look at Flutter from a different Angle. Take a look at the interesting aspects of Flutter’s size layout.

First, before we begin

Before we talk about the layout of a Flutter, what do you think a Flutter is?

Flutter is primarily a cross-platform UI framework. Its core capability is to make THE UI cross-platform. Unlike other cross-platform frameworks, Flutter has a near-native performance while achieving control and platform independence.

But if you’ve used Flutter, you know that the interface that we write in Flutter is done with widgets and can look like many layers are nested. Why?

There are widgets, Elements, RenderObject, Layer, and other key core Settings in Flutter.

The Widget we write most often is not a real View instance, it needs to be converted to the corresponding RenderObject to draw, and Element is a key intermediate instance of the Widget and RenderObject. The BuildContext we use in our daily Flutter development is the Element abstraction object.

This is basically Widget -> Element -> RenderObject.

So the Widget code in Flutter is just a “configuration file”, the real working instances are its corresponding Element and RenderObject entities inside Flutter.

This is why a Widget can be immutable. It can be built frequently while in use, because it is not really working. The Widget carries the state information needed to draw in the RenderObject.

As a simple example, we define a Text Widget as shown in the code below, add it in four different places, and run successfully. If it is a real View, it cannot be loaded in four places at the same time.

As you can see from this example, the Widget doesn’t really do the work, and the main logic responsible for drawing and layout is in the RenderObject. Since much of the logic for layout and drawing is in RenderObject, today’s focus will be on RenderObject.

RenderObject, as the drawing and layout entity of Flutter, can be divided into two main subclasses: RenderBox and RenderSliver. RenderSliver is mainly used in slidable list scenarios, so this time we mainly discuss RenderBox layout scenarios.

2. The layout of Flutter

The normal Size layout of a Flutter is to pass Constraints from top down and return Size from bottom up.

The parent container passes down a constraint according to the layout needs, and the child container returns an explicit size according to its state. If it does not have one, the child recurses down.

Pass constraints from top to bottom, usually minHeight, maxHeight, minWidth, maxWidth, etc., but return size from bottom to top with a fixed width and height.

For Flutter, the layout logic is mainly in the corresponding performLayout of the RenderObject.

If you are interested in the layout of a Widget, you can find the RednerObject for the Widget and see how the performLayout logic is implemented.

Container, the most commonly used abstract configuration template for Flutter, uses ConstrainedBox, ConstrainedBox, and ConstrainedBox. And either ConstrainedBox or SizedBox, their corresponding RenderObject is RenderConstrainedBox.

So let’s use RenderConstrainedBox as an example to see how ConstrainedBox is laid out.

2.1. ConstrainedBox constraint layout

As shown in the following code, you can see that ColoredBox does not specify a size, but when run ColoredBox returns a red square of 100 x 100, Because its parent, ConstrainedBox, passes down a 100 x 100 ConstrainedBox constraint.

Scaffold(
  body: Center(
    child: ConstrainedBox(
      constraints: BoxConstraints(
          maxHeight: 100, minHeight: 100, maxWidth: 100, minWidth: 100),
      child: ColoredBox(
        color: Colors.red,
      ),
    ),
  ),
)
Copy the code

So what happens if I change the width and height of min to 10?

You can see that the size of the ColoredBox becomes the same as the width and height of min. Why?

Scaffold(
  body: Center(
    child: ConstrainedBox(
      constraints: BoxConstraints(
          maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
      child: ColoredBox(
        color: Colors.red,
      ),
    ),
  ),
)
Copy the code

ColoredBox does not implement its own performLayout. Instead, it inherits the default logic of RenderProxyBox. This situation is common in Flutter.

  • If there is no child, use the constraints. Smallest value which can be passed to the constraint.
  • Use the size of a child when there is one;

As we know, the smallest control that does not implement a custom performLayout and which does not have a child will most likely follow the parent constraint.

If you add a child of 80 to the ColoredBox, you can see that the red box changes to the size of the ColoredBox’s child, which is 80 instead of the smallest, because the ColoredBox now has a child. I’m using the size of the child.

Scaffold(
  body: Center(
    child: ConstrainedBox(
      constraints: BoxConstraints(
          maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
      child: ColoredBox(
        color: Colors.red,
        child: SizedBox(
          width: 80,
          height: 80(), (), (), (Copy the code

What if I change the size of the ColoredBox’s child to 150?

You can see that the red block is still 100, not 150.

 Scaffold(
  body: Center(
    child: ConstrainedBox(
      constraints: BoxConstraints(
          maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
      child: ColoredBox(
        color: Colors.red,
        child: SizedBox(
          width: 150,
          height: 150(), (), (), (Copy the code

Why is that?

The RenderConstrainedBox will render 100 even though we configured the SizedBox with 150 parameters.

There are two points:

  • The first is that the Widget is only configuration information. We set the width and height to 150, but the RenderObject output is 100, so we are not writing a real View. The real layout depends on the RenderObject.

  • SizedBox’s RenderConstrainedBox shows that its performLayout implementation uses enforce to 100 the size of parent when there is no child

Clamp (10, 100) is used to complete internal enforcement of Enforce by MEANS of API. Enforce effect is equivalent to 150.clamp(10, 100), so 100 result will be obtained.

Clamp returns this value if the data is within the range, and returns the nearest boundary value otherwise.

So enforceRenderConstrainedBoxDoes not exceed the size of the parent container.

So for the sake of the experiment, let’s change the SizeBox to ConstrainedBox and set it to a size between 10 and 150 constraints.

Scaffold(
  body: Center(
    child: ConstrainedBox(
      constraints: BoxConstraints(
          maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
      child: ColoredBox(
        color: Colors.red,
        child: ConstrainedBox(
          constraints: BoxConstraints(
              maxHeight: 150, minHeight: 10, maxWidth: 150, minWidth: 10() (() (() (() () (Copy the code

You can see that the red square is 10 again. Why is that?

From the source can be seen:

  • First of all,enforceExecution is150.clamp(10, 100)10.clamp(10, 100)Wait until the nature is10-100.;
  • And then toconstrainClamp (10, 100), so the output is the minimum value of 10;

Clamp (10, 100) was 100.clamp(10, 100) is naturally 100, and now it is 0. Clamp (10, 100) is naturally 10.

From the above example, you can see that the parent layout constraint affects the size of the child, or even restricts the return of the child’s size, but this is obtained after child.layout.

What if you want to get the size of the child before child.layout? Get the size of the child before the layout, okay?

Is that ok? Of course you can! RenderBox usually has these four methods:

  • computeMaxIntrinsicWidth
  • computeMinIntrinsicWidth
  • computeMaxIntrinsicHeight
  • computeMinIntrinsicHeight

Why is it average?

Because you don’t write it, you don’t get an error, and these four methods are rarely called. The official description of these methods is that they are expensive, and we call them not directly, but through the corresponding get method:

  • getMaxIntrinsicWidth
  • getMinIntrinsicWidth
  • getMaxIntrinsicHeight
  • getMinIntrinsicHeight

In the default specification, you can override the API starting with compute to implement the required logic, then call the API starting with compute that can only be called by get, and then execute the API starting with compute, which is one-to-one.

Also is invoked by getMinIntrinsicWidth, such as: child. Call to computeMinIntrinsicWidth getMinIntrinsicWidth eventually.

How does RenderBox get the Child? How does a Child change from Widget to RenderObject?

Here’s what Element does when the Widget is loaded:

  • Is calledinflateWidgetTo create itElementAnd then throughmountcreateRenderObjectThat created itRenderObject;
  • Execute laterattachRenderObject That’s when the child passes_findAncestorRenderObjectElementFind its parent, the one closest to itRenderObjectElment;
  • Last to execute parentinsertRenderObjectChild, the child is insertedRenderObjectIn theRenderObjectYou can get it inWidget;

When a Child is loaded inside an Element, a RenderObject is created, and its parent is found and added to it.

A Flutter should also have a RenderObject Element, such as ComponentElement (StatelessWidget, etc.).

Here you can see the link between elements.

3. Layout of multiple Children

Having introduced the layout of a single Child, here are the main differences between multiple children.

Multiple children, like a single one, pass Constraints from top to bottom and return Size from bottom to top.

For example, here is the example we saw earlier, where the Column control is used to lay out multiple texts.

Column and Row are both subclasses of Flex. If we look at RenderFlex’s implementation, we can see that there are several key points for multiple Child layouts:

  • MultiChildRenderObjectWidget;
  • MultiChildRenderObjectElement;
  • ParentData;

We won’t go into the logic of Widget and Element here, but the main difference is the ParentData in RenderBox.

As you can see above, almost all implementations of Multi Child have their own specific ParentData, and they do not inherit ParentData directly, but their subclass ContainterBoxParentData.

As the picture shows, their functions are:

  • BoxParentDatahaveOffsetArgument is used to feel the position of the Child in the control.
  • ContainterBoxParentDataWith twoSiblingParameters, mainlyRenderBoxChildren are accessed through this double linked list;
  • FlexParentDataIs the currentRenderFlexParameters required for the layout;

As you can see, this is the key parameter for RenderFlex layout. The children Widget we added, after being loaded by Element, The insert step described earlier changes from a List

to a bidirectional linked List joined together by ParentData’s two Sibling arguments, through which access is made.

So in the children layout, we return child by the corresponding ParentData subclass, and then determine the position of the child by setting Offset to ParentData.

With CustomMultiChildLayout, you don’t need to implement it step by step. Scaffolding is the default page scaffolding.

Four, interesting knowledge points

While we’re at it, let’s delve into some interesting points, such as the Scaffold that appears all the time in the previous code. This Scaffold is the most commonly used page Scaffold for our Flutter development and the beginning of a page layout.

If that Scaffold is removed and the original code is run, you see that the screen is red, ConstrainedBox fills the screen.

MaterialApp(
  title: 'GSY Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: ConstrainedBox(
    constraints: BoxConstraints(
        maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
    child: ColoredBox(
      color: Colors.red,
    ),
  ),
);
Copy the code

Why is that?

Clamp (392.72, 392.72) We can see from the debug tool of Flutter that the constraint given to you by the superiors is the screen size, no interval, and The thief is equal to 10. Clamp (392.72, 392.72)

Clamp (392.72, 392.72) also acts on the width of the screen.

What if we add a Center control?

You can see the constraint size again!

MaterialApp(
  title: 'GSY Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: Center(
	  child:ConstrainedBox(
	    constraints: BoxConstraints(
	        maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
	    child: ColoredBox(
	      color: Colors.red,
	    ),
	  )
  ),
);
Copy the code

Clamp (0, 392.72)

Why is that?

Because the RenderObject of Center is the RenderPositionedBox, it has a constraints. Loosen () when it is used for layout, which is why you sometimes add a new Center layout and it suddenly takes effect. Loosen = 0-392.72

BoxConstraints loosen() {
  assert(debugAssertIsValid());
  return BoxConstraints(
    minWidth: 0.0,
    maxWidth: maxWidth,
    minHeight: 0.0,
    maxHeight: maxHeight,
  );
}
Copy the code

If you do not add Center, why does scaffolding like the one used before allow BoxConstraints to work?

Because it’s going to happen even though it’s not in the right place, it’s going to be easier to see it at 100.

Scaffold(
  body: ConstrainedBox(
    constraints: BoxConstraints(
        maxHeight: 100, minHeight: 100, maxWidth: 100, minWidth: 100),
    child: ColoredBox(
      color: Colors.red,
    ),
  ),
)
Copy the code

That’s because the Scaffold implementation is a control called CustomMultiChildLayout.

The Scaffold with CustomMultiChildLayout uses a Constraints subclass called bodyBoxConstraints for body. This class defaults to 0 for all min values.

Therefore, for child under body, there will be 0 min constraint information.

Clamp (0, 392.72) can be effective.

Where is the size returned by child used?

The answer must be paint. What is this Offset?

For example, if we look at the Center we used before, it will add the Offset information to the paintChild, so that the child will be Offset when drawing, so that it will be drawn to the exact place.

Therefore, as shown in the figure below, ColoredBox draws the red box of the corresponding position through Offset (to determine the position) and Size (to determine the Size) when Rect is drawn.

What if I draw something that doesn’t follow this Offset?

Here we can use a simple example to draw a Demo directly with CustomPaint.

new Container(
  height: 200,
  width: 200,
  color: Colors.greenAccent,
  child: CustomPaint(
    ///Animate directly with values
    foregroundPainter: _AnimationPainter(animation1),
  ),
)
Copy the code

As you can see, although CustomPaint is at a size of 200 x 200, the animated circles can be directly larger than this size.

Therefore, it can be seen that Flutter is essentially a drawing board, layered with various layers and drawing controls according to the agreed Size and Offset on each Layer.

A Layer is a collection of RenderObjects.

In fact, as long as you get the Canvas of this Layer, you can know any position on this Layer. Of course, in general, in order to draw the correct layout, you still need to follow this rule.

Typically, each Route is a separate Layer.

conclusion

A final conclusion:

  • WidgetIt’s just a configuration file, it’s immutable, it’s refactored every time it changes, it’s not reallyView ;
  • Layout logic is mainly inRenderBoxA subclassperformLayoutAnd can be obtained in advancechild.size
  • ElementThe connection function of,WidgetIs created when it is first loadedElementRenderObjectAnd connected together;
  • morechildThe layout is throughContainerBoxParentDataTo access multiple children;
  • Constraint layoutsmallestWhether and have 0 value (interval minimum value) will affect the effect of constraint;
  • Control is drawn following the correspondingSizeOffsetAnd you can go beyondSizeDraw, depending on where you areLayerCanvas