0
点赞
收藏
分享

微信扫一扫

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter


hello 大家好,我是《Flutter开发实战详解》的作者郭树煜,看标题就知道今天我要给大家分享的是 Flutter 相关的主题,分享内容是也比较直接简单,就是关于 Flutter 布局相关的知识点


相信大家可能都听说过或者用过 Flutter ,对这部分内容可能有一定了解,但是正如标题所示,本次的主题是带你了解不一样的 Flutter ,或者说经常性被萌新忽略的东西 ,所以这次将通过不一样的角度,带你看看 Flutter 的尺寸布局有趣的地方。

一、开始之前

在聊 Flutter 的布局之前,首先大家觉得 Flutter 是什么?

Flutter 其实主要是跨平台的 UI 框架,它核心能力是解决 UI 的跨平台,和别的跨平台框架不一样的地方在于:它在性能接近原生的同时,做到了控件和平台无关的实现

但如果大家用过 Flutter ,应该知道 Flutter 里的我们写的界面都是通过 ​​Widget​​ 完成,并且可能会看起来嵌套得很多层,为什么呢?

这里就要先简单说一下 Flutter 的一些基础信息,在 Flutter 里有 Widget​Element​​RenderObject​​Layer​ 等关键的核心设定

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件

其中我们最常写的 ​Widget 并不是真正的 View 实例,它需要转化为对应的 ​​RenderObject ​​​ 才能绘制,而 ​​Element​​​ 是 ​​Widget​​​ 和 ​​RenderObject​​​ 关键的中间实例,我们日常 Flutter 开发里用到的 ​BuildContext 就是 ​Element​ 的抽象对象


也就是大致 ​​Widget​​​ -> ​​Element​​​ -> ​​RenderObject​​ 这样的过程。


所以在 Flutter 里 Widget 代码只是“配置文件”的作用,真正工作的实例是它内部对应的 ​Element​​RenderObject​ 实体

这也是 ​​Widget​​​ 为什么可以是不可变的原因,它可以在使用时的被频繁构建,因为它不是真正干活的,​Widget 承载的是 ​RenderObject​ 里绘制时需要的各种状态信息

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Flutter_02

这里举个简单例子,如图代码所示,我们定义了一个 text 的 Widget,然后分别在 4 个地方添加,并成功运行,如果是一个真正的 View ,是不可以同时在 4 个地方被加载。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件_03

通过这个例子可以看到 ​​Widget​​​ 并不是真正干活的,而主要负责绘制和布局的逻辑都在 ​​RenderObject​​ 。 因为布局和绘制的主要逻辑都在 RenderObject ,所以今天我们主要的内容也是在 ​RenderObject​

在 Flutter 里 ​​RenderObject​​​ 作为绘制和布局的实体,主要可以分为两大子类:​​RenderBox​​​ 和 ​​RenderSliver​​​ ,其中 ​​RenderSliver​​​ 主要是在可滑动列表这种场景中使用,所以本次我们主要讨论的是 ​​RenderBox​​ 这种布局场景。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件_04

二、Flutter 的布局

一般情况 Flutter 里的大小布局是从上往下传递 Constraints ,从下往上返回 ​Size​ 这样的流程

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_flutter_05

简单理解这句话就是:父容器根据布局需要往下传递一个约束信息,而最子容器会根据自己的状态返回一个明确的大小,如果自己没有就继续往下的 child 递归。


更粗旷一些说就是:从上往下传递约束,传入的约束一般是有 ​​minHeight​​​、 ​​maxHeight​​​ 、 ​​minWidth​​​ 和 ​​maxWidth​​​ 等等,但是从下往上返回的 size 时,就会是一个固定 ​​width​​​ 和 ​​height​​ 尺寸。


而对于 Flutter ,布局的逻辑主要在对应 RenderObject​performLayout​


所以一般如果对于 ​​Widget​​​ 的布局感兴趣或者有疑惑,就可以先找到这个 ​​Widget​​​ 的 ​​RednerObject​​​ ,看这个 ​​RednerObject​​​ 的 ​​performLayout​​ 逻辑是怎么实现。


