Flutter初学者必读的高级布局规则

假设有人正在学习Flutter,他问你为什么有的width:100的widget宽度不是100像素,标准答案是让他将widget放在一个Center里面,对吗?

别这么做。

如果你这么回答他,他就会一次又一次跑回来问你新的问题,比如说为什么某些FittedBox无法正常工作,为什么那个Column溢出,或者IntrinsicWidth是用来做什么的,诸如此类。

这时候你应该告诉他:Flutter布局与HTML布局(他之前可能接触的就是后者)有着很大不同,然后让他记住以下规则:

约束(Constraints)在下面,大小(Sizes)在上面。位置(Positions)由父项(Parents)决定。

想要真正理解Flutter的布局,就得搞清楚上面这条规则,所以大家都应该尽早学会它。

具体来说:

  • widget从其父项获得自己的约束。一个“约束”是由4个double值组成的:分别是最小和最大宽度,以及最小和最大高度。
  • 然后,widget会遍历自己的子项(children)列表。widget会逐个向每个子项告知它们的约束(各个子项的约束可以是不同的),然后询问每个子项想要设置的大小。
  • 接下来,widget一个个确定子项位置(在x轴上确定水平位置,在y轴上确定垂直位置)。
  • 最后,widget将其自身大小告知父项(当然这个大小也要符合原始约束)。

例如,如果一个 widget 是一个带有一些 padding 的 column,并且想要布局自己的两个子项:

Widget:你好父项,我的约束是什么?

父项:你的宽度必须在 90 到 300 像素之间,高度在 30 到 85 像素之间。

Widget:我想有 5 像素的 padding,所以我的子项最多有 290 像素的宽度和 75 像素的高度。

Widget:你好第一个子项,你的宽度必须在 0 到 290 像素之间,高度在 0 到 75 像素之间。

第一个子项:好的,那么我希望自己的宽度是 290 像素,高度为 20 像素。

Widget:那么,因为我想将第二个子项放在第一个子项之下,因此第二个子项只剩下 55 像素的高度。

Widget:你好第二个子项,你的宽度必须介于 0 到 290 像素之间,并且高度必须介于 0 到 55 像素之间。

第二个子项:好吧,我希望宽度是 140 像素,高 30 像素。

Widget:很好。我将把第一个子项放在 x: 5 和 y: 5 的位置,将第二个子项放在 x: 80 和 y: 25 的位置。

Widget:你好父项,我决定将自己设为 300 像素宽和 60 像素高。

限制

因为上述布局规则的关系,Flutter 的布局引擎有一些重要的限制:

  • 一个 widget 只能在其父项赋予的约束内决定其自身的大小。这意味着 widget 往往不能自由决定自己的大小
  • widget不知道,也无法确定自己在屏幕上的位置,因为它的位置是由父项决定的。
  • 由于父项的大小和位置又取决于上一级父项,因此只有考虑整个树才能精确定义每个 widget 的大小和位置。

示例

可以运行这个 DartPad 来观察每个示例的效果。另外可以从这个 GitHub 存储库中获取最新代码。

示例 1

Container(color: Colors.red)

屏幕是 Container 的父项。它强制红色的 Container 与屏幕大小完全相同。

这样 Container 就会填满整个屏幕,并且全都变成红色。

示例 2

Container(width: 100, height: 100, color: Colors.red)

红色的 Container 想要设为 100×100 的大小,但这是不行的,因为屏幕会强制使其大小与屏幕完全相同。

因此,Container 将填满整个屏幕。

示例 3

Center(
   child: Container(width: 100, height: 100, color: Colors.red)
)

屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。

Center 告诉 Container,后者的大小不能超出屏幕。现在,Container 就可以是 100×100。

示例 4

Align(
   alignment: Alignment.bottomRight,
   child: Container(width: 100, height: 100, color: Colors.red),
)

这与前面的示例不同之处是使用了 Align 代替 Center。

Align 还告诉 Container,后者的大小可以自由决定,但是如果有空白空间,它不会让 Container 居中,而是将其对齐到可用空间的右下角。

示例 5

Center(
   child: Container(
      color: Colors.red,
      width: double.infinity,
      height: double.infinity,
   )
)

屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。

Center 告诉 Container,后者的大小不能超出屏幕。Container 希望具有无限大的尺寸,但由于存在前述约束,因此它只能填满屏幕。

示例 6

Center(child: Container(color: Colors.red))

屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。

Center 告诉 Container,后者的大小不能超出屏幕。由于 Container 没有子项且没有固定大小,因此它决定要尽可能变大,结果就填满了屏幕。

