Flutter 的渲染逻辑及和 Native 通讯

本文首发于 RTC 开发者社区,做者刘斯龙, 5年的 Android 程序员,从事过 AR ,Unity3D,Weex,Cordova,Flutter 及小程序开发html

做者 github: github.com/liusilongjava

做者 blog:liusilong.github.io/android

做者 StackOverflow:stackoverflow.com/users/47233…git

做者掘金博客:juejin.im/user/58eb94…程序员

在这篇文章中,咱们主要了解两个部分的内容,一个是 Flutter 的基本渲染逻辑 另外一个是 Flutter 和 Native 互通的方法,这里的 Native 是以 Android 为例。而后使用案例分别进行演示。github

Flutter 渲染

Android 中,咱们所说的 View 的渲染逻辑指的是 onMeasure(), onLayout(), onDraw(), 咱们只要重写这三个方法就能够自定义出符合咱们需求的 View。其实,即便咱们不懂 Android 中 View 的渲染逻辑,也能写出大部分的 App,可是当系统提供的 View 知足不了咱们的需求的时候,这时就须要咱们自定义 View 了,而自定义 View 的前提就是要知道 View 的渲染逻辑。canvas

Flutter 中也同样,系统提供的 Widget 能够知足咱们大部分的需求,可是在一些状况下咱们仍是得渲染本身的 Widget。小程序

和 Android 相似,Flutter 中的渲染也会经历几个必要的阶段,以下:数组

  • Layout : 布局阶段,Flutter 会肯定每个子 Widget 的大小和他们在屏幕中将要被放置的位置。
  • Paint : 绘制阶段,Flutter 为每一个子 Widget 提供一个 canvas,并让他们绘制本身。
  • Composite : 组合阶段,Flutter 会将全部的 Widget 组合在一块儿,并交由 GPU 处理。

上面三个阶段中,比较重要的就是 Layout 阶段了,由于一切都始于布局。markdown

在 Flutter 中,布局阶段会作两个事情:父控件将 约束(Constraints) 向下传递到子控件;子控件将本身的 布局详情(Layout Details) 向上传递给父控件。以下图:

布局过程以下:

这里咱们将父 widget 称为 parent;将子 widget 称为 child

  1. parent 会将某些布局约束传递给 child,这些约束是每一个 child 在 layout 阶段必需要遵照的。如同 parent 这样告诉 child :“只要你遵照这些规则,你能够作任何你想作的事”。最多见的就是 parent 会限制 child 的大小,也就是 child 的 maxWidth 或者 maxHeight。

  2. 而后 child 会根据获得的约束生成一个新的约束,并将这个新的约束传递给本身的 child(也就是 child 的 child),这个过程会一直持续到出现没有 child 的 widget 为止。

  3. 以后,child 会根据 parent 传递过来的约束肯定本身的布局详情(Layout Details)。如:假设 parent 传递给 child 的最大宽度约束为 500px,child 可能会说:“好吧,那我就用500px”,或者 “我只会用 100px”。这样,child 就肯定了本身的布局详情,并将其传递给 parent。

  4. parent 反过来作一样的事情,它根据 child 传递回来的 Layout Details 来肯定其自身的 Layout Details,而后将这些 Layout Details 向上层的 parent 传递,直到到达 root widget (根 widget)或者遇到了某些限制。

那咱们上面所提到的 约束(Constraints)布局详情(Layout Details) 都是什么呢?这取决于布局协议(Layout protocol)。Flutter 中有两种主要的布局协议:Box ProtocolSliver Protocol,前者能够理解为相似于盒子模型协议,后者则是和滑动布局相关的协议。这里咱们之前者为例。

Box Protocol 中,parent 传递给 child 的约束都叫作 BoxConstraints 这些约束决定了每一个 child 的 maxWidth 和 maxHeight 以及 minWidth 和 minHeight。如:parent 可能会将以下的 BoxConstraints 传递给 child。