在 Flutter 最常用的就是应是 ​​Container​​​ 了, ​​Container​​​ 作为 Flutter 里最常用的抽象配置模版,它在宽高布局这一块用的是 ​​ConstrainedBox​​​,而不管是 ​​ConstrainedBox​​​ 还是 ​​SizedBox​​​, 他们对应的 ​​RenderObject​​​ 都是 ​​RenderConstrainedBox​​。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Android_06

所以我们就以 RenderConstrainedBox 相关的例子来举例,看看 ​​ConstrainedBox​​ 是如何大小布局。

2.1、ConstrainedBox 的约束布局

如下代码所示,可以看到 ​​ColoredBox​​​ 没有指定大小,但是运行后 ​​ColoredBox​​​ 得到的是一个 100 x 100 的红色正方形, 因为它的父级 ​​ConstrainedBox​​​ 往下传递的是 100 x 100 大小的 ​​ConstrainedBox​​ 约束。

Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 100, maxWidth: 100, minWidth: 100),
child: ColoredBox(
color: Colors.red,
),
),
),
)

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_前端_07

那如果这时候,把 ​​min​​ 的宽高改为 10 会发生什么事?

可以看到此时 ​​ColoredBox​​​ 的大小变成和 ​​min​​ 的宽高一样大,为什么呢?

Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
child: ColoredBox(
color: Colors.red,
),
),
),
)

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Android_08

首先 ​​ColoredBox​​​ 并没有实现自己的 ​​performLayout​​​,而是通过继承了 ​​RenderProxyBox​​​ 默认的逻辑来实现,这种情况在 Flutter 里比较常见,可以看到默认 ​​RenderProxyBox​​ 下:

  • 在没有 child 的时候,用的是 constraints.smallest​ ,也就是传递下来约束的最小值宽高;
  • 在有 child 的时候使用 child 的大小;

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Flutter_09

所以我们知道了,当控件没有实现自定义的 ​​performLayout​​ 时,并且没有 child 时,它很可能就是跟着父级约束的 smallest 走。

继续测试,如果这时候给 ​​ColoredBox​​​ 增加一个 80 的 child ,可以看到红色框变了,变成了 ​​ColoredBox​​​ 的 child 的大小 80 而不是 smallest,因为这时候 ​​ColoredBox​​ 有了 child, 用的是 child 的大小。

Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
child: ColoredBox(
color: Colors.red,
child: SizedBox(
width: 80,
height: 80,
),
),
),
),
)

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_flutter_10

那如果我把 ​​ColoredBox​​ 的 child 修改为 150 的大小呢?

可以看到运行后红色方块还是 100 的大小,并没有变成 150。

Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
child: ColoredBox(
color: Colors.red,
child: SizedBox(
width: 150,
height: 150,
),
),
),
),
)

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件_11

这是为什么呢?

我们通过 Flutter 的调试工具看,可以看到我们虽然给 ​​SizedBox​​​ 配置了 150 的参数,但是实际 ​​RenderConstrainedBox​​ 最终渲染时输出是 100 。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件_12

这里有两点:

  • 第一就是 ​​Widget​​ 仅仅是作为配置信息,我们配置的宽高是 150 ,而实际 ​​RenderObject​​ 输出的是 100 ,所以我们写的并不是真实的 ​​View​​, 真正的布局效果还是要看 ​​RenderObject​​ 的脸色;
  • 从 ​​SizedBox​​ 的 ​​RenderConstrainedBox​​ 看, 它的 ​​performLayout​​ 的实现在没有 child时, 150 的大小会被 ​​enforce​​ 成 parent 的 100

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件_13

对应 ​​enforce​​​ 内部是通过 ​​clamp​​​ 这个 API 完成, ​​enforce​​​ 执行效果等同于 ​​150.clamp(10, 100)​​,所以会得到 100 的结果。


