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

作者 github: github.com/liusilong

作者 blog:liusilong.github.io/

作者 StackOverflow:stackoverflow.com/users/47233…

作者掘金博客:juejin.im/user/58eb94…

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

Flutter 渲染

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

Flutter 中也一样,系统提供的 Widget 可以满足我们大部分的需求,但是在一些情况下我们还是得渲染自己的 Widget。

和 Android 类似,Flutter 中的渲染也会经历几个必要的阶段,如下:

  • Layout : 布局阶段,Flutter 会确定每一个子 Widget 的大小和他们在屏幕中将要被放置的位置。
  • Paint : 绘制阶段,Flutter 为每个子 Widget 提供一个 canvas,并让他们绘制自己。
  • Composite : 组合阶段,Flutter 会将所有的 Widget 组合在一起,并交由 GPU 处理。

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

在 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 的两个非常重要的子类是 RenderBox 和 RenderSliver 。这两个类是所有实现 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);@overrideRenderObject createRenderObject(BuildContext context) {// TODO: implement createRenderObjectreturn RenderStingy();}
}
复制代码

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

RenderStingy

class RenderStingy extends RenderShiftedBox {RenderStingy() : super(null);// 绘制方法@overridevoid paint(PaintingContext context, Offset offset) {// TODO: implement paintsuper.paint(context, offset);}// 布局方法@overridevoid performLayout() {// 布局 child 确定 child 的 sizechild.layout(BoxConstraints(minHeight: 0.0,maxHeight: constraints.minHeight,minWidth: 0.0,maxWidth: constraints.minWidth),parentUsesSize: true);print('constraints: $constraints');// child 的 Offsetfinal 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});@overridevoid performLayout(Size size) {// 获取父控件的 widthdouble parentWidth = size.width;for (int i = 0; i < itemCount; i++) {// 获取子控件的 idString id = '${this.childId}$i';// 验证该 childId 是否对应一个 非空的 childif (hasChild(id)) {// layout child 并获取该 child 的 sizeSize childSize = layoutChild(id, BoxConstraints.loose(size));// 换行条件判断if (parentWidth - dx < childSize.width) {dx = 0;dy += childSize.height;}// 根据 Offset 来放置 childpositionChild(id, Offset(dx, dy));dx += childSize.width;}}}/// 该方法用来判断重新 layout 的条件@overridebool 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 startInitializationat 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();  // 初始化 FlutterFlutter.startInitialization(this);  }
}
复制代码

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

public class FlutterApplication extends Application {...@CallSuperpublic 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>() {@Overridepublic void onMessage(final String s, final BasicMessageChannel.Reply<String> reply) {// 接收到的消息linearMessageContainer.addView(buildMessage(s, true));scrollToBottom();// 延迟 500ms 回复flutterContainer.postDelayed(new Runnable() {@Overridepublic void run() {// 回复 FlutterString replyMsg = "Android : " + new Random().nextInt(100);linearMessageContainer.addView(buildMessage(replyMsg, false));scrollToBottom();// 回复reply.reply(replyMsg);}}, 500);}
});// ----------------------------------------------// 向 Flutter 发送消息basicMessageChannel.send(message, new BasicMessageChannel.Reply<String>() {@Overridepublic void reply(final String s) {linearMessageContainer.postDelayed(new Runnable() {@Overridepublic 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。

Flutter 的渲染逻辑及和 Native 通信相关推荐

