Flutter 的渲染逻辑及和 Native 通信
本文首发于 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
parent 会将某些布局约束传递给 child,这些约束是每个 child 在 layout 阶段必须要遵守的。如同 parent 这样告诉 child :“只要你遵守这些规则,你可以做任何你想做的事”。最常见的就是 parent 会限制 child 的大小,也就是 child 的 maxWidth 或者 maxHeight。
然后 child 会根据得到的约束生成一个新的约束,并将这个新的约束传递给自己的 child(也就是 child 的 child),这个过程会一直持续到出现没有 child 的 widget 为止。
之后,child 会根据 parent 传递过来的约束确定自己的布局详情(Layout Details)。如:假设 parent 传递给 child 的最大宽度约束为 500px,child 可能会说:“好吧,那我就用500px”,或者 “我只会用 100px”。这样,child 就确定了自己的布局详情,并将其传递给 parent。
parent 反过来做同样的事情,它根据 child 传递回来的 Layout Details 来确定其自身的 Layout Details,然后将这些 Layout Details 向上层的 parent 传递,直到到达 root widget (根 widget)或者遇到了某些限制。
那我们上面所提到的 约束(Constraints) 和 布局详情(Layout Details) 都是什么呢?这取决于布局协议(Layout protocol)。Flutter 中有两种主要的布局协议:Box Protocol 和 Sliver 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 中 我们常见的 widget 有 StatefulWidget
,StatelessWidget
,InheritedWidget
等等。但是这里还有另外一种 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 Protocol 和 Sliver 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
,顾名思义,他只能有一个 child 而 createRenderObject(...)
方法创建并返回了一个 RenderObject
为 RenderStingy
类的实例
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
,该类是继承自 RenderBox
。RenderShiftedBox
实现了 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 选择一个尺寸,以至于 Stingy 的 parent 知道如何放置它。类似于在 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)
复制代码
上述我们自定义 RenderBox
的 performLayout()
中做的事情可大概分为如下三个步骤:
- 使用
child.layout(...)
来布局 child,这里是为 child 根据 parent 传递过来的约束选择一个大小 child.parentData.offset
, 这是在为 child 如何摆放设置一个偏移量- 设置当前 widget 的
size
在我们的例子中,Stingy 的 child 是一个 Container
,并且 Container
没有 child,因此他会使用 child.layout(...)
中设置的最大约束。通常,每个 widget 都会以不同的方式来处理提供给他的约束。如果我们使用 RaiseButton
替换 Container
:
Stingy( child: RaisedButton( child: Text('Button'),onPressed: (){})
)
复制代码
效果如下:
可以看到,RaisedButton
的 width 使用了 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 的时候会根据 child 的 id
来布局。
下面我们来使用 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
表示的是当前 widget 的 parent 的 size
,在我们这个例子中也就表示 Container
的 size
。我们可以看看 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(...)
方法,这个方法接受一个 childId,childId 是由我们自己规定的,这个方法的作用是判断当前的 childId 是否对应着一个非空的 child。
满足 hasChild(...)
之后,接着就是 layoutChild(...)
来布局 child , 这个方法中我们会传递两个参数,一个是 childId,另外一个是 child 的约束(Constraints),这个方法返回的是当前这个 child 的 Size。
布局完成之后,就是如何摆放的问题了,也就是上述代码中的 positionChild(..)
了,此方法接受一个 childId
和 一个当前 child 对应的 Offset
,parent 会根据这个 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 中还有其他类型的编解码器BinaryCodec
,JSONMessageCodec
等,他们都有一个共同的父类 MessageCodec
。 所以我们也可以根据规则创建自己编解码器。
接下来创建的例子是:Flutter
给 Android
发送一条消息,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 通信相关推荐
- 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 完整代码示例 )
文章目录 前言 一.Android 端完整代码示例 二.Flutter 端完整代码示例 三.相关资源 前言 前置博客 : [Flutter]Flutter 混合开发 ( Flutter 与 Nativ ...
- 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 MethodChannel 通信 )
文章目录 前言 一.Android 端 MethodChannel 构造函数 二.Android 端 setMethodCallHandler 方法 三.Android 端实现 MethodChann ...
- 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 EventChannel 通信 )
文章目录 前言 一.Android 端 EventChannel 构造函数 二.Android 端 setStreamHandler 方法 三.Android 端实现 EventChannel 通信步 ...
- 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 BasicMessageChannel 通信 )
文章目录 前言 一.Android 端 BasicMessageChannel 构造函数 二.Android 端 MessageCodec 子类实现 三.Android 端 setMessageHan ...
- 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 在 Flutter 端实现 EventChannel 通信 )
文章目录 一.EventChannel 简介 二.EventChannel 在 Dart 端的实现 1.EventChannel 构造方法 2.创建广播流 Stream 3.设置监听回调函数 4.Ev ...
- 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 在 Flutter 端实现 MethodChannel 通信 )
文章目录 一.MethodChannel 简介 二.MethodChannel 在 Dart 端的实现 1.MethodChannel 构造函数 2.invokeMethod 函数 3.MethodC ...
- 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 在 Flutter 端实现 BasicMessageChannel 通信 )
文章目录 一.BasicMessageChannel 简介 二.BasicMessageChannel 在 Dart 端的实现 1.BasicMessageChannel 构造方法 2.使用 Basi ...
- Flutter 画面渲染的全面解析
本篇将通过不一样的角度来介绍 Flutter Framework 的整体渲染原理,深入剖析 Flutter 中构成 Layer 后的绘制流程,让开发者对 Flutter 的渲染原理和实现逻辑有更清晰的 ...
- 深入解析Flutter下一代渲染引擎Impeller
作者 魏国梁:字节 Flutter Infra 工程师, Flutter Member,长期专注 Flutter 引擎技术 袁 欣:字节 Flutter Infra 工程师, 长期关注渲染技术发展 谢 ...
最新文章
- 你,保持童心;我,帮你保持童颜
- 项目源码分享之[条码扫描后台监控程序]
- 使用 Bamboo 构建项目的 CICD 过程文档
- 摊牌了,我靠他实现了NLP模型使用入门
- matlab转向梯形优化设计,转向梯形优化设计matlab程序
- [vue] watch的属性用箭头函数定义结果会怎么样?
- jQuery中的视图样式和动画效果
- 关于如何在同一个浏览器用不同的session登录同个系统
- Axure RPO_ 8 下载安装教程
- 迷你屏+OLED好屏:个性专业两手抓 华硕灵耀X 14专业好屏体验
- JUNIT5 + Mockito
- 计蒜客1185出书最多
- Office Professional Plus 2010 产品密钥
- ArcGIS Pro常见地图包系列--切片包(TPK)
- 【Node.js 微信公众号实战】4.Node.js 微信消息管理
- 2020ICPC·小米 网络选拔赛第一场(Matrix Subtraction (二维差分))
- js判断苹果ios各类机型
- mybatis配置类设置驼峰命名不起作用
- PDF处理软件:无法加注释加高亮(解密PDF等)
- Java获取上周一周末和上月初月末
热门文章
- VisualStudio2005英文版被SQL2005简体中文版汉化
- CentOS下ELK基于ElastAlert实现日志的微信报警
- 函数专题:sum、row_number、count、rank\dense_rank over
- 【Python】AxisError: axis 0 is out of bounds for array of dimension 0
- 新安装XCode7/XCode8 模拟器无法运行报-unable to boot the simulator解决方法
- 将asp.net webapi的运行时版本由4.0升级到4.5.1时遇到的问题及解决
- Cannot set property ‘innerHTML‘ of null 问题的解决
- Eclipse里编辑代码,进度条出现“Remote System Explorer Operation”解决方法
- 记录一次nginx502/504问题解决过程
- 终于解决 k8s 集群中部署 nodelocaldns 的问题