跳转至

布局

在第四课我们认识了一些用来布局的 Widget,比如 Row、Column、Expanded、Align 等,但背后的原理我们并没有细说。可能有同学在课下尝试的时候已经发现一些 Widget 没有呈现自己想要的大小,或者是在调试界面看到了黄黑相见的溢出提示。这节课我们聚焦约束,解释 Flutter 背后布局的原理。

Flutter | Understanding constraints

首先我们需要了解一些概念:

  • 「布局 Layout」:每一个 Widget 在屏幕上的大小和位置;Flutter 框架确定每一个 Widget 在屏幕上的大小和位置的过程。
  • 「约束 Constraint」:父 Widget 对子 Widget 布局的要求,为 最大/最小 宽度/高度(max/min Width/Height)。
  • 「大小 Size」:一个 Widget 在屏幕上的长和宽(width/height)。
  • 「位置 Position」:一个 Widget 在屏幕上的位置(x/y)。

有了这些概念,我们就可以了解 Flutter 布局的基本原理:

Constraints go down. Sizes go up. Parent sets position.

展开来说,是指:

  • 父 Widget 向子 Widget 传递约束。
  • 子 Widget 根据设置的大小和父 Widget 传来的约束决定自己的最终大小。
  • 父 Widget 确定子 Widget 的位置。

案例

看一些比较经典的案例会更容易理解,在 Flutter | Understanding constraints 中共有 29 个案例,推荐将其中一千多行的代码复制到本地运行(网页版较卡顿),查看每个案例的效果。

  • 1-6 Container & Align
  • 7-8 Container & Container
  • 9-12 ConstrainedBox
  • 13-17 UnconstrainedBox / OverflowBox / LimitedBox
  • 18-22 FittedBox & Text
  • 23-27 Row / Expaned / Flexible
  • 28-29 tight constraints / loose constraints

紧约束和松约束

紧约束给出了一个确切的大小:

Dart
1
2
3
4
5
BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

松约束则只指定最大的大小:

Dart
1
2
3
4
5
BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;

LayoutBuilder

由于约束向下传递,我们可以在下层的 Widget 树上方包裹一个 LayoutBuilder 来获取当前的约束。

LayoutBuilder 的构造函数:

Dart
1
LayoutBuilder({Key? key, required Widget builder(BuildContext, BoxConstraints)})

通过匿名函数 builder() 的第二项参数,我们可以获取到上层 Widget 传给下层 Widget 的约束,例如:

Dart
1
2
3
4
5
6
7
8
9
LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    if (constraints.maxWidth > 600) {
      return _buildWideContainers();
    } else {
      return _buildNormalContainer();
    }
  },
)

Widget 布局特性

通过上面的例子可以看到,每个 Widget 都有着自己的布局特性,这是由它们本身的功能决定的。你可以通过文档或者源码来了解。

打开 API 文档 之后,你可以通过右上角的搜索来找到对应的 Widget。

源码查看则可以使用 VS Code 的跳转功能,按住 ctrl 或者 cmd 点击你感兴趣的 Widget,查看它继承的基类,其中 createRenderObject() 和 performLayout() 这两个函数与布局最相关,你可能需要多次跳转才能找到。

局限

上面所提到的 Constraints go down. Sizes go up. Parent sets position. 有下面的一些局限:

  • 一个 Widget 需要结合父 Widget 的约束才能得到其大小,这意味着 Widget 的最终大小很可能和代码中设置的大小不一样。
  • 一个 Widget 不会决定也不会知道自己在屏幕中的位置,因为父 Widget 决定子 Widget 的位置。
  • 父子 Widget 的约束和大小传递是逐层的,这意味着开发者很难精确控制每一个 Widget 的位置和大小。
  • 布局的最后一步是确定位置,需要做到精确,否则子 Widget 的大小可能被忽略。