Flutter 的渲染逻辑及和 Native 通信 (四)

本文作者刘斯龙, 5年的 Android 程序员
作者 github: github.com/liusilong
作者 blog:liusilong.github.io/
作者 StackOverflow:stackoverflow.com/users/47233…

在这篇文章中,我们主要了解两个部分的内容,一个是 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 所属的树形结构如下:

  |_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

  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,
List children = 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 实现一个用于展示热门标签的效果:

   child: CustomMultiChildLayout(
     delegate: _LabelDelegate(itemCount: items.length, childId: childId),
     children: items,
   ),
 )

代码我们的 _LabelDelegate 里面接受两个参数,一个为 itemCount,还有是 childId。
_LabelDelegate 代码如下:


  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 的时候会抛出如下异常:

        at io.flutter.view.FlutterMain.ensureInitializationComplete(FlutterMain.java:178)
.```
复制代码我们有两种方式来执行初始化操作:一个是直接让我们的 Application 继承 FlutterApplication,另外一个是需要我们在我们自己的 Application 中手动初始化:
方法一:
```public class App extends FlutterApplication {  
  
}

代码方法二:

  @Override  
  public void onCreate() {  
  super.onCreate();  
  // 初始化 Flutter
  Flutter.startInitialization(this);  
  }  
}

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


	...
	
    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }
    
    ...
}

如果我们的 App 只是需要使用 Flutter 在屏幕上绘制 UI,那么没问题, Flutter 框架能够独立完成这些事情。但是在实际的开发中,难免会需要调用 Native 的功能,如:定位,相机,电池等等。这个时候就需要 Flutter 和 Native 通信了。
官网上有一个案例 是使用 MethodChannel来调用给本地的方法获取手机电量。
其实我们还可以使用另外一个类进行通信,叫做 BasicMessageChannel,先来看看它如果创建:

basicMessageChannel = new BasicMessageChannel<String>(getFlutterView(), "foo", StringCodec.INSTANCE);

代码BasicMessageChannel 需要三个参数,第一个是 BinaryMessenger;第二个是通道名称,第三个是交互数据类型的编解码器,我们接下来的例子中的交互数据类型为 String ,所以这里传递的是 StringCodec.INSTANCE,Flutter 中还有其他类型的编解码器BinaryCodec,JSONMessageCodec等,他们都有一个共同的父类 MessageCodec。 所以我们也可以根据规则创建自己编解码器。
接下来创建的例子是:Flutter 给 Android 发送一条消息,Android 收到消息之后给 Flutter 回复一条消息,反之亦然。
先来看看 Android 端的部分代码:
// 接收 Flutter 发送的消息

    @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);

    }
});

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


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);

    }
});
  // 消息通道
  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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值