In the development of Flutter, the interface was implemented as a nested Widget. Widgets such as MaterialApp, Container, Text, etc., are widgets. Everything is a Widget.

Introduction to Widget Principles

1. What is StatelessWidget and StatefulWidget

StatelessWidget and StatefulWidget inherit from widgets. The StatelessWidget has fewer methods, no refresh logic, and simply implements the Build method to return the Widget tree. The StatefulWidget has page refresh logic, so it moves the build method into the derived class State, which is used to save the page State, and has a series of callback methods that can trigger rebuild through the setState method to refresh the page.

Both classes have a createElement() method, using StatelessElement as an example:

/// An [Element] that uses a [StatelessWidget] as its configuration.
class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true; rebuild(); }}Copy the code

The constructor passes in the current Widget instance, and the build method calls the Widget’s method and passes itself in. Here we can see that the BuildContext parameter in the build method is the Element class.

abstract class Element extends DiagnosticableTree implements BuildContext
Copy the code

As we can see, these two classes are not responsible for the drawing and layout of the control itself. Their value is to make an association between the Widget tree generated by the build method and the Element. The actual drawing, layout, and so on are implemented by the widgets under the build method.

2, SingleChildRenderObjectWidget

SingleChildRenderObjectWidget inherited from RenderObjectWidget, can accommodate a child widgets, rewrite the createElement method method. Many of the container implementations in Flutter inherit from this implementation, such as Align, ColoredBox, ConstrainedBox, and so on. SingleChildRenderObjectWidget and RenderObjectWidget classes are abstract classes, subclasses need to implement to return different RenderObject createRenderObject method, Overwrite the updateRenderObject method to update to complete different layouts or draw effects.

Analogy in the Android code, SingleChildRenderObjectWidget equivalent to a ViewGroup SingleChild, can only hold a child widgets, to its layout, higher customized wide, also can undertake some draw yourself.

3, MultiChildRenderObjectWidget

MultiChildRenderObjectWidget also inherit from RenderObjectWidget, unlike SingleChildRenderObjectWidget, he can hold a collection of child that create instances of Element is not the same. In Flutter, containers that can hold multiple Child Widgets inherit from this implementation. The Row and Column, for example, they inherit from Flex, while the Flex inherits from MultiChildRenderObjectWidget.

This kind of implementation is similar to the responsibilities of the ViewGroup in Android, we need to set the children width and height, and draw the area set and a series of operations.

4, LeafRenderObjectWidget

This type of exposure will be relatively small. The official class comment reads:

/// A superclass for RenderObjectWidgets that configure RenderObject subclasses
/// that have no children.
Copy the code

LeafRenderObjectWidget also inherits from RenderObjectWidget, except that it does not contain any sub-widgets. The main responsibility of this Widget is to draw by itself. Texture, AndroidSurfaceView, RawImage, etc. The main thing they focus on is drawing.

5, summary

In the official Library of Flutter, we are provided with hundreds of Widget implementations. It is not practical or necessary to master and understand the use of so many widgets at the same time. Many of the official widgets are actually inherited implementations, and as we can see above, the actual class that affects the Widget style is RenderObject, which is the core of the drawing and layout. After mastering and understanding RenderObject, we can completely customize some layout or drawing, so that when the implementation of the official class library cannot meet our needs, we will not be at a loss.

RenderObject parsing

1. What is RenderObject

Let’s start with the official introduction:

/// An object in the render tree.
///
/// The [RenderObject] class hierarchy is the core of the rendering
/// library's reason for being.
///
/// [RenderObject]s have a [parent], and have a slot called [parentData] in
/// which the parent [RenderObject] can store child-specific data, for example,
/// the child position. The [RenderObject] class also implements the basic
/// layout and paint protocols.
Copy the code

As you can see, RenderObject is the core of the rendering library implementation. You can implement it to measure widgets, draw layouts, and more. In the previous section we also saw that each of the core libraries was implemented with a custom RenderObject.

RenderObject can have a parentData, which can be used to store some child-specific information, such as the location of the child. However, the RenderObject class itself is not defined as a container. To hold other RenderObjects, you need to implement some official mixins. RenderObject is a more top-level definition. In many scenarios, implementing a subclass of RenderObject, RenderBox, would be a better choice.

