写点什么

Flutter 的渲染逻辑,以及与 Native 通信(一)

  • 2019-11-30
  • 本文字数:3646 字

    阅读完需:约 12 分钟

Flutter 的渲染逻辑,以及与 Native 通信(一)

本文来自 RTC 开发者社区(rtcdeveloper.com)的用户投稿,作者是一位资深 Android 工程师,熟悉跨平台开发。在这篇文章中,主要包含两个部分内容, Flutter 的渲染逻辑,以及 Flutter 和 Native 互通的方法,这里的 Native 是以 Android 为例。对于在 Flutter 上构建实时音视频场景开发者有一定的参考意义。

1 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 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);
@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(…) 一样。


运行效果如下:



2019-11-30 15:04532

评论

发布
暂无评论
发现更多内容
Flutter 的渲染逻辑,以及与 Native 通信(一)_文化 & 方法_声网_InfoQ精选文章