​clamp​​ 便是如果数据时在区间内就返回该数值,否则返回离其最近的边界值。


所以通过 enforce RenderConstrainedBox 不会超出父容器的大小。

那么为了实验,我们接下来把 ​​SizeBox​​​ 换成 ​​ConstrainedBox​​ ,并且调整为约束为 10 - 150 的大小。

Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
child: ColoredBox(
color: Colors.red,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 150, minHeight: 10, maxWidth: 150, minWidth: 10),
),
),
),
),
)

可以看到红色正方形又变成了 10 的大小,为什么呢?

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件_14

通过源码可以看到:

  • 首先​​enforce​​​ 执行是​​150.clamp(10, 100)​​​ 和​​10.clamp(10, 100)​​​ ,等到的自然就是​​10-100​​;
  • 之后再到​​constrain​​ 里 0.clamp(10, 100),所以输出的是 10 这个最小值;


先前是 100.clamp(10, 100) 自然就是 100 的大小,而现在是 0.clamp(10, 100) ,自然就成了 10 。


从上面的例子,可以看到父布局约束影响 child 的大小的过程,甚至是变相局限住了 child 的大小返回,但是这都是在 ​​child.layout​​ 之后取得的大小。

那如果想要在 child.layout 之前就获取到 child 的大小呢?也就是 child 布局之前就获取到 child 的大小?

可以这样吗?当然可以!一般在官方的 RenderBox 都会有这四个方法:

  • ​computeMaxIntrinsicWidth​
  • ​computeMinIntrinsicWidth​
  • ​computeMaxIntrinsicHeight​
  • ​computeMinIntrinsicHeight​

为什么说一般呢?

因为你不写一般也不报错,并且这四个方法其实一般很少被调用,官方对它的描述是开销昂贵,并且我们调用时也不是直接调用它,而是通过对应的 get 方法:

  • ​getMaxIntrinsicWidth​
  • ​getMinIntrinsicWidth​
  • ​getMaxIntrinsicHeight​
  • ​getMinIntrinsicHeight​

在默认规范里,一般你只能 override ​​compute​​​ 开头的 API 去实现需要的逻辑,然后调用只能通过 get 对应的方法去调用,最后会执行到 ​​compute​​ 开头的 API ,它们之间时一一对应的。


也就是通过 ​​getMinIntrinsicWidth​​​ 来调用,比如:​​child.getMinIntrinsicWidth​​​ 最终调用到 ​​computeMinIntrinsicWidth​​。


看到这里大家有没想过: RenderBox 如何拿到 child ?child 如何从 Widget 变成 RenderObject?

这里就是 Element 起到的作用,当 ​​Widget​​ 被加载时:

  • 就会调用​​inflateWidget​​​ 去创建它的​​Element​​​,然后通过​​mount​​​ 用​​createRenderObject​​​ 创建出它的​​RenderObject​​;
  • 之后再执行​​attachRenderObject ​​​, 这时候这个 child 会通过​​_findAncestorRenderObjectElement​​​ 去找到它的 parent ,也就是离他最近的一个​​RenderObjectElment​​;
  • 最后执行 parent 的​​insertRenderObjectChild​​​ ,这时 child 就被插入进去​​RenderObject​​​,在​​RenderObject​​​ 里就可以获取到​​Widget​​;

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Flutter_15

也就是 child 在 ​​Element​​​ 里被加载后,创建出对应的 ​​RenderObject​​ ,并且找到自己的 parent 然后将自己加入进去。


Flutter 既然有具备 ​​RenderObject​​​ 的 ​​Element​​​ ,那同样也就有没有 ​​RenderObject​​​ 的 ​​Element​​​ ,比如 ​​ComponentElement​​​ ,也就是我们常用的 ​​StatelessWidget​​ 等。


这里可以看到 Element 得连接作用

三、多个 Child 的布局

前面介绍了单个 Child 的布局,这里简单介绍下多个 Child 主要有什么不同。