但为什么 Container 要这样决定呢?因为这是 Container widget 的创建者的设计决策。它也可能会有其他设计,所以你需要阅读 Container 的文档以了解它在不同情况下的行为方式。

示例 7

Center(
   child: Container(
      color: Colors.red,
      child: Container(color: Colors.green, width: 30, height: 30),
   )
)

屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。

Center 告诉红色 Container,后者的大小不能超出屏幕。由于红色 Container 没有大小,但有一个子项,因此它决定要与子项的大小相同。

红色的 Container 告知其子项,后者的大小不能超出屏幕。

这个子项恰好是一个绿色的 Container,希望自己的大小是 30×30。如上所述,红色的 Container 会将自己的大小设为子项的大小,因此它也会是 30×30。结果红色是显示不出来的,因为绿色的 Container 会完全覆盖红色的 Container。

示例 8

Center(
   child: Container(
     color: Colors.red,
     padding: const EdgeInsets.all(20.0),
     child: Container(color: Colors.green, width: 30, height: 30),
   )
)

红色的 Container 会根据子项的大小设置自己的大小,但同时会考虑自己的 padding。因此它将是 70×70(=30×30 加上各个面的 20 像素 padding)。由于存在 padding,因而红色将是可见的,绿色的 Container 的大小与上一个示例中的相同。

示例 9

ConstrainedBox(
   constraints: BoxConstraints(
      minWidth: 70, 
      minHeight: 70,
      maxWidth: 150, 
      maxHeight: 150,
   ),
   child: Container(color: Colors.red, width: 10, height: 10),
)

你可能会以为 Container 会是 70 到 150 像素之间,但是你错了。ConstrainedBox 只会在 widget 从父项获得的约束基础之上施加额外的约束。

在这里,屏幕将 ConstrainedBox 强制为与屏幕大小完全相同,因此它将告诉自己的子 Container 也不能超出屏幕大小,这样就忽略了它的 constraints 参数。

示例 10

Center(
   child: ConstrainedBox(
      constraints: BoxConstraints(
         minWidth: 70, 
         minHeight: 70,
         maxWidth: 150, 
         maxHeight: 150,
      ),
      child: Container(color: Colors.red, width: 10, height: 10),
   )    
)

现在,Center 将允许 ConstrainedBox 的大小最大不能超出屏幕。ConstrainedBox 将从其 constraints 参数中为其子项施加额外的约束。

因此,Container 必须介于 70 到 150 像素之间。它希望自己是 10 个像素,所以结果会是 70 像素(最小约束值)。

示例 11

Center(
  child: ConstrainedBox(
     constraints: BoxConstraints(
        minWidth: 70, 
        minHeight: 70,
        maxWidth: 150, 
        maxHeight: 150,
        ),
     child: Container(color: Colors.red, width: 1000, height: 1000),
  )  
)

Center 将允许 ConstrainedBox 的大小最大不能超出屏幕。ConstrainedBox 将从其 constraints 参数中为其子项施加额外的约束。

因此,Container 必须介于 70 到 150 像素之间。它希望自己是 1000 个像素,所以最后会是 150 像素(最大约束值)。

示例 12

Center(
   child: ConstrainedBox(
      constraints: BoxConstraints(
         minWidth: 70, 
         minHeight: 70,
         maxWidth: 150, 
         maxHeight: 150,
      ),
      child: Container(color: Colors.red, width: 100, height: 100),
   ) 
)

Center 将允许 ConstrainedBox 的大小最大不能超出屏幕。ConstrainedBox 将从其 constraints 参数中为其子项施加额外的约束。

因此,Container 必须介于 70 到 150 像素之间。它希望自己是 100 像素,结果就会是这个大小,因为这个值介于 70 到 150 之间。

示例 13

UnconstrainedBox(
   child: Container(color: Colors.red, width: 20, height: 50),
)

屏幕强制 UnconstrainedBox 与屏幕大小完全相同。但是,UnconstrainedBox 允许其 Container 子项自由设定大小。

示例 14

UnconstrainedBox(
   child: Container(color: Colors.red, width: 4000, height: 50),
);

屏幕强制 UnconstrainedBox 与屏幕大小完全相同,UnconstrainedBox 允许 Container 子项自由设定大小。

不幸的是,在这个例子中 Container 的宽度为 4000 像素,因为太大而无法容纳在 UnconstrainedBox 中,因此 UnconstrainedBox 将显示让人胆战心惊的“溢出警告”。

示例 15

OverflowBox(
   minWidth: 0.0,
   minHeight: 0.0,
   maxWidth: double.infinity,
   maxHeight: double.infinity,   
   child: Container(color: Colors.red, width: 4000, height: 50),
);

