本文首发于 ,作者刘斯龙, 5年的 Android 程序员,从事过 AR ,Unity3D,Weex,Cordova,Flutter 及小程序开发
作者 github:
作者 blog:
作者 StackOverflow:
作者掘金博客:
在这篇文章中,我们主要了解两个部分的内容,一个是 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 的约束都叫做 这些约束决定了每个 child 的 maxWidth 和 maxHeight 以及 minWidth 和 minHeight。如:parent 可能会将如下的 传递给 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 中都有许多用来执行渲染的属性和方法:
- : 从 parent 传递过来的约束。
- : 这里面携带的是 parent 渲染 child 的时候所用到的数据。
- :此方法用于布局所有的 child。
- :这个方法用于绘制自己或者 child。
- 等等...
但是, 是一个抽象类,他需要被子类继承来进行实际的渲染。 的两个非常重要的子类是 和 。这两个类是所有实现 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); @override RenderObject createRenderObject(BuildContext context) { // TODO: implement createRenderObject return RenderStingy(); }}复制代码
Stingy
继承了 SingleChildRenderObjectWidget
,顾名思义,他只能有一个 child 而 createRenderObject(...)
方法创建并返回了一个 RenderObject
为 RenderStingy
类的实例
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
,该类是继承自 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, Listchildren = const [],})复制代码
- 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}); @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
表示的是当前 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 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 通信了。
官网上有一个 是使用 来调用给本地的方法获取手机电量。
其实我们还可以使用另外一个类进行通信,叫做 ,先来看看它如果创建:
// javabasicMessageChannel = new BasicMessageChannel(getFlutterView(), "foo", StringCodec.INSTANCE);复制代码
BasicMessageChannel
需要三个参数,第一个是 ;第二个是通道名称,第三个是交互数据类型的编解码器,我们接下来的例子中的交互数据类型为 String
,所以这里传递的是 StringCodec.INSTANCE
,Flutter 中还有其他类型的编解码器,等,他们都有一个共同的父类 MessageCodec
。 所以我们也可以根据规则创建自己编解码器。
接下来创建的例子是:Flutter
给 Android
发送一条消息,Android
收到消息之后给 Flutter
回复一条消息,反之亦然。
先来看看 Android
端的部分代码:
// 接收 Flutter 发送的消息basicMessageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler() { @Override public void onMessage(final String s, final BasicMessageChannel.Reply 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 () { @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 BasicMessageChannelchannel = BasicMessageChannel ('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 应用实现实时音视频功能的 plugin。