其实多个 Child 和单个一样,都会是从上往下传递 ​​Constraints​​​ ,从下往上返回 ​​Size​​ 这样的流程。

比如下图,这是我们前面看到的例子,这里使用了 ​​Column​​​ 控件对多个 ​​Text​​ 进行布局。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_前端_16

而其实 ​​Column​​​ 和 ​​Row​​​ 都是 ​​Flex​​​ 的子类,我们按照思路去看 ​​RenderFlex​​ 的实现,就可以看到,对于多个 Child 的布局主要有这么几个关键点:

  • ​MultiChildRenderObjectWidget​​;
  • ​MultiChildRenderObjectElement​​;
  • ​ParentData​​;

​Widget​​​ 和 ​​Element​​​ 的逻辑我们这里暂时不深入展开,主要讲解不同的就是在 ​​RenderBox​​​ 的 ​​ParentData​​。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_flutter_17

如上图所示,基本上所有 Multi Child 的实现都有自己特有的 ​​ParentData​​​ ,并且他们还不是直接继承 ​​ParentData​​​, 而是继承他们的子类 ​​ContainterBoxParentData​​。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_flutter_18

如图所示,他们的作用就是:

  • ​BoxParentData​​​ 具备​​Offset​​ 参数,是用来觉得 Child 在控件的位置;
  • ​ContainterBoxParentData​​​ 带有两个​​Sibling​​​ 参数,主要是​​RenderBox​​ 里访问 children 就是通过这个双链表的方式访问的;
  • ​FlexParentData​​​ 就是当前​​RenderFlex​​ 布局所需的参数;

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_flutter_19

可以看到这就是 ​​RenderFlex​​​ 布局时关键的参数所在,我们添加的 children ​​Widget​​​,在经过 ​​Element​​​ 加载后,在前面说过的 ​​insert​​​ 步骤会从一个 ​​List<Widget>​​​ 变成通过 ​​ParentData​​​ 的两个 ​​Sibling​​ 参数连接在一起的双向链表,访问时就是通过它进行访问的。

所以在 children 布局时,我们通过对应的 ParentData 子类返回 child,然后通过给 ​ParentData​ 配置 ​Offset​ 来决定 child 的位置


官方提供了更方便的自定义布局 ​​CustomMultiChildLayout​​​ ,不需要你一步一步实现,比如常用的默认页面脚手架 ​​Scaffold​​ 就是用它实现。


四、有趣的知识点

既然聊到这个,我们在深入聊聊一些有趣的知识点,比如前面代码里的一直出现的 Scaffold ,这个是我们 Flutter 开发里最常用到的页面脚手架,也是一个页面布局的开始。

如果这时候把 ​​Scaffold​​​ 给去掉,运行最初的代码,可以看到整个屏幕都红了,也即是 ​​ConstrainedBox​​ 铺满了整个屏幕。

MaterialApp(
title: 'GSY Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
child: ColoredBox(
color: Colors.red,
),
),
);

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Flutter_20

为什么呢?

我们通过 Flutter 的调试工具可以看到,此时上级给你的约束就是屏幕大小,没有区间,而 ​​enforce​​​ 等于 ​​10.clamp(392.72, 392.72)​

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Android_21

看到了没有,你没得选,​​clamp(392.72, 392.72)​​ 也就是强行都变成了屏幕的宽度。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_flutter_22

那如果这时候,我们加了一个 ​​Center​​ 控件呢?

可以看到约束大小又有了!

MaterialApp(
title: 'GSY Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Center(
child:ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
child: ColoredBox(
color: Colors.red,
),
)
),
);

可以看到约束变成了 ​​0-392.72​​​ 的约束,也就是 ​​10.clamp(0, 392.72)​

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_flutter_23

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件_24

为什么呢?

因为 ​​Center​​​ 的 ​​RenderObject​​​ 是 ​​RenderPositionedBox​​ ,它在布局的时候会有一个 constraints.loosen() 的操作,这也是为什么你有时候加多一个 ​​Center​​​ 布局就突然生效的原因,因为 ​​loosen​​ 就成了 0-392.72 的约束。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Android_25

