Flutter Widget :Container 布局详解

在 Flutter 中,号称一切皆 Widget,手势是 Widget,动画是 Widget,UI 更是 Widget,今天我们就来说说 Widget 里比较特殊的一个,Container。

1. 介绍

Container 初用起来很简单,但是里面的逻辑又有些复杂,我也不敢说完全吃透,所以本文还是以总结网上各种文章为主,再加上自己的理解,如果有不对的地方,请一定指出

在 Flutter 中,所有的功能都被分散成单一功能的 Widget,比如居中有 Center,边框有 Padding,文字是 Text,手势是 GestureDetector,他们各自维护一个功能,但是我们商业 App 的 UI 都很精美,如果要实现一个很好的布局,需要嵌套非常多的布局 Widget,所以 Container 应运而生:

A convenience widget that combines common painting, positioning, and sizing widgets.

官方文档一语道破了 Container 复杂的原因,它是一个便利部件,融合了绘图、定位和尺寸部件,据我所知,它可以设置大小,背景颜色,边框,圆角,阴影,渐变。而且大小可以有很多种情况,时而依赖于父部件,时而依赖于子部件,时而依赖于自己,所以我们稍后会重点说一说 Container 的布局(尺寸规则,宽高)。

2. Widget渲染流程

Flutter 是树状渲染结构,首先从根结点开始渲染,从上到下传递约束,直到最终的叶子节点(没有子节点了),然后叶子节点根据约束确定自身大小,然后将大小返回给上级结点,然后上一级根据叶子节点的尺寸,决定自己的大小,再返回上一级,最终根节点确定了大小。之后,根结点逐级往下摆放子节点的位置(根据子节点及子节点兄弟节点的大小和偏移量)。

3. Container渲染流程

根据官网的介绍,Container 会首先使用设置的 padding 来围绕子部件,然后对 padding 的大小添加额外的约束(如果非空),然后 Container 被外部的空白区域(margin)包围。在绘制过程中,Container 首先应用变换(transform),然后绘制装饰(decoration)来填充区域,接着绘制子部件,最后绘制前景装饰(foregroundDecoration),同时填充该区域。

decoration 和 foregroundDecoration 是填充配置,前者是在子部件之下,后者是子部件之上,可以设置填充颜色,边框,填充形状,阴影,渐变色,背景图片等。

如果 Container 的约束是有限制的,那么没有子部件的 Container 会尝试尽可能大,如果 Container 的约束是没有限制的(unbounded),它就会尽可能小。

有子部件的 Container,根据子部件确定自己的大小。

4. 有无限制约束

到底什么是有限制约束和无限制约束呢,各种部件又都是哪种约束呢?

在 Flutter 中,Widget 由底层的 RenderBox 渲染盒渲染,父组件向渲染盒提供约束条件,然后渲染盒用这些约束调整自己的尺寸,约束(Constraint)由最大和最小的宽/高组成,尺寸(Size)由特定的宽/高组成。

通常来说,有三种处理约束的盒子:

• 尽可能大:Center 和 ListView 等

• 和子部件一样大:Transform 和 Opacity 等

• 特定大小:Image 和 Text 等

• 特殊情况:Row 和 Column 由其给定的约束决定,Container 由其构造函数的参数决定

但约束有时会变得紧凑,意思是它没有留给渲染盒子自行决定尺寸的余地(例如最大宽度和最小宽度相等,那允许的宽度是个固定值),例如 App 这个 Widget,它的约束被设置为固定的应用程序内容大小(也就是整个屏幕)。而在 Flutter 中,很多的 widget,尤其是只能有一个子部件的 widget,会传递自己的约束到子部件。也就是说,如果你在 App 根渲染树里嵌套了一系列 widget,他们将会因为紧凑的约束,一级级地贴着。

但是有些 widget会使约束宽松,意思是最大的约束保留,但是最小的约束移除了,比如 Center。

4.1 无限制约束

• 在某些情况下,赋给 widget 的约束是无限制(unbounded)的,或者说是无限(infinite)的。也就是说,最大宽度和最大高度,都是 double.INFINITY。

• 一个尝试尽可能大的 widget,遇到无限制约束的时候,是不会起作用的,因为它不知道到底该有多大,在 debug 模式下,就会抛出异常。

• 最常见的拥有无限制约束的情况,就是嵌入在弹性盒子里面,比如 Row,Column 或者可以滚动的区域(ListView 或者其他 ScrollView 子类)。

