阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

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

  • 2020-08-06
  • 本文字数:7546 字

    阅读完需:约 25 分钟

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


2020-08-06 14:182157

评论

发布
暂无评论
发现更多内容
Flutter初学者必读的高级布局规则_大前端_Marcelo Glasberg_InfoQ精选文章