写点什么

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:04696

评论

发布
暂无评论
发现更多内容

云函数中使用Python-ORM: Peewee

刘宇

裸机Ubuntu18.04 配置实现人脸识别的第三方库

月夜

dlib face_recognition 人脸识别 环境配置

CentOS7使用Iptables做网络转发

wong

Centos 7 iptables

《CSS 选择器世界》读书笔记

云走

CSS Java html 读书笔记 大前端 张鑫旭

Flink 1.10 细粒度资源管理解析

Apache Flink

大数据 flink 流计算 实时计算

谈一谈自由职业者的心态

Bob Jiang

自由职业 写作 心态 营销

个人的投资原则

史前靓仔

引入了绩效管理,团队反而一天不如一天了?(一)

Geek_6rptuk

团队管理 企业文化 绩效

回"疫"录(13):不信谣,不传谣

小天同学

疫情 回忆录 现实纪录 纪实 谣言

可能是最最最最简单的搭建博客方法

彭宏豪95

GitHub 写作 博客 GitPress

人生就是一场说走就走的旅行

kimmking

Web3极客日报#136

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

Web3极客日报#137

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

CTO股权”避坑“,你根本不知道我们多努力

赵新龙

TGO鲲鹏会 股权 CTO

和儿子装一台 Hackintosh

苏锐

DIY Hackintosh 装机

带你100% 地了解 Redis 6.0 的客户端缓存

程序员历小冰

redis 缓存 redis6.0.0

Mac 自带软件-聚焦搜索

Winann

macos Mac spotlight

我的编程之路-3(熟练)

顿晓

c++ 调试 经历 项目 疑问

Spring Boot可执行JAR的原理

小判

Spring Boot 类加载 Fat-JAR deflate JAR URL

抽象

落英亭郎

系统设计 面向对象 抽象

《Linux就该这么学》笔记(一)

编程随想曲

Linux

找到自己的领域,然后封神

一尘观世界

成长 提升 领域 机遇 趋势

基于Serverless架构的Git代码统计

刘宇

(乱记)“怎样培养优秀孩子”

启润

【Howe 学 JAVA】Java 类集框架2——集合输出

Howe

Java 集合 输出 类集

我跑步的时候会想些什么

养牛致富带头人

跑步 运动 锻炼

TL如何在团队中培养出更多前端技术专家

贵重

大前端 团队建设 技术管理

你觉得你是哪类人?

Janenesome

读书笔记 思考

回文串解题记录

晓刚学代码

Java 算法

当你不知道怎么学习新技术时

石君

学习 方法论

MacOS使用指南之我并不需要系统菜单栏

lmymirror

macos 高效工作 完美主义 操作系统 新手指南

Flutter 的渲染逻辑,以及与 Native 通信(一)_文化 & 方法_声网_InfoQ精选文章