This article is published in RTC developer community by Silong Liu, an Android programmer for 5 years. He has worked on AR, Unity3D, Weex, Cordova, Flutter and small applications

Author: github github.com/liusilong

Author’s blog: Liusilong.github. IO /

The author StackOverflow:stackoverflow.com/users/47233…

Author nuggets blog: juejin.cn/user/694547…

In this article, we mainly understand two parts, one is the basic rendering logic of Flutter and the other is the interworking method between Flutter and Native. Here, Native takes Android as an example. Then use cases to demonstrate each.

Flutter rendering

In Android, the rendering logic of View refers to onMeasure(), onLayout() and onDraw(). We can customize the View by rewriting these three methods. In fact, even if we do not understand the View rendering logic in Android, we can write most of the App, but when the View provided by the system can not meet our needs, then we need to customize the View, and the premise of the custom View is to know the View rendering logic.

Flutter also provides widgets that meet most of our needs, but in some cases we have to render our own widgets.

Similar to Android, Rendering in Flutter goes through several necessary stages, as follows:

  • Layout: In the Layout phase, a Flutter determines the size of each Widget and the position that it will be placed on the screen.
  • Paint: In the Paint phase, Flutter provides each child Widget with a canvas and lets them draw themselves.
  • Composite: In the Composite phase, Flutter combines all the widgets together and delivers them to the GPU for processing.

The most important of the three stages is the Layout stage, because everything starts with the Layout.

In a Flutter, the layout phase does two things: the parent controls pass Constraints down to the child controls; The child control passes its Layout Details up to the parent control. The diagram below:

The layout process is as follows:

Here we call the parent widget parent; Call the child widget child

  1. Parent passes to the child certain layout constraints that each child must follow during the Layout phase. As Parent tells Child, “As long as you follow these rules, you can do whatever you want.” The most common is that the parent limits the size of the child, i.e., its maxWidth or maxHeight.

  2. The child then generates a new constraint based on the resulting constraint and passes the new constraint to its child (that is, child’s child) until there is a widget without a child.

  3. The child then determines its own Layout Details based on the constraints passed by the parent. For example, if the parent passes a maximum width constraint of 500px to the child, the child might say, “OK, I’ll use 500px”, or “I’ll only use 100px”. In this way, the child determines its layout details and passes them to the parent.

  4. The parent does the same in reverse. It determines its own Layout Details from the Layout Details passed back by the child, and then passes those Layout Details to the parent. Until the root widget is reached or some restriction is encountered.

What are the Constraints and Layout Details we mentioned above? This depends on the Layout protocol. Flutter has two main layout protocols: the Box Protocol, which can be understood as similar to the Box model Protocol, and the Sliver Protocol, which is related to a sliding layout. Let’s take the former as an example.

In Box Protocol, the constraints that a parent passes to a child are called BoxConstraints. These constraints determine the maxWidth and maxHeight and minWidth and minHeight of each child. For example, the parent might pass the following BoxConstraints to the child.

In the figure above, the light green rectangle is Parent and the light red rectangle is Child. Then, the constraints passed from parent to child are 150 ≤ width ≤ 300, 100 ≤ height ≤ infinity, and the layout details returned from child to parent are the Size of the child.

With the Child’s Layout Details, the parent can draw them.

Before we Render our widget, let’s take a look at another thing called Render Tree.

Render Tree

We have View Tree in Android and Widget Tree in Flutter, but there is another tree called Render Tree in Flutter.

The common widgets in Flutter include StatefulWidget, StatelessWidget, InheritedWidget, and so on. But there is another widget called RenderObjectWidget that has a createRenderObject() method instead of a build() method, This method allows you to create a RenderObject and add it to the Render Tree.

RenderObject is a very important component in the rendering process. The contents of the Render Tree are all renderObjects, and each RenderObject has a number of properties and methods used to perform rendering:

  • Constraints: Constraints passed from parent.
  • ParentData: This contains the data used by the parent to render the Child.
  • PerformLayout () : This method is used to lay out all children.
  • Paint () : This method is used to draw itself or child.
  • And so on…

However, RenderObject is an abstract class that needs to be inherited by subclasses to do the actual rendering. Two very important subclasses of RenderObject are RenderBox and RenderSliver. These two classes are the parent classes of all render objects that implement Box Protocol and Sliver Protocol. Moreover, these two classes extend dozens and several others that deal with specific scenes and implement the details of the rendering process.