  1. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 完整代码示例 )

    文章目录 前言 一.Android 端完整代码示例 二.Flutter 端完整代码示例 三.相关资源 前言 前置博客 : [Flutter]Flutter 混合开发 ( Flutter 与 Nativ ...

  2. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 MethodChannel 通信 )

    文章目录 前言 一.Android 端 MethodChannel 构造函数 二.Android 端 setMethodCallHandler 方法 三.Android 端实现 MethodChann ...

  3. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 EventChannel 通信 )

    文章目录 前言 一.Android 端 EventChannel 构造函数 二.Android 端 setStreamHandler 方法 三.Android 端实现 EventChannel 通信步 ...

  4. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 BasicMessageChannel 通信 )

    文章目录 前言 一.Android 端 BasicMessageChannel 构造函数 二.Android 端 MessageCodec 子类实现 三.Android 端 setMessageHan ...

  5. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 在 Flutter 端实现 EventChannel 通信 )

    文章目录 一.EventChannel 简介 二.EventChannel 在 Dart 端的实现 1.EventChannel 构造方法 2.创建广播流 Stream 3.设置监听回调函数 4.Ev ...

  6. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 在 Flutter 端实现 MethodChannel 通信 )

    文章目录 一.MethodChannel 简介 二.MethodChannel 在 Dart 端的实现 1.MethodChannel 构造函数 2.invokeMethod 函数 3.MethodC ...

  7. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 在 Flutter 端实现 BasicMessageChannel 通信 )

    文章目录 一.BasicMessageChannel 简介 二.BasicMessageChannel 在 Dart 端的实现 1.BasicMessageChannel 构造方法 2.使用 Basi ...

  8. Flutter 画面渲染的全面解析

    本篇将通过不一样的角度来介绍 Flutter Framework 的整体渲染原理,深入剖析 Flutter 中构成 Layer 后的绘制流程,让开发者对 Flutter 的渲染原理和实现逻辑有更清晰的 ...

  9. 深入解析Flutter下一代渲染引擎Impeller

    作者 魏国梁:字节 Flutter Infra 工程师, Flutter Member,长期专注 Flutter 引擎技术 袁 欣:字节 Flutter Infra 工程师, 长期关注渲染技术发展 谢 ...

最新文章

  1. 你,保持童心;我,帮你保持童颜
  2. 项目源码分享之[条码扫描后台监控程序]
  3. 使用 Bamboo 构建项目的 CICD 过程文档
  4. 摊牌了,我靠他实现了NLP模型使用入门
  5. matlab转向梯形优化设计,转向梯形优化设计matlab程序
  6. [vue] watch的属性用箭头函数定义结果会怎么样?
  7. jQuery中的视图样式和动画效果
  8. 关于如何在同一个浏览器用不同的session登录同个系统
  9. Axure RPO_ 8 下载安装教程
  10. 迷你屏+OLED好屏:个性专业两手抓 华硕灵耀X 14专业好屏体验
  11. JUNIT5 + Mockito
  12. 计蒜客1185出书最多
  13. Office Professional Plus 2010 产品密钥
  14. ArcGIS Pro常见地图包系列--切片包(TPK)
  15. 【Node.js 微信公众号实战】4.Node.js 微信消息管理
  16. 2020ICPC·小米 网络选拔赛第一场(Matrix Subtraction (二维差分))
  17. js判断苹果ios各类机型
  18. mybatis配置类设置驼峰命名不起作用
  19. PDF处理软件:无法加注释加高亮(解密PDF等)
  20. Java获取上周一周末和上月初月末

热门文章

  1. VisualStudio2005英文版被SQL2005简体中文版汉化
  2. CentOS下ELK基于ElastAlert实现日志的微信报警
  3. 函数专题:sum、row_number、count、rank\dense_rank over
  4. 【Python】AxisError: axis 0 is out of bounds for array of dimension 0
  5. 新安装XCode7/XCode8 模拟器无法运行报-unable to boot the simulator解决方法
  6. 将asp.net webapi的运行时版本由4.0升级到4.5.1时遇到的问题及解决
  7. Cannot set property ‘innerHTML‘ of null 问题的解决
  8. Eclipse里编辑代码,进度条出现“Remote System Explorer Operation”解决方法
  9. 记录一次nginx502/504问题解决过程
  10. 终于解决 k8s 集群中部署 nodelocaldns 的问题