2. Main methods of RenderObject are introduced

void setupParentData(covariant RenderObject child)

To override this method, we can set a ParenData for the child before it is added. Defining different ParenData can store some of the information we need to meet different needs.

void markNeedsLayout()

This method marks the layout information, and the render pipeline updates the layout information of the object at an appropriate time. If a Parent of a RenderObject declares the layout information that needs to be used by that RenderObject, the Parent of that RenderObject will also be marked when the markNeedsLayout() method is called for that RenderObject. When both the parent and child containers need to be relaid, only the parent container is notified. When the layout is complete, the child container’s layout method is called, and the child container’s layout is completed.

  • FlutterThe layout andAndroidThe layout name looks similar, but the actual function is not quite the sameFlutterThe layout operation performed in is closer toAndroidHe is mainly responsible for completing the WidgetConstraintsConfiguration of layout constraints.ConstraintsThis affects the area where the RenderObject is displayed.

void layout(Constraints constraints, { bool parentUsesSize = false })

This method is usually called for a child. This method passes Constraints to the child, which describes the width and height Constraints that the Child can use and which the child needs to adhere to. If you need the width and height of the child to adjust the size information, you need to pass parentUsesSize to true, so that the RenderObject will be notified to update the layout information when the child layout information changes. This method should not be overridden. Instead, the performResize or performLayout methods should be overridden. The layout method will delegate the actual work to these two methods. If RenderObject is defined as a container, then it needs to call this method for all renderObjects it holds.

bool get sizedByParent

The get method returns false by default, which indicates that the size of the RenderObject is not affected by the parent container, so we can set the size of the RenderObject in the performLayout method. If true is returned, then we need to adjust the RenderObject’s size setting to performResize.

void performResize()

Override this method to update the Widget size information. If you want to rely on the size of the parent container to set the size, you need sizedByParent

Returns true. This method is called when the parent container has finished laying out the layout. This is where you get the layout constraint information passed by the parent container. This method is only going to be called if sizedByParent returns true.

void performLayout()

Normally we would override this method to set the size information for this RenderObject. If there is a child, we would also need to call the Layout method for that child in this method.

void markNeedsPaint()

This method is called when the RenderObject needs to be redrawn. The render pipeline is planned after the call, and the RenderObject’s paint method is updated after an appropriate actual call.

Rect get paintBounds

Use this method to get the area that this RenderObject should draw. If null is returned, the RenderObject will be fully drawn. This Rect is also used by the showOnScreen method to display the Rect.

void paint(PaintingContext context, Offset offset)

Overwriting this method, we can get the Canvas object to do some drawing by calling paintingContext.canvas. Normally, we need to shift the starting point of the canvas to offset. If we do not move, the default starting point of the canvas will be the top left corner of the screen. If the RenderObject is a container type, you can call here PaintingContext. PaintChild to draw the specified child, paintChild method need to pass a child and Offset, according to the Offset of the incoming is different, Affects where the child is displayed on the screen.

Compared to the onDraw method on Android, the paint method in the Flutter should be drawn according to the Offset. This Offset defines the starting point information that this RenderObject should draw.

Rect operator &(Size other) => Rect.fromLTWH(dx, dy, other.width, other.height);
Copy the code

The Offset class overwrites the operator &, and we previously determined the Size of this RenderObject. With Offset & Size we get the Rect, which is the region that this RenderObject should draw.

  • You can draw from any area of the screen, but it’s best to stick to this area and not draw boundaries unless you have special needs.

3, RenderBox

In terms of RenderObject, we have to talk about RenderBox, which is a very important subclass of RenderObject. Because the definition of a RenderObject is at the top level, even the height and width are not defined, so we usually need to inherit the RenderBox to customize the RenderObject.

RenderBox defines a series of getMinIntrinsicHeight methods, such as getMinIntrinsicHeight, getMaxIntrinsicWidth, etc. These methods are essentially called externally to expose the width and height of the RenderBox. We should rewrite is some such as computeMaxIntrinsicWidth, computeMinIntrinsicHeight

