GSYFlutterBook/Flutter-DevFest2021.md

488 lines
20 KiB
Markdown
Raw Permalink Normal View History

2022-03-28 14:36:58 +08:00
> hello 大家好我是《Flutter开发实战详解》的作者郭树煜看标题就知道今天我要给大家分享的是 Flutter 相关的主题,分享内容是也比较直接简单,就是关于 **Flutter 布局相关的知识点**。
相信大家可能都听说过或者用过 Flutter ,对这部分内容可能有一定了解,但是正如标题所示,本次的主题是带你了解不一样的 Flutter **或者说经常性被萌新忽略的东西** ,所以这次将通过不一样的角度,带你看看 Flutter 的尺寸布局有趣的地方。
## 一、开始之前
在聊 Flutter 的布局之前,*首先大家觉得 Flutter 是什么?*
**Flutter 其实主要是跨平台的 UI 框架,它核心能力是解决 UI 的跨平台**,和别的跨平台框架不一样的地方在于:**它在性能接近原生的同时,做到了控件和平台无关的实现**。
但如果大家用过 Flutter ,应该知道 Flutter 里的我们写的界面都是通过 `Widget` 完成,并且可能会看起来嵌套得很多层,为什么呢?
这里就要先简单说一下 Flutter 的一些基础信息,**在 Flutter 里有 `Widget``Element``RenderObject``Layer` 等关键的核心设定**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image1)
其中我们最常写的 **`Widget` 并不是真正的 View 实例**,它需要转化为对应的 `RenderObject ` 才能绘制,而 `Element``Widget``RenderObject` 关键的中间实例,我们日常 Flutter 开发里用到的 **`BuildContext` 就是 `Element` 的抽象对象**。
> 也就是大致 `Widget` -> `Element` -> `RenderObject` 这样的过程。
**所以在 Flutter 里 `Widget` 代码只是“配置文件”的作用,真正工作的实例是它内部对应的 `Element``RenderObject` 实体**。
这也是 `Widget` 为什么可以是不可变的原因,它可以在使用时的被频繁构建,因为它不是真正干活的,**`Widget` 承载的是 `RenderObject` 里绘制时需要的各种状态信息**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image2)
这里举个简单例子,如图代码所示,我们定义了一个 text 的 Widget然后分别在 4 个地方添加,并成功运行,如果是一个真正的 View ,是不可以同时在 4 个地方被加载。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image3)
通过这个例子可以看到 `Widget` 并不是真正干活的,而主要负责绘制和布局的逻辑都在 `RenderObject`**因为布局和绘制的主要逻辑都在 `RenderObject` ,所以今天我们主要的内容也是在 `RenderObject`**
在 Flutter 里 `RenderObject` 作为绘制和布局的实体,主要可以分为两大子类:`RenderBox` 和 `RenderSliver` ,其中 `RenderSliver` 主要是在可滑动列表这种场景中使用,所以本次我们主要讨论的是 `RenderBox` 这种布局场景。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image4)
## 二、Flutter 的布局
**一般情况 Flutter 里的大小布局是从上往下传递 `Constraints` ,从下往上返回 `Size` 这样的流程**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image5)
简单理解这句话就是:父容器根据布局需要往下传递一个约束信息,而最子容器会根据自己的状态返回一个明确的大小,如果自己没有就继续往下的 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`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image6)
**所以我们就以 `RenderConstrainedBox` 相关的例子来举例**,看看 `ConstrainedBox` 是如何大小布局。
### 2.1、ConstrainedBox 的约束布局
如下代码所示,可以看到 `ColoredBox` 没有指定大小,但是运行后 `ColoredBox` 得到的是一个 100 x 100 的红色正方形, 因为它的父级 `ConstrainedBox` 往下传递的是 100 x 100 大小的 `ConstrainedBox` 约束。
```dart
Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 100, maxWidth: 100, minWidth: 100),
child: ColoredBox(
color: Colors.red,
),
),
),
)
```
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image7)
那如果这时候,把 `min` 的宽高改为 10 会发生什么事?
可以看到此时 `ColoredBox` 的大小变成和 `min` 的宽高一样大,为什么呢?
```dart
Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 10, maxWidth: 100, minWidth: 10),
child: ColoredBox(
color: Colors.red,
),
),
),
)
```
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image8)
首先 `ColoredBox` 并没有实现自己的 `performLayout`,而是通过继承了 `RenderProxyBox` 默认的逻辑来实现,这种情况在 Flutter 里比较常见,可以看到默认 `RenderProxyBox` 下:
- **在没有 child 的时候,用的是 `constraints.smallest`** ,也就是传递下来约束的最小值宽高;
- 在有 child 的时候使用 child 的大小;
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image9)
所以我们知道了,当控件没有实现自定义的 `performLayout` 时,并且没有 child 时,它很可能就是跟着父级约束的 smallest 走。
继续测试,如果这时候给 `ColoredBox` 增加一个 80 的 child ,可以看到红色框变了,变成了 `ColoredBox` 的 child 的大小 80 而不是 smallest因为这时候 `ColoredBox` 有了 child 用的是 child 的大小。
```dart
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,
),
),
),
),
)
```
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image10)
那如果我把 `ColoredBox` 的 child 修改为 150 的大小呢?
可以看到运行后红色方块还是 100 的大小,并没有变成 150。
```dart
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,
),
),
),
),
)
```
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image11)
这是为什么呢?
我们通过 Flutter 的调试工具看,可以看到我们虽然给 `SizedBox` 配置了 150 的参数,但是实际 `RenderConstrainedBox` 最终渲染时输出是 100 。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image12)
这里有两点:
- 第一就是 `Widget` 仅仅是作为配置信息,我们配置的宽高是 150 ,而实际 `RenderObject` 输出的是 100 ,所以我们写的并不是真实的 `View` 真正的布局效果还是要看 `RenderObject` 的脸色;
-`SizedBox``RenderConstrainedBox` 看, 它的 `performLayout` 的实现在没有 child时 150 的大小会被 `enforce` 成 parent 的 100
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image13)
对应 `enforce` 内部是通过 `clamp` 这个 API 完成, `enforce` 执行效果等同于 `150.clamp(10, 100)`,所以会得到 100 的结果。
> `clamp` 便是如果数据时在区间内就返回该数值,否则返回离其最近的边界值。
**所以通过 enforce `RenderConstrainedBox` 不会超出父容器的大小。**
那么为了实验,我们接下来把 `SizeBox` 换成 `ConstrainedBox` ,并且调整为约束为 10 - 150 的大小。
```dart
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 的大小,为什么呢?
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image14)
通过源码可以看到:
- 首先 `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`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image15)
也就是 child 在 `Element` 里被加载后,创建出对应的 `RenderObject` ,并且找到自己的 parent 然后将自己加入进去。
> Flutter 既然有具备 `RenderObject` 的 `Element` ,那同样也就有没有 `RenderObject` 的 `Element` ,比如 `ComponentElement` ,也就是我们常用的 `StatelessWidget` 等。
**这里可以看到 Element 得连接作用**。
## 三、多个 Child 的布局
前面介绍了单个 Child 的布局,这里简单介绍下多个 Child 主要有什么不同。
其实多个 Child 和单个一样,都会是从上往下传递 `Constraints` ,从下往上返回 `Size` 这样的流程。
比如下图,这是我们前面看到的例子,这里使用了 `Column` 控件对多个 `Text` 进行布局。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image16)
而其实 `Column``Row` 都是 `Flex` 的子类,我们按照思路去看 `RenderFlex` 的实现,就可以看到,对于多个 Child 的布局主要有这么几个关键点:
- `MultiChildRenderObjectWidget`
- `MultiChildRenderObjectElement`
- `ParentData`
`Widget``Element` 的逻辑我们这里暂时不深入展开,主要讲解不同的就是在 `RenderBox``ParentData`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image17)
如上图所示,基本上所有 Multi Child 的实现都有自己特有的 `ParentData` ,并且他们还不是直接继承 `ParentData` 而是继承他们的子类 `ContainterBoxParentData`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image18)
如图所示,他们的作用就是:
- `BoxParentData` 具备 `Offset` 参数,是用来觉得 Child 在控件的位置;
- `ContainterBoxParentData` 带有两个 `Sibling` 参数,主要是 `RenderBox` 里访问 children 就是通过这个双链表的方式访问的;
- `FlexParentData` 就是当前 `RenderFlex` 布局所需的参数;
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image19)
可以看到这就是 `RenderFlex` 布局时关键的参数所在,我们添加的 children `Widget`,在经过 `Element` 加载后,在前面说过的 `insert` 步骤会从一个 `List<Widget>` 变成通过 `ParentData` 的两个 `Sibling` 参数连接在一起的双向链表,访问时就是通过它进行访问的。
**所以在 children 布局时,我们通过对应的 `ParentData` 子类返回 child然后通过给 `ParentData` 配置 `Offset` 来决定 child 的位置**。
> 官方提供了更方便的自定义布局 `CustomMultiChildLayout` ,不需要你一步一步实现,比如常用的默认页面脚手架 `Scaffold` 就是用它实现。
## 四、有趣的知识点
既然聊到这个,我们在深入聊聊一些有趣的知识点,比如前面代码里的一直出现的 Scaffold ,这个是我们 Flutter 开发里最常用到的页面脚手架,也是一个页面布局的开始。
如果这时候把 `Scaffold` 给去掉,运行最初的代码,可以看到整个屏幕都红了,也即是 `ConstrainedBox` 铺满了整个屏幕。
```dart
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,
),
),
);
```
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image20)
为什么呢?
我们通过 Flutter 的调试工具可以看到,此时上级给你的约束就是屏幕大小,没有区间,而 `enforce` 等于 `10.clamp(392.72, 392.72)`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image21)
看到了没有,你没得选,`clamp(392.72, 392.72)` 也就是强行都变成了屏幕的宽度。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image22)
那如果这时候,我们加了一个 `Center` 控件呢?
可以看到约束大小又有了!
```dart
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)`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image23)
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image24)
为什么呢?
因为 `Center``RenderObject``RenderPositionedBox` **它在布局的时候会有一个 `constraints.loosen()` 的操作**,这也是为什么你有时候加多一个 `Center` 布局就突然生效的原因,因为 `loosen` 就成了 0-392.72 的约束。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image25)
```dart
BoxConstraints loosen() {
assert(debugAssertIsValid());
return BoxConstraints(
minWidth: 0.0,
maxWidth: maxWidth,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
```
如果不加 `Center`,像之前用的 `Scaffold` 为什么也能让 `BoxConstraints` 生效呢?
> 因为会出现虽然位置不对,所以这里调成了 100 比较好看到。
```dart
Scaffold(
body: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100, minHeight: 100, maxWidth: 100, minWidth: 100),
child: ColoredBox(
color: Colors.red,
),
),
)
```
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image26)
这其实是因为 `Scaffold` 的实现是一个叫 `CustomMultiChildLayout` 的控件。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image27)
**`Scaffold` 内的 `CustomMultiChildLayout` 布局时,对 `body` 使用了一个叫 `_BodyBoxConstraints``Constraints` 子类,这个类默认下所有 min 都是 0**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image28)
所以对于 body 下的 child 而言,都会有 0 的 min 约束信息存在。
> 所以 10.clamp(0, 392.72) 可以生效。
**那可能还会有人就疑惑, child 返回的 size 是在哪里使用?**
答案肯定是在 `paint` 的时候了使用,那这个 `Offset` 又是什么?
举个例子,我们看之前用过的 `Center` 里面,它会在 `paintChild` 的时候,会添加 `Offset` 信息,所以 child 就会在绘制的时候有偏移,从而绘制到准确的地方。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image29)
所以最终如下图所示,**`ColoredBox` 在绘制 Rect 时,通过 `Offset` (决定位置) 和 `Size`(决定大小),而至绘制出对应位置的红色方框**。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image30)
那如果我画的时候不遵循这个 `Offset` 呢?
这里我们可以通过一个简单的例子,直接用 `CustomPaint` 画一个 Demo。
```dart
new Container(
height: 200,
width: 200,
color: Colors.greenAccent,
child: CustomPaint(
///直接使用值做动画
foregroundPainter: _AnimationPainter(animation1),
),
)
```
可以看到,虽然 CustomPaint 是在 200 x 200 的大小下,但是动画绘制的圆可以很直接的超出这个大小。
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image31)
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image32)
**所以可以看到 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`
![](http://img.cdn.guoshuyu.cn/20220328_Flutter-DevFest2021/image33)