上图中,浅绿色的为 parent,浅红色的小矩形为 child。 那么,parent 传递给 child 的约束就是 150 ≤ width ≤ 300, 100 ≤ height ≤ 无限大 而 child 回传给 parent 的布局详情就是 child 的尺寸(Size)。

有了 child 的 Layout Details ,parent 就能够绘制它们了。

在咱们渲染本身的 widget 以前,先来了解下另一个东西 Render Tree

Render Tree

咱们在 Android 中会有 View tree,Flutter 中与之对应的为 Widget tree,可是 Flutter 中还有另一种 tree,称为 Render tree

Flutter 中 咱们常见的 widgetStatefulWidgetStatelessWidgetInheritedWidget 等等。可是这里还有另一种 widget 称为 RenderObjectWidget,这个 widget 中没有 build() 方法,而是有一个 createRenderObject() 方法,这个方法容许建立一个 RenderObject 并将其添加到 render tree 中。

RenderObject 是渲染过程当中很是重要的组件,render tree 中的内容都是 RenderObject,每一个 RenderObject 中都有许多用来执行渲染的属性和方法:

  • constraints : 从 parent 传递过来的约束。
  • parentData: 这里面携带的是 parent 渲染 child 的时候所用到的数据。
  • performLayout():此方法用于布局全部的 child。
  • paint():这个方法用于绘制本身或者 child。
  • 等等...

可是,RenderObject 是一个抽象类,他须要被子类继承来进行实际的渲染。RenderObject 的两个很是重要的子类是 RenderBoxRenderSliver 。这两个类是全部实现 Box ProtocolSliver Protocol 的渲染对象的父类。并且这两个类还扩展了数十个和其余几个处理特定场景的类,而且实现了渲染过程的细节。

如今咱们开始渲染本身的 widget,也就是建立一个 RenderObject。这个 widget 须要知足下面两点要求:

  • 它只会给 child 最小的宽和高
  • 它会把它的 child 放在本身的右下角

如此 “小气” 的 widget ,咱们就叫他 Stingy 吧!Stingy 所属的树形结构以下:

MaterialApp
  |_Scaffold
	|_Container  	  // Stingy 的 parent
	  |_Stingy  	  // 自定义的 RenderObject
	    |_Container   // Stingy 的 child
复制代码

代码以下:

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,
          ),
        ),
      ),
    ),
  ));
}
复制代码

Stingy

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

  @override
  RenderObject createRenderObject(BuildContext context) {
    // TODO: implement createRenderObject
    return RenderStingy();
  }
}
复制代码

Stingy 继承了 SingleChildRenderObjectWidget,顾名思义,他只能有一个 childcreateRenderObject(...) 方法建立并返回了一个 RenderObjectRenderStingy 类的实例

RenderStingy

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

  // 绘制方法
  @override
  void paint(PaintingContext context, Offset offset) {
    // TODO: implement paint
    super.paint(context, offset);
  }

  // 布局方法
  @override
  void performLayout() {
    // 布局 child 肯定 child 的 size
    child.layout(
        BoxConstraints(
            minHeight: 0.0,
            maxHeight: constraints.minHeight,
            minWidth: 0.0,
            maxWidth: constraints.minWidth),
        parentUsesSize: true);

    print('constraints: $constraints');


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

    // 肯定本身(Stingy)的大小 相似于 Android View 的 setMeasuredDimension(...)
    size = Size(constraints.maxWidth, constraints.maxHeight);
    print('size: $size');
  }
}
复制代码

RenderStingy 继承自 RenderShiftedBox,该类是继承自 RenderBoxRenderShiftedBox 实现了 Box Protocol 全部的细节,而且提供了 performLayout() 方法的实现。咱们须要在 performLayout() 方法中布局咱们的 child,还能够设置他们的偏移量。