Now we start rendering our widget, creating a RenderObject. The widget needs to meet the following two requirements:

  • It only gives the child the minimum width and height
  • It puts its child in its lower right corner

Such Stingy widgets are what we call Stingy. Stingy belongs to the following tree structure:

MaterialApp
  |_Scaffold
	|_Container  	  The parent / / Stingy
	  |_Stingy  	  // Create a custom RenderObject
	    |_Container   / / Stingy child
Copy the code

The code is as follows:

void main() {
  runApp(MaterialApp(
    home: Scaffold(
      body: Container(
        color: Colors.greenAccent,
        constraints: BoxConstraints(
            maxWidth: double.infinity,
            minWidth: 100.0,
            maxHeight: 300,
            minHeight: 100.0),
        child: Stingy(
          child: Container(
            color: Colors.red,
          ),
        ),
      ),
    ),
  ));
}
Copy the code

Stingy

class Stingy extends SingleChildRenderObjectWidget {
  Stingy({Widget child}) : super(child: child);

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

Stingy inherited SingleChildRenderObjectWidget, as the name suggests, he can only have one child and createRenderObject (…). The RenderStingy () method creates and returns an instance of the RenderObject class

RenderStingy

class RenderStingy extends RenderShiftedBox {
  RenderStingy() : super(null);

  // Draw method
  @override
  void paint(PaintingContext context, Offset offset) {
    // TODO: implement paint
    super.paint(context, offset);
  }

  // The layout method
  @override
  void performLayout() {
    // set the size of the child
    child.layout(
        BoxConstraints(
            minHeight: 0.0,
            maxHeight: constraints.minHeight,
            minWidth: 0.0,
            maxWidth: constraints.minWidth),
        parentUsesSize: true);

    print('constraints: $constraints');


    / / the child Offset
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = Offset(constraints.maxWidth - child.size.width,
        constraints.maxHeight - child.size.height);
    print('childParentData: $childParentData');

    Stingy size is similar to the Android View setMeasuredDimension(...).
    size = Size(constraints.maxWidth, constraints.maxHeight);
    print('size: $size'); }}Copy the code

RenderStingy inherits from RenderShiftedBox, which inherits from RenderBox. RenderShiftedBox implements all the details of the Box Protocol and provides an implementation of the performLayout() method. We need to lay out our children in the performLayout() method and also set their offsets.

We are using child.layout(…) The parentUserSize method passes two arguments to the child layout constraint. If this parameter is set to false, it means that the parent does not care about the size selected by the child, which is useful for layout optimization. Because if the child changes its size, the parent does not have to rearrange. But in our example, we need to place the child in the lower right corner of the parent, which means that if the Size of the child changes, its Offset will also change, which means that the parent needs to be rearranged. So we passed a true here.

When the child layout (…). Once done, the Child determines its own Layout Details. And then we can also set an offset to put it where we want to put it. In our case, the lower right corner.

Finally, just as the child chooses a size based on the constraints passed by the parent, we need to choose a size for the Stingy so that the parent of the Stingy knows how to place it. Similar to the custom View override onMeasure(…) in Android SetMeasuredDimension (…) The same.

The running effect is as follows:

The green is the Stingy that we define, and the little red square is the child of the Stingy, so this is a Container

The input in the code is as follows (iPhone 6 size) :

flutter: constraints: BoxConstraints(100.0<=w<=375.0.100.0<=h<=300.0)
flutter: childParentData: offset=Offset(275.0.200.0)
flutter: size: Size(375.0.300.0)
Copy the code

What we did in performLayout() for our custom RenderBox can be roughly divided into three steps:

  • usechild.layout(...)To layoutchildHere is theta for thetachildAccording to theparentThe constraint passed in selects a size
  • child.parentData.offset, this is forchildHow to set an offset
  • Set the currentwidgetsize

In our example, Stingy’s child is a Container, and the Container has no child, so it uses child.layout(…). Maximum constraint set in. Typically, each widget handles the constraints provided to it in a different way. If we replace Container with RaiseButton:

Stingy(  
  child: RaisedButton(  
    child: Text('Button'),
    onPressed: (){}
  )  
)
Copy the code

The effect is as follows:

As you can see, the width of RaisedButton uses the constraint 100 passed by parent, but the height is obviously not 100. RaisedButton’s height defaults to 48. This shows that RaisedButton does some internal work on the constraints passed by parent.

We are Stingy on inherited SingleChildRenderObjectWidget, is the only one child. The child what to do if there are multiple, don’t worry, there is also a MultiChildRenderObjectWidget, and this class has a subclass called CustomMultiChildLayout, we directly use the subclasses.

The CustomMultiChildLayout constructor is as follows:

/// The [delegate] argument must not be null.
CustomMultiChildLayout({
  Key key,
  @required this.delegate,
  List<Widget> children = const <Widget>[],
})
Copy the code
  • Key:widgetIs a tag that acts as an identifier
  • Delegate: This one is particularly important. The comment explicitly states that this parameter must not be empty, as we’ll get to next
  • Children: That’s easy to understand. He’s awidgetArrays, that’s what we need to renderwidget

The delegate parameter above has the following type:

  /// The delegate that controls the layout of the children.
  final MultiChildLayoutDelegate delegate;
Copy the code

As you can see, the delegate type is MultiChildLayoutDelegate, and the annotation explains what it does: control the layout of children. In other words, the layout of our CustomMultiChildLayout depends entirely on the implementation of our custom MultiChildLayoutDelegate. So MultiChildLayoutDelegate will have a similar performLayout(..) Methods.

In addition, each child in CustomMultiChildLayout must be wrapped with LayoutId, commented as follows:

/// Each child must be wrapped in a [LayoutId] widget to identify the widget for  
/// the delegate.
Copy the code

LayoutId is constructed as follows:

  /// Marks a child with a layout identifier.
  /// Both the child and the id arguments must not be null.
  LayoutId({
    Key key,
    @required this.id,
    @required Widget child
  })
Copy the code

Use a layout identifier to identify a child; The child and id arguments may not be null. We layout the child based on the id of the child.

Let’s use CustomMultiChildLayout to create an effect for displaying popular tags:

Container(
   child: CustomMultiChildLayout(
     delegate: _LabelDelegate(itemCount: items.length, childId: childId),
     children: items,
   ),
 )
Copy the code

Our _LabelDelegate accepts two parameters, itemCount and childId.

The _LabelDelegate code looks like this:

class _LabelDelegate extends MultiChildLayoutDelegate {

  final int itemCount;
  final String childId;

  // Offset in the x direction
  double dx = 0.0;
  // Offset in the y direction
  double dy = 0.0;

  _LabelDelegate({@required this.itemCount, @required this.childId});