屏幕强制 OverflowBox 与屏幕大小完全相同,并且 OverflowBox 允许 Container 子项自由设定大小。

这里的的 OverflowBox 与 UnconstrainedBox 相似,不同之处在于,如果子项超出了它的范围,它也不会显示任何警告。

在这个例子中下,Container 的宽度为 4000 像素,因为太大而无法容纳在 OverflowBox 中,但是 OverflowBox 只会显示自己能显示的部分,而不会发出警告。

示例 16

UnconstrainedBox(
   child: Container(
      color: Colors.red, 
      width: double.infinity, 
      height: 100,
   )
)

这不会渲染任何内容,并且你会在控制台中收到错误消息。

UnconstrainedBox 允许其子项自由设定大小,但是其 Container 子项的大小是无限的。

Flutter 无法渲染无限的大小,因此会显示以下错误消息:BoxConstraints forces an infinite width。

示例 17

UnconstrainedBox(
   child: LimitedBox(
      maxWidth: 100,
      child: Container( 
         color: Colors.red,
         width: double.infinity, 
         height: 100,
      )
   )
)

这里你不会再遇到错误,因为当 UnconstrainedBox 为 LimitedBox 赋予一个无限的大小时,后者将向自己的子项传递 100 的宽度上限。

请注意,如果将 UnconstrainedBox 更改为 Center widget,则 LimitedBox 就不会再应用自己的限制(因为其限制仅在约束为无限时才会应用),并且 Container 的宽度将被允许超过 100。

这清楚表明了 LimitedBox 和 ConstrainedBox 之间的区别。

示例 18

FittedBox(
   child: Text('Some Example Text.'),
)

屏幕强制 FittedBox 与屏幕大小完全相同。Text 将有一些自然宽度(也称为其固有宽度),该宽度取决于文本的数量和字体大小等。

FittedBox 将让 Text 自由设定大小,但是在 Text 将其大小告知 FittedBox 之后,FittedBox 会对其进行缩放,使其填满可用宽度。

示例 19

Center(
   child: FittedBox(
      child: Text('Some Example Text.'),
   )
)

但是,如果将 FittedBox 放在 Center 内会怎样?Center 会让 FittedBox 的大小最大不能超出屏幕。

然后,FittedBox 会将其自身调整为 Text 的大小,并让 Text 自由设定大小。由于 FittedBox 和 Text 的大小相同,因此不会发生缩放。

示例 20

Center(
   child: FittedBox(
      child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
   )
)

但是,如果 FittedBox 位于 Center 内部,但 Text 太大而超出了屏幕该怎么办?

FittedBox 将尝试让自己和 Text 一样大,但它不能超出屏幕。然后,它会设定和屏幕大小一样的目标,并调整 Text 的大小以使其也适合屏幕。

示例 21

Center(
   child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
)

但是,如果我们移除 FittedBox,则 Text 将从屏幕获得自己的最大宽度,并且会换行来适合屏幕宽度。

示例 22

FittedBox(
   child: Container(
      height: 20.0, 
      width: double.infinity,
   )
)

注意 FittedBox 只能缩放有界的widget(宽度和高度都不是无限的)。否则,它将无法渲染任何内容,并且你会在控制台中收到错误消息。

示例 23

Row(
   children:[
      Container(color: Colors.red, child: Text('Hello!')),
      Container(color: Colors.green, child: Text('Goodbye!)),
   ]
)

屏幕强制 Row 与屏幕大小完全相同。

就像 UnconstrainedBox 一样,Row 不会对其子项施加任何约束,而是让它们自由设定大小。然后 Row 会将子项并排放置,并且空下剩余的空间。

示例 24

Row(
   children:[
      Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.')),
      Container(color: Colors.green, child: Text('Goodbye!')),
   ]
)

由于 Row 不会对其子项施加任何约束,因此子项可能会太大而超出了可用的 Row 宽度。在这种情况下,就像 UnconstrainedBox 一样,Row 将显示“溢出警告”。

示例 25

Row(
   children:[
      Expanded(
         child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))
      ),
      Container(color: Colors.green, child: Text('Goodbye!')),
   ]
)

当一个 Row 子项包装在一个 Expanded widget 中时,Row 将不再允许该子项定义自己的宽度。

相反,它将根据其他子项定义 Expanded 的宽度,只有这样 Expanded widget 才会强制原始子项的宽度与 Expanded 相同。

换句话说,一旦你使用了 Expanded,原始子项的宽度就不重要了,并且将被忽略。

示例 26