咱们在使用 child.layout(...) 方法布局 child 的时候传递了两个参数,第一个为 child 的布局约束,而另一个参数是 parentUserSize, 该参数若是设置为 false,则意味着 parent 不关心 child 选择的大小,这对布局优化比较有用;由于若是 child 改变了本身的大小,parent 就没必要从新 layout 了。可是在咱们的例子中,咱们的须要把 child 放置在 parent 的右下角,这意味着若是 child大小(Size)一旦改变,则其对应的偏移量(Offset) 也会改变,这就意味着 parent 须要从新布局,因此咱们这里传递了一个 true

child.layout(...) 完成了之后,child 就肯定了本身的 Layout Details。而后咱们就还能够为其设置偏移量来将它放置到咱们想放的位置。在咱们的例子中为 右下角

最后,和 child 根据 parent 传递过来的约束选择了一个尺寸同样,咱们也须要为 Stingy 选择一个尺寸,以致于 Stingyparent 知道如何放置它。相似于在 Android 中咱们自定义 View 重写 onMeasure(...) 方法的时候须要调用 setMeasuredDimension(...) 同样。

运行效果以下:

绿色部分为咱们定义的 Stingy,红色小方块为 Stingy 的 child ,这里是一个 Container

代码中的输入以下 (iphone 6 尺寸):

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)
复制代码

上述咱们自定义 RenderBoxperformLayout() 中作的事情可大概分为以下三个步骤:

  • 使用 child.layout(...) 来布局 child,这里是为 child 根据 parent 传递过来的约束选择一个大小
  • child.parentData.offset , 这是在为 child 如何摆放设置一个偏移量
  • 设置当前 widgetsize

在咱们的例子中,Stingychild 是一个 Container,而且 Container 没有 child,所以他会使用 child.layout(...) 中设置的最大约束。一般,每一个 widget 都会以不一样的方式来处理提供给他的约束。若是咱们使用 RaiseButton 替换 Container

Stingy(  
  child: RaisedButton(  
    child: Text('Button'),
    onPressed: (){}
  )  
)
复制代码

效果以下:

能够看到,RaisedButtonwidth 使用了 parent 给他传递的约束值 100,可是高度很明显没有 100,RaisedButton 的高度默认为 48 ,因而可知 RaisedButton 内部对 parent 传递过来的约束作了一些处理。

咱们上面的 Stingy 继承的是 SingleChildRenderObjectWidget,也就是只能有一个 child。那若是有多个 child 怎么办,不用担忧,这里还有一个 MultiChildRenderObjectWidget,而这个类有一个子类叫作 CustomMultiChildLayout,咱们直接用这个子类就好。

先来看看 CustomMultiChildLayout 的构造方法以下:

/// The [delegate] argument must not be null.
CustomMultiChildLayout({
  Key key,
  @required this.delegate,
  List<Widget> children = const <Widget>[],
})
复制代码
  • key:widget 的一个标记,能够起到标识符的做用
  • delegate:这个特别重要,注释上明确指出这个参数必定不能为空,咱们在下会说
  • children:这个就很好理解了,他是一个 widget 数组,也就是咱们们须要渲染的 widget

上面的 delegate 参数类型以下:

/// The delegate that controls the layout of the children.
  final MultiChildLayoutDelegate delegate;
复制代码

能够看出 delegate 的类型为 MultiChildLayoutDelegate,而且注释也说明了它的做用:控制 children 的布局。也就是说,咱们的 CustomMultiChildLayout 里面要怎么布局,彻底取决于咱们自定义的 MultiChildLayoutDelegate 里面的实现。因此 MultiChildLayoutDelegate 中也会有相似的 performLayout(..) 方法。

另外,CustomMultiChildLayout 中的每一个 child 必须使用 LayoutId 包裹,注释以下:

/// Each child must be wrapped in a [LayoutId] widget to identify the widget for 
/// the delegate.
复制代码

LayoutId 的构造方法以下:

/// 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
  })
复制代码

注释的大概意思说的是:使用一个布局标识来标识一个 child;参数 child 和 参数 id 不定不能为空。 咱们在布局 child 的时候会根据 childid 来布局。

下面咱们来使用 CustomMultiChildLayout 实现一个用于展现热门标签的效果:

Container(
   child: CustomMultiChildLayout(
     delegate: _LabelDelegate(itemCount: items.length, childId: childId),
     children: items,
   ),
 )
复制代码

咱们的 _LabelDelegate 里面接受两个参数,一个为 itemCount,还有是 childId

_LabelDelegate 代码以下:

class _LabelDelegate extends MultiChildLayoutDelegate {

  final int itemCount;
  final String childId;

  // x 方向上的偏移量
  double dx = 0.0;
  // y 方向上的偏移量
  double dy = 0.0;

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

  @override
  void performLayout(Size size) {
    // 获取父控件的 width
    double parentWidth = size.width;

    for (int i = 0; i < itemCount; i++) {
      // 获取子控件的 id
      String id = '${this.childId}$i';
      // 验证该 childId 是否对应一个 非空的 child
      if (hasChild(id)) {
        // layout child 并获取该 child 的 size
        Size childSize = layoutChild(id, BoxConstraints.loose(size));

        // 换行条件判断
        if (parentWidth - dx < childSize.width) {
          dx = 0;
          dy += childSize.height;
        }
        // 根据 Offset 来放置 child
        positionChild(id, Offset(dx, dy));
        dx += childSize.width;
      }
    }
  }

  /// 该方法用来判断从新 layout 的条件
  @override
  bool shouldRelayout(_LabelDelegate oldDelegate) {
    return oldDelegate.itemCount != this.itemCount;
  }
}
复制代码

_LabelDelegate 中,重写了 performLayout(...) 方法。方法中有一个参数 size,这个 size 表示的是当前 widgetparentsize,在咱们这个例子中也就表示 Containersize。咱们能够看看 performLayout(...)方法的注释:

/// 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);
复制代码

还有一个是 hasChild(...) 方法,这个方法接受一个 childIdchildId 是由咱们本身规定的,这个方法的做用是判断当前的 childId 是否对应着一个非空的 child

知足 hasChild(...) 以后,接着就是 layoutChild(...) 来布局 child , 这个方法中咱们会传递两个参数,一个是 childId,另一个是 child约束(Constraints),这个方法返回的是当前这个 childSize

布局完成以后,就是如何摆放的问题了,也就是上述代码中的 positionChild(..) 了,此方法接受一个 childId 和 一个当前 child 对应的 Offsetparent 会根据这个 Offset 来放置当前的 child

最后咱们重写了 shouldRelayout(...) 方法用于判断从新 Layout 的条件。

完整源码在文章末尾给出。

效果以下:

Flutter 和 Native 的交互

咱们这里说的 Native 指的是 Android 平台。

那既然要相互通讯,就须要将 Flutter 集成到 Android 工程中来,不清楚的如何集成能够看看这里

这里有一点须要注意,就是咱们在 Android 代码中须要初始化 Dart VM,否则咱们在使用 getFlutterView() 来获取一个 Flutter View 的时候会抛出以下异常:

Caused by: java.lang.IllegalStateException: ensureInitializationComplete must be called after startInitialization
        at io.flutter.view.FlutterMain.ensureInitializationComplete(FlutterMain.java:178)
...
复制代码

咱们有两种方式来执行初始化操做:一个是直接让咱们的 Application 继承 FlutterApplication,另一个是须要咱们在咱们本身的 Application 中手动初始化:

方法一:

public class App extends FlutterApplication {  
  
}
复制代码

方法二:

public class App extends Application {  
  @Override  
  public void onCreate() {  
  super.onCreate();  
  // 初始化 Flutter
  Flutter.startInitialization(this);  
  }  
}
复制代码

其实方法一中的 FlutterApplication 中在其 onCreate() 方法中干了一样的事情,部分代码以下:

public class FlutterApplication extends Application {

	...
	
    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }
    
    ...
}
复制代码

若是咱们的 App 只是须要使用 Flutter 在屏幕上绘制 UI,那么没问题, Flutter 框架可以独立完成这些事情。可是在实际的开发中,不免会须要调用 Native 的功能,如:定位,相机,电池等等。这个时候就须要 Flutter 和 Native 通讯了。