BoxConstraints loosen() {
assert(debugAssertIsValid());
return BoxConstraints(
minWidth: 0.0,
maxWidth: maxWidth,
minHeight: 0.0,
maxHeight: maxHeight,
);
}

如果不加 ​​Center​​​,像之前用的 ​​Scaffold​​​ 为什么也能让 ​​BoxConstraints​​ 生效呢?


因为会出现虽然位置不对,所以这里调成了 100 比较好看到。


Scaffold(
body: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 100, maxWidth: 100, minWidth: 100),
child: ColoredBox(
color: Colors.red,
),
),
)

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Flutter_26

这其实是因为 ​​Scaffold​​​ 的实现是一个叫 ​​CustomMultiChildLayout​​ 的控件。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Flutter_27

Scaffold 内的 ​CustomMultiChildLayout​ 布局时,对 ​body​ 使用了一个叫 ​_BodyBoxConstraints​​Constraints​ 子类,这个类默认下所有 min 都是 0

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Android_28

所以对于 body 下的 child 而言,都会有 0 的 min 约束信息存在。


所以 10.clamp(0, 392.72) 可以生效。


那可能还会有人就疑惑, child 返回的 size 是在哪里使用?

答案肯定是在 ​​paint​​​ 的时候了使用,那这个 ​​Offset​​ 又是什么?

举个例子,我们看之前用过的 ​​Center​​​ 里面,它会在 ​​paintChild​​​ 的时候,会添加 ​​Offset​​ 信息,所以 child 就会在绘制的时候有偏移,从而绘制到准确的地方。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_Android_29

所以最终如下图所示,​ColoredBox 在绘制 Rect 时,通过 ​Offset​ (决定位置) 和 ​Size​(决定大小),而至绘制出对应位置的红色方框

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_前端_30

那如果我画的时候不遵循这个 ​​Offset​​ 呢?

这里我们可以通过一个简单的例子,直接用 ​​CustomPaint​​ 画一个 Demo。

new Container(
height: 200,
width: 200,
color: Colors.greenAccent,
child: CustomPaint(
///直接使用值做动画
foregroundPainter: _AnimationPainter(animation1),
),
)

可以看到,虽然 CustomPaint 是在 200 x 200 的大小下,但是动画绘制的圆可以很直接的超出这个大小。

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_flutter_31

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_控件_32

所以可以看到 Flutter 本质是一块画板,通过各种 Layer 分层,在每个 ​Layer​ 上又根据约定好的 ​Size​​Offset​ 绘制控件


Layer 就是一群 ​​RenderObject​​ 的集合。


其实只要你拿到这个 ​​Layer​​​ 上的 ​​Canvas​​​ ,就可以会知道这个 ​​Layer​​ 上的任意位置,当然一般情况下为了正确布局绘制,还是要遵循这个规则的。


常见的每个 ​​Route​​​ 就是一个独立的 ​​Layer​​ 。


总结

最后做个总结:

  • ​Widget​​​ 只是配置文件,它不可变,每次改变都会重构,它并不是真正的​​View ​​;
  • 布局逻辑主要在​​RenderBox​​​ 子类的​​performLayout​​​,并且可以提前获取​​child.size​​ ;
  • ​Element​​​ 的连接作用,​​Widget​​​ 被首次加载会创建​​Element​​​ 和​​RenderObject​​ ,并连接到一起;
  • 多​​child​​​ 布局里是通过​​ContainerBoxParentData​​ 来访问多个 child;
  • 约束布局时​​smallest​​ 和有没有 0 值(区间最小值)会影响约束的效果;
  • 控件绘制时遵循对应的​​Size​​​ 和​​Offset​​​ ,也可以超出​​Size​​​ 绘制,具体看所在​​Layer​​​ 的​​Canvas​​ ;

谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter_前端_33

举报

相关推荐

0 条评论