Row(
   children:[
      Expanded(
         child: Container(color: Colors.red, child: Text(‘This is a very long text that won’t fit the line.’)),
      ),
      Expanded(
         child: Container(color: Colors.green, child: Text(‘Goodbye!’),
      ),
   ]
)

如果所有 Row 子项都包装在 Expanded widget 中,则每个 Expanded 的大小将与其 flex 参数成比例,只有这样,每个 Expanded widget 才会强制其子项的宽度等于 Expanded。

换句话说,Expanded 会忽略其子项的首选宽度。

示例 27

Row(children:[
  Flexible(
    child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))),
  Flexible(
    child: Container(color: Colors.green, child: Text(‘Goodbye!’))),
  ]
)

如果使用 Flexible 代替 Expanded,则唯一的区别是 Flexible 将使其子项的宽度小于等于 Flexible 自身,而 Expanded 会强制其子项的宽度和 Expanded 完全相同。

但是,Expanded 和 Flexible 在调整自己的大小时都会忽略自己子项的宽度。

请注意,这意味着我们无法按大小比例扩展 Row 子项。Row 要么使用与子项相同的宽度,或者在使用 Expanded 或 Flexible 时完全忽略子项。

示例 28

Scaffold(
   body: Container(
      color: blue,
      child: Column(
         children: [
            Text('Hello!'),
            Text('Goodbye!'),
         ]
      )))

屏幕会强制 Scaffold 与屏幕完全相同。因此 Scaffold 会填满屏幕。

Scaffold 告诉 Container,后者不能超出屏幕大小。

注意:当 widget 告诉其子项可以小于某个特定大小时,我们说该 widget 为其子项提供了“宽松”的约束。稍后会进一步说明。

示例 29

Scaffold(
   body: SizedBox.expand(
      child: Container(
         color: blue,
         child: Column(
            children: [
               Text('Hello!'),
               Text('Goodbye!'),
            ],
         ))))

如果我们希望 Scaffold 的子项大小与 Scaffold 本身完全相同,则可以将其子项包装到一个 SizedBox.expand 中。

注意:当 widget 告诉其子项必须等于某个大小时,我们说该 widget 为其子项提供了“严格”的约束。

严格×宽松约束

我们经常听到某些约束是“严格”或“宽松”的说法,因此这里讲讲它们的含义。

严格的约束只提供了一种可能性:一个确定的大小。换句话说,严格约束的最大宽度等于其最小宽度,并且其最大高度等于最小高度。

转到 Flutter 的 box.dart 文件并搜索 BoxConstraints 构造器,你会发现以下内容:

BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

再次回顾上面的示例 2,它告诉我们屏幕强制红色的 Container 与屏幕尺寸完全相同。当然,屏幕是将严格的约束传递给 Container 来实现这一点的。

另一方面,宽松的约束可设置最大宽度 / 高度,但允许 widget 自由取小于这个值的大小。换句话说,宽松约束的最小宽度 / 高度都等于

BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;

重新查看示例 3,它告诉我们:Center 让红色的 Container 大小不能大于屏幕。Center 将宽松的约束传递给 Container 来做到这一点。最终,Center 的主要目的是将其从父项(屏幕)获得的严格约束转换为对其子项(Container)的宽松约束。

学习特定 widget 的布局规则

我们需要了解通用的布局规则,但光是这样这还不够。

每个 widget 在应用通用规则时都有很大的自由度,因此只看 widget 的名称是没法知道它会做什么事情的。

如果你只靠猜测的话可能会猜错。除非你已阅读过 widget 的文档或研究了其源代码,否则你无法知道 widget 的确切行为。

布局源码往往是很复杂的,因此最好去看它们的文档。但是如果你决定要研究布局的源码,则可以使用 IDE 的导航功能轻松找到它。

下面是一个示例:

  • 在你的代码中找到一些 Column,然后导航到其源代码(IntelliJ 中按下 Ctrl-B)。你将被带到 basic.dart 文件。由于 Column 扩展了 Flex,因此请导航至 Flex 源代码(也位于 basic.dart 中)。
  • 现在向下滚动,直到找到一个名为 createRenderObject 的方法。如你所见,此方法返回一个 RenderFlex。这是和 Column 对应的渲染对象。现在导航到 RenderFlex 的源代码,IDE 会带你进入 flex.dart 文件。
  • 现在向下滚动,直到找到一个名为 performLayout 的方法。这就是为 Column 布局的方法。

非常感谢 Simon Lightfoot 校对本文,提供标题图片并为本文提供内容建议。

备注:本文已加入 Flutter 官方文档

参考阅读:

https://medium.com/flutter-community/flutter-the-advanced-layout-rule-even-beginners-must-know-edc9516d1a2

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/jMjviktLrJHRiS2WKFyw
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券