官网上有一个案例 是使用 MethodChannel来调用给本地的方法获取手机电量。

其实咱们还可使用另一个类进行通讯,叫作 BasicMessageChannel,先来看看它若是建立:

// java
basicMessageChannel = new BasicMessageChannel<String>(getFlutterView(), "foo", StringCodec.INSTANCE);
复制代码

BasicMessageChannel 须要三个参数,第一个是 BinaryMessenger;第二个是通道名称,第三个是交互数据类型的编解码器,咱们接下来的例子中的交互数据类型为 String ,因此这里传递的是 StringCodec.INSTANCE,Flutter 中还有其余类型的编解码器BinaryCodecJSONMessageCodec等,他们都有一个共同的父类 MessageCodec。 因此咱们也能够根据规则建立本身编解码器。

接下来建立的例子是:FlutterAndroid 发送一条消息,Android 收到消息以后给 Flutter 回复一条消息,反之亦然。

先来看看 Android 端的部分代码:

// 接收 Flutter 发送的消息
basicMessageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<String>() {
    @Override
    public void onMessage(final String s, final BasicMessageChannel.Reply<String> reply) {

        // 接收到的消息
        linearMessageContainer.addView(buildMessage(s, true));
        scrollToBottom();

        // 延迟 500ms 回复
        flutterContainer.postDelayed(new Runnable() {
            @Override
            public void run() {
                // 回复 Flutter
                String replyMsg = "Android : " + new Random().nextInt(100);
                linearMessageContainer.addView(buildMessage(replyMsg, false));
                scrollToBottom();
                // 回复
                reply.reply(replyMsg);
            }
        }, 500);

    }
});

 // ----------------------------------------------
 
 // 向 Flutter 发送消息
 basicMessageChannel.send(message, new BasicMessageChannel.Reply<String>() {
     @Override
     public void reply(final String s) {
         linearMessageContainer.postDelayed(new Runnable() {
             @Override
             public void run() {
                 // Flutter 的回复
                 linearMessageContainer.addView(buildMessage(s, true));
                 scrollToBottom();
             }
         }, 500);

     }
 });
复制代码

相似的,Flutter 这边的部分代码以下:

// 消息通道
  static const BasicMessageChannel<String> channel =
      BasicMessageChannel<String>('foo', StringCodec());

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

 // 接收 Android 发送过来的消息,而且回复
 channel.setMessageHandler((String message) async {
   String replyMessage = 'Flutter: ${Random().nextInt(100)}';
   setState(() {
     // 收到的android 端的消息
     _messageWidgets.add(_buildMessageWidget(message, true));
     _scrollToBottom();
   });

   Future.delayed(const Duration(milliseconds: 500), () {
     setState(() {
       // 回复给 android 端的消息
       _messageWidgets.add(_buildMessageWidget(replyMessage, false));
       _scrollToBottom();
     });
   });

   // 回复
   return replyMessage;
 });
 
 // ----------------------------------------------
 
 // 向 Android 发送消息
 void _sendMessageToAndroid(String message) {
   setState(() {
     _messageWidgets.add(_buildMessageWidget(message, false));
     _scrollToBottom();
   });
   // 向 Android 端发送发送消息并处理 Android 端给的回复
   channel.send(message).then((value) {
     setState(() {
       _messageWidgets.add(_buildMessageWidget(value, true));
       _scrollToBottom();
     });
   });
 }
复制代码

最后的效果以下:

屏幕的上半部分为 Android,下半部分为 Flutter

源码地址: flutter_rendering flutter_android_communicate

参考:

Flutter’s Rendering Engine: A Tutorial — Part 1

Flutter's Rendering Pipeline

相关阅读

构建你的第一个 Flutter 视频通话应用


推广:欢迎进入 Github 体验 Agora Flutter SDK,一个帮助 Flutter 应用实现实时音视频功能的 plugin。

相关文章
相关标签/搜索