Method to calculate and return the desired width and height information. RenderBox also provides the Size property, which specifies the Size of the RenderBox. This Size is usually calculated based on the constraint information to arrive at an appropriate Size. The Size of Size must not exceed constraints. MaxWidth and constraints. MaxHeight in the layout constraints, otherwise rendering errors will occur. RenderBox also defines the getDistanceToBaseline method to be called externally to return the y-base distance from the given TextBaseline. GetDistanceToBaseline method return values calculated by the computeDistanceToActualBaseline method.

RenderBox has many subclasses, and most of the official container RenderObject implementations are subclasses of RenderBox. Such as the Align, he inherited from SingleChildRenderObjectWidget, is the realization of a RenderObject RenderPositionedBox, RenderPositionedBox hierarchy is as follows:

Does that seem like a lot? In fact, this is just the official responsibility to do, each implementation class to do a few different things. RenderShiftedBox takes the child’s width and height information and overwrites the paint method so that the child is drawn in the correct area. RenderAligningShiftedBox sets an Offset for the child’s parentData by calculating the Alignment. The core method is as follows

/// Apply the current [alignment] to the [child].
///
/// Subclasses should call this method if they have a child, to have
/// this class perform the actual alignment. If there is no child,
/// do not call this method.
///
/// This method must be called after the child has been laid out and
/// this object's own size has been set.
@protected
void alignChild() {
  _resolve();
  assert(child ! =null);
  assert(! child! .debugNeedsLayout);assert(child! .hasSize);assert(hasSize);
  assert(_resolvedAlignment ! =null);
  finalBoxParentData childParentData = child! .parentDataasBoxParentData; childParentData.offset = _resolvedAlignment! .alongOffset(size - child! .sizeas Offset);
}
Copy the code

The RenderPositionedBox is used to define the size of the container. The RenderPositionedBox is used to fill the size of the layout Constraints as much as possible. If there is a child, the alignChild method of RenderAligningShiftedBox is called to calculate the offset, and the child is eventually displayed at the specified position.

4, summary

RenderObject is an object in the Render tree whose implementation affects how the Widget will eventually appear on the screen. He defined some basic methods by rewriting

With the performLayout() or performResize() method, we confirm the size of this RenderObject and pass the layout constraint information by calling layout for the child in the performLayout() method. In the paint method, we can do some drawing, and if we have a child, we need to do some drawing for the child. The rendering order also affects the actual display. The effects of foreground and background colors, such as Decoration, are mainly the result of the drawing order. To change the display position of the Child, we need to set the drawing Offset for the child relative to the Widget. Generally speaking, the Offset is stored in the parentData of the child. When paintChild is called to draw, the Offset in parentData needs to be fetched and plotted with the Offset passed above.

Custom layout combat

1. Layout constraints

Above, when we introduced the component rendering process, we learned that the controls in the Flutter need to perform a Layout before rendering on the screen. The process can be divided into two linear processes: constraints are passed down from the top and layout information is passed up from the bottom. The process can be shown in the following figure.

The first linear procedure is used to pass layout constraints. The parent node passes constraints to each child node, which are rules that each child node must follow during the layout phase. It’s like a parent telling their child, “You have to follow the school rules before you can do anything else.” Common constraints include specifying the maximum and minimum widths of child nodes or the maximum and minimum heights of child nodes. This constraint extends down, and the child component produces a constraint that it passes to its children, all the way down to the leaf.

The second linear process is used to convey specific layout information. After receiving the constraint from the parent node, the child node will generate its own specific layout information according to it. For example, the parent node specifies that my minimum width is 500 unit pixels, and the child node may define its width to be 500 pixels, or any value greater than 500 pixels according to this rule. In this way, once you have your layout information, you tell it to the parent node. The parent node will continue to do this and pass it up to the very top.

Let’s look at what specific layout constraints can be passed in the tree. There are two main layout protocols in Flutter: the Box protocol and the Sliver sliding protocol. Here we take the box protocol as an example to expand the specific introduction.

In the box protocol, the constraint passed by the parent to its children is BoxConstraints. This constraint specifies the maximum and minimum width and height allowed for each child node. The parent node passes in BoxConstraints with Min Width 150 and Max Width 300:

When the child node accepts the constraint, it can obtain the value in the green range in the figure above, that is, the width is between 150 and 300, and the height is greater than 100. After obtaining the specific value, it will upload the value of the specific size to the parent node, so as to achieve the parent-child layout communication.

2, Alignment,