需要指出的是,ListView 会在其交叉方向扩张到父部件边界,例如一个纵向滚动的列表,在横向会尽量和父部件一样宽。

当你在横向滚动列表里,嵌入一个纵向滚动列表的时候,纵向列表会尽可能宽,也就是无限宽,因为横向列表是无尽宽的,这就会异常。

另外,弹性盒子(Row 和 Column)在有限制和无限制约束的情况下,表现出来的行为也不同。

• 在有限制的约束时,他们会在其方向上尽可能大。

• 在无限制约束时,他们会在其方向上适应他们子组件(包住子组件)。这种情况下,你不能在弹性盒子里用Expanded,因为这是将无法确定部件大小。

5. Container 布局(尺寸规则)

因为 Container 集合了其他部件的功能,所以它的布局有些复杂,简而言之,按照顺序,Container 会:

• 遵循对齐规则

• 为子部件调整自身大小

• 遵循宽高和约束

• 然后 Container 尝试尽可能小。

5.1 来看看官方文档的解释(这个解释看不懂就算了,有点啰嗦):

• 如果 Container 没有子部件,没有宽高,没有约束,并且父部件提供了无限制约束(unbounded constraints),Container 会尽可能小。

• 如果 Container 没有子部件,没有对齐规则,但是提供了高度、宽度或者约束,那么 Container 会在遵循宽、高、约束和父部件约束的情况下,尽可能小。

• 如果 Container 没有子部件,没有宽高,没有约束,没有对齐,但是父部件提供了有限制约束,那么 Container 会扩张以适应(fit)父部件约束

• 如果 Container 有一个对齐规则,并且父部件提供了无限制约束,那么 Container 会尝试调整自己来包围子部件

• 如果 Container 有一个对齐规则,而且父部件提供了有限制约束,那么 Container 会尝试扩张以适应(fit)父部件,然后根据对齐方式,将子部件置于其内

• 另外,Container 有子部件,但是 Container 没有宽高、约束、对齐规则,那么 Container 会传递父部件的约束到其子部件,然后调整自身来匹配子部件。

• margin 和 padding 也会影响布局,decoration 会隐性增加 padding(比如设置 border)。

• 默认是尽可能大

5.2 我们来总结一下:

maxWidth maxHeight 约束 有无子组件 布局规则
有值 有值 有限制 尽可能大
有值 无值 高度无限制,宽度有限制 高度尽可能小,宽度尽可能大
无值 有值 高度有限制,宽度无限制 高度尽可能大,宽度尽可能小
无值 无值 无限制 尽可能小
都行 都行 都行 在满足约束的前提下尽可能小

5.3 例子

没有子组件,有约束,尽可能大↓

没有子组件,父组件约束最大宽度是屏幕宽度,最大高度是无限,自己的约束最小宽度是100,最小高度是100,则高度尽可能小到100,宽度尽可能大到屏幕宽度↓

父组件最大约束是屏幕宽高,自己是固定宽度100,则宽度是100,高度尽可能大到屏幕高度↓

自己没有设置约束,有子组件,所以自己包住子组件↓

自己设置固定高度100,宽度没有约束,有子组件,则高度为100,宽度包住子组件↓

有子组件,自己的高度是固定100,宽度设置为无限,则高度为100,宽度是父组件的约束屏幕宽度↓

有子组件,设置最小宽高为无限,则大小为屏幕大小↓

6. 总结

Container 应该是 Flutter 中最灵活的布局 widget,大家一定要善用 Container,用巧妙的方式处理布局,否则可能会让代码可读性变差,难以维护

7. 参考文献

[1]https://juejin.im/post/5b8ce76f51882542c0626887

[2]https://api.flutter.dev/flutter/widgets/Container-class.html

[3]https://flutter.dev/docs/development/ui/layout

[4]https://flutter.dev/docs/development/ui/layout/box-constraints

[5]https://medium.com/jlouage/container-de5b0d3ad184

[6]https://flutterdoc.com/widgets-container-d8eee21ad2f4

[7]https://flutteracademy.com/posts/what-is-a-container-in-flutter/

[8]https://www.youtube.com/watch?v=wj1ZGQ-Jc04

[9]https://www.youtube.com/watch?v=4KVCaP69GV8

[10]https://proandroiddev.com/understanding-flutter-layout-box-constraints-292cc0d5e807