  @override
  void performLayout(Size size) {
    // Get width of the parent control
    double parentWidth = size.width;

    for (int i = 0; i < itemCount; i++) {
      // Get the id of the child control
      String id = 'The ${this.childId}$i';
      // Verify that the childId corresponds to a non-empty child
      if (hasChild(id)) {
        // Layout Child and get the size of the child
        Size childSize = layoutChild(id, BoxConstraints.loose(size));

        // Line break condition judgment
        if (parentWidth - dx < childSize.width) {
          dx = 0;
          dy += childSize.height;
        }
        // Place the child according to OffsetpositionChild(id, Offset(dx, dy)); dx += childSize.width; }}}/// This method is used to determine the condition of the layout relayout
  @override
  bool shouldRelayout(_LabelDelegate oldDelegate) {
    returnoldDelegate.itemCount ! =this.itemCount; }}Copy the code

In _LabelDelegate, rewrite performLayout(…) Methods. The size method takes one parameter, size, which represents the size of the parent of the current widget or, in our case, the Container. We can look at performLayout(…) Method comment:

  /// Override this method to lay out and position all children given this
  /// widget's size.
  ///
  /// This method must call [layoutChild] for each child. It should also specify
  /// the final position of each child with [positionChild].
  void performLayout(Size size);
Copy the code

Another is hasChild(…) Method, which accepts a childId specified by ourselves. The function of this method is to determine whether the current childId corresponds to a non-empty child.

Meet hasChild (…). After that, layoutChild(…) In this method, we pass two parameters, childId and Constraints. This method returns the Size of the current child.

Once the layout is complete, it’s a matter of placement, which is the positionChild(..) in the code above. This method takes a childId and an Offset corresponding to the current child, and the parent places the current child at that Offset.

Finally we rewrite shouldRelayout(…) The relayout method is used to determine conditions for a relayout.

The full source code is given at the end of the article.

The effect is as follows:

Interaction of Flutter and Native

And by Native, we mean the Android platform.

To communicate with each other, Flutter needs to be integrated into the Android project. If it is not clear how to integrate Flutter, see here

One thing to note here is that we need to initialize the Dart VM in our Android code, otherwise we will throw the following exception when we use getFlutterView() to get a FlutterView:

Caused by: java.lang.IllegalStateException: ensureInitializationComplete must be called after startInitialization
        at io.flutter.view.FlutterMain.ensureInitializationComplete(FlutterMain.java:178)...Copy the code

There are two ways to initialize the FlutterApplication: one is to have our Application inherit the FlutterApplication directly, and the other is to initialize it manually in our own Application:

Method one:

public class App extends FlutterApplication {}Copy the code

Method 2:

public class App extends Application {  
  @Override  
  public void onCreate(a) {  
  super.onCreate();  
  // Initialize the Flutter
  Flutter.startInitialization(this); }}Copy the code

FlutterApplication in method 1 does the same thing in its onCreate() method:

public class FlutterApplication extends Application {...@CallSuper
    public void onCreate(a) {
        super.onCreate();
        FlutterMain.startInitialization(this); }... }Copy the code

If our App just needs to use Flutter to draw the UI on the screen, then no problem, the Flutter framework can do this independently. However, in actual development, it is inevitable to call Native functions, such as positioning, camera, battery and so on. This is when the Flutter needs to communicate with the Native.

The official website has an example of using MethodChannel to call a local method to get your phone’s battery.

There is another class we can use to communicate, called BasicMessageChannel. Let’s see how it creates:

// java
basicMessageChannel = new BasicMessageChannel<String>(getFlutterView(), "foo", StringCodec.INSTANCE);
Copy the code

BasicMessageChannel takes three arguments, the first being BinaryMessenger; The second one is the channel name, and the third one is the codec of the interaction data type. In our next example the interaction data type is String, so here we pass StringCodec.INSTANCE. JSONMessageCodec, etc., they all have a common parent class MessageCodec. So we can also create our own codecs based on the rules.

The following example is created: Flutter sends a message to Android, which responds to Flutter with a message, and vice versa.

Let’s take a look at some of the Android code:

Receive messages sent by Flutter
basicMessageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<String>() {
    @Override
    public void onMessage(final String s, final BasicMessageChannel.Reply<String> reply) {

        // The received message
        linearMessageContainer.addView(buildMessage(s, true));
        scrollToBottom();

        // Delay 500ms reply
        flutterContainer.postDelayed(new Runnable() {
            @Override
            public void run(a) {
                / / reply to Flutter
                String replyMsg = "Android : " + new Random().nextInt(100);
                linearMessageContainer.addView(buildMessage(replyMsg, false));
                scrollToBottom();
                / / replyreply.reply(replyMsg); }},500); }});// ----------------------------------------------
 
 // Send messages to Flutter
 basicMessageChannel.send(message, new BasicMessageChannel.Reply<String>() {
     @Override
     public void reply(final String s) {
         linearMessageContainer.postDelayed(new Runnable() {
             @Override
             public void run(a) {
                 // Response of Flutter
                 linearMessageContainer.addView(buildMessage(s, true)); scrollToBottom(); }},500); }});Copy the code

Similarly, part of the code for the Flutter side is as follows:

  // Message channel
  static const BasicMessageChannel<String> channel =
      BasicMessageChannel<String> ('foo', StringCodec());

 // ----------------------------------------------

 // Receive a message from Android and reply
 channel.setMessageHandler((String message) async {
   String replyMessage = 'Flutter: ${Random().nextInt(100)}';
   setState(() {
     // Received android message
     _messageWidgets.add(_buildMessageWidget(message, true));
     _scrollToBottom();
   });

   Future.delayed(const Duration(milliseconds: 500), () {
     setState(() {
       // Reply to the Android message
       _messageWidgets.add(_buildMessageWidget(replyMessage, false));
       _scrollToBottom();
     });
   });

   / / reply
   return replyMessage;
 });
 
 // ----------------------------------------------
 
 // Send a message to Android
 void _sendMessageToAndroid(String message) {
   setState(() {
     _messageWidgets.add(_buildMessageWidget(message, false));
     _scrollToBottom();
   });
   // Send a message to Android and process the reply from Android
   channel.send(message).then((value) {
     setState(() {
       _messageWidgets.add(_buildMessageWidget(value, true));
       _scrollToBottom();
     });
   });
 }
Copy the code

The final effect is as follows:

The top half of the screen is Android and the bottom half is Flutter

Source address: flutter_rendering flutter_android_communicate

Reference:

Flutter’s Rendering Engine: A Tutorial — Part 1

Flutter’s Rendering Pipeline

reading

Build your first Flutter video calling app


Welcome to Github to experience the Agora Flutter SDK, a plugin that enables Flutter applications to perform real-time audio and video functions.