Alignment defines a point that represents a position in a matrix. Alignment defines the following points by default:

/// The top left corner.
static const Alignment topLeft = Alignment(1.0.1.0);

/// The center point along the top edge.
static const Alignment topCenter = Alignment(0.0.1.0);

/// The top right corner.
static const Alignment topRight = Alignment(1.0.1.0);

/// The center point along the left edge.
static const Alignment centerLeft = Alignment(1.0.0.0);

/// The center point, both horizontally and vertically.
static const Alignment center = Alignment(0.0.0.0);

/// The center point along the right edge.
static const Alignment centerRight = Alignment(1.0.0.0);

/// The bottom left corner.
static const Alignment bottomLeft = Alignment(1.0.1.0);

/// The center point along the bottom edge.
static const Alignment bottomCenter = Alignment(0.0.1.0);

/// The bottom right corner.
static const Alignment bottomRight = Alignment(1.0.1.0);
Copy the code

We can see that the Alignment defines values in the range -1.0 to 1.0. So are the only points that Alignment can define? And it isn’t. The following integration of the points will make it easier to understand the nature of Alignment:

Alignment is defined as 0,0, based on the middle of the Rect. A coordinate system can be established based on this. The horizontal value is -1.0~1.0, which is actually a percentage. The percentage is calculated according to the width of Rect, so as to determine the horizontal position. For example, define a dot Alignment(0.5,0,0) so that the point is offset to the right by 1/4 of the Rect width based on the center point.

3. Implement an Align yourself

With all that said, it is better to write an Align by hand to deepen your understanding of the layout of the Flutter. Here’s a simpler implementation.

class CustomAlign extends SingleChildRenderObjectWidget {
  final Alignment alignment;

  const CustomAlign({Key key, Widget child, this.alignment = Alignment.topLeft})
      : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    // TODO: implement createRenderObject
    throwUnimplementedError(); }}Copy the code

First define a class CustomAlign, he inherited from SingleChildRenderObjectWidget, because need based on the Alignment positioning, so here to set the parameters to use. Next we need to implement an AlignRenderBox ourselves to achieve this effect.

class AlignRenderBox extends RenderBox
    with RenderObjectWithChildMixin<RenderBox> {
  Alignment alignment;

  AlignRenderBox({RenderBox child, this.alignment}) {
    this.child = child;
  }

  @override
  void performLayout() {
    ///super.performLayout(); Just be careful not to call super.performLayout()
    if (child == null) {
      size = Size.zero;
      ///No child occupies the control
    } else {
      size = constraints.constrain(Size.infinite); // Fill as much as possible
      child.layout(constraints.loosen(), parentUsesSize: true); // Do not limit the size of the child
      BoxParentData parentData = child.parentData as BoxParentData;
      parentData.offset = alignment.alongOffset(size - child.size as Offset); // Set the offset}}@override
  void paint(PaintingContext context, Offset offset) {
    super.paint(context, offset);
    if(child ! =null) {
      // Draw child and be child if child is not empty
      BoxParentData parentData = child.parentData asBoxParentData; context.paintChild(child, offset + parentData.offset); }}}Copy the code

The implementation of the AlignRenderBox is also simple. In performLayout(), we first determine if there are any children. If there are no children, the RenderObject is set to 0 width and height. Fetch the child’s BoxParentData, specifying the offset coordinates caused by Alignment. Finally, draw the child in the paint method, specifying an offset for it. Finally, provide this AlignRenderBox to CustomAlign for use:

class CustomAlign extends SingleChildRenderObjectWidget {
  final Alignment alignment;

  const CustomAlign({Key key, Widget child, this.alignment = Alignment.topLeft})
      : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return AlignRenderBox(alignment: alignment);
  }

  @override
  void updateRenderObject(
      BuildContext context, covariantAlignRenderBox renderObject) { renderObject.alignment = alignment; }}Copy the code

Note that we also need to override updateRenderObject to update the AlignRenderBox.

Above, a simple Align is done, start testing.

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page')); }}class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  void _incrementCounter() {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SizedBox(
        child: CustomAlign(
          alignment: Alignment.center,
          child: Text('File transfer'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.); }}Copy the code

The running effect is as follows:

1. Principles of Flutter: Three Important Trees (Rendering process, layout constraints, construction of application views, etc.)