Flutter:开发小白也必须知道的高级布局规则

这篇文章是 Flutter 官方文档的一部分,还有 Kirill Matrosov 翻译的俄语版、Suat Özkaya 翻译的土耳其语版和 Saeed Hassankhan 翻译的波斯语版。

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

不对。

如果你这么回答他,他会有层出不穷的问题,比如:为什么某些 FittedBox 不工作了?为什么那个 Column 溢出?或者 IntrinsicWidth 是用来做什么的?

所以,我们应该告诉他们 Flutter 布局与 HTML 布局有很大差异(他最先接触的大概率是 HTML ),让他们记住以下规则。

**

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

**

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

具体来说:

  • 微件从其父元素处获得自己的约束 。一个“约束”是由 4 个 double 值组成的:分别是最小和最大宽度以及最小和最大高度。

  • 然后,微件会遍历自己的子元素(children) 列表。微件会逐个告诉每个子元素它们的约束 (每个子元素的约束可以是不同的),然后询问每个子元素想要设置的大小。

  • 接下来,微件会确定每个子元素位置 (在 x 轴上确定水平位置,在 y 轴上确定垂直位置)。

  • 最后,微件将其自身大小告知父元素(当然这个大小也要符合原始约束)。

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

微件:你好,父元素,我的约束是什么?

父元素:你的宽度必须在 90300 像素之间,高度在 3085 像素之间。

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

微件:你好,第一个子元素,你的宽度必须在 0290 像素之间,高度在 075 像素之间。

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

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

微件:你好第二个子元素,你的宽度必须介于 0290 像素之间,并且高度必须介于 055 像素之间。

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

微件:很好。我将把第一个子元素放在 x: 5y: 5 的位置,将第二个子元素放在 x: 80y: 25 的位置。

微件:你好父元素,我决定将自己的宽度设为 300 像素,高度设为 60 像素。

限制

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

  • 一个微件只能在其父元素赋予的约束内决定其自身的大小,所以微件往往不能自由决定自己的大小

  • 微件不知道、也无法确定自己在屏幕上的位置 ,因为它的位置是由父元素决定的。

  • 由于父元素的大小和位置又取决于上一级父元素,因此只有考虑整个树才能精确定义每个微件的大小和位置。

示例

为获得互动体验:

  • 运行下面的 CodePen(先点击 Run Pen 按钮,然后点击出现在右下方的 Rerun 按钮);

  • 或者运行 DartPad

  • 或者从 GitHub repo 获取最新代码。

https://codepen.io/marcglasberg/embed/ExVmwed?default-tab=&theme-id=

例 1

1_6yLUHp92rQtZDSEv9aBQcA

Container(color: Colors.red)

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

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

例 2

1_6yLUHp92rQtZDSEv9aBQcA

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

红色的 Container 的尺寸不能被设置为 100×100,因为屏幕会强制将其尺寸与屏幕设置成一样。

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

例 3

1_Mwp8fmF4Uce1G6pxuuJNBw

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

屏幕强制将 Center 的尺寸设置成与屏幕一样。因此 Center 将填满整个屏幕。

Center 告诉 Container 可以选择任何尺寸,只要不超出屏幕即可,所以 Container 的尺寸可以是 100×100。

例 4

1_GuTTQKTH8LCB1Ha343NbWQ

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

这个示例与前一个示例的不同之处是用 Align 代替了 Center

Align 还让 Container 自由选择尺寸,但是如果有空白空间,它不会让 Container 居中,而是将 Container 对齐放到可用空间的右下角。

例 5

1_6yLUHp92rQtZDSEv9aBQcA

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

屏幕强制将 Center 设置成与屏幕一样大,因此 Center 将填满整个屏幕。

Center 告诉 Container 可以选择任何尺寸,前提是不能超出屏幕。Container 希望具有无限大的尺寸,但由于这个前提,只能填满屏幕。

例 6

1_6yLUHp92rQtZDSEv9aBQcA

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

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

Center 告诉 Container 可以选择任何尺寸,但不能超出屏幕。由于 Container 没有子元素且没有固定大小,因此它决定选择尽可能大的尺寸,所以填满了屏幕。

但为什么 Container 要这样决定呢?因为这是搭建 Container 微件的开发人员的设计。有可能有其他设计,所以我们需要阅读 Container 文档,了解它在不同情况下的行为方式。

例 7

1_AYOkoZFkYhmmmmQMo3hrRw

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

1_c3fwXjxtfHl34QyG1MU2ng

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

1_6yLUHp92rQtZDSEv9aBQcA

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

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

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

例 10

1_Mwp8fmF4Uce1G6pxuuJNBw

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

1_aZuAYE68PZuUeUBmtI2dNw

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

1_Mwp8fmF4Uce1G6pxuuJNBw

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

1_brAZzN2-S_fDGXiMDUZheQ

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

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

例 14

1_OWv20n8bQInTHZAP1CxMHA

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

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

但是,这个例子中的 Container 的宽度为 4000 像素,宽度过大,所以无法容纳在 UnconstrainedBox 中,因此 UnconstrainedBox 会有“溢出警告”。

例 15

1_OWv20n8bQInTHZAP1CxMHA

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 子元素自由设定大小。

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

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

例 16

1_mpooMmLzFAQfKkQpuF_T_g

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

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

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

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

例 17

1_Mwp8fmF4Uce1G6pxuuJNBw

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

这里不会再收到报错信息,因为当 UnconstrainedBoxLimitedBox 赋予无限的大小时,LimitedBox 会向自己的子元素传递 100 的宽度上限。

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

这清楚表明了 LimitedBoxConstrainedBox 的区别。

例 18

1_zu9CkTZLLcFEzErsxMVwzA

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

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

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

例 19

1_VBIPl_EXOQVCx7LBCBsXDg

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

但是,如果将 FittedBox 放在 Center 内会怎样?Center 会让 FittedBox 选择任何尺寸,但不能超出屏幕。

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

例 20

1_i26QWIAngt0rc6l-AWg1rQ

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

1_OS2d9BAPJ_BTNB91ZEoqfg

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

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

例 22

1_mpooMmLzFAQfKkQpuF_T_g

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

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

例 23

1_eFm7Ka9zwK1rAmyoQc1IkA

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

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

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

例 24

1_kdgCHYEMJ5P8VfU60jLBnQ

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

1_ljuvetVvLljfy_try-bETQ

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 微件中时,Row 将不再允许该子元素定义自己的宽度。

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

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

例 26

1_ljuvetVvLljfy_try-bETQ

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 微件中,则每个 Expanded 的大小将与其 flex 参数成比例,只有这样,每个 Expanded 微件才会强制其子元素的宽度等于 Expanded

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

例 27

1_ljuvetVvLljfy_try-bETQ

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 完全相同

但是,ExpandedFlexible 在调整自己的大小时都会忽略子元素的宽度。

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

例 28

1_V3mGIoK_py3zWf_eZkKxzg

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

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

Scaffold 告诉 Container 可以选择任何尺寸,但不能超出屏幕大小。

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

例 29

1_X_eWxGnCsvIkXlFBtLskyg

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

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

注意:当微件告诉其子元素必须设为某个特定尺寸时,我们说该微件为其子元素提供了“严格”的约束。

严格/松散的约束

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

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

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

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

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

相反,宽松的约束可设置最大宽度 / 高度,但允许微件自由选择小于这个值的尺寸,即宽松约束的最小宽度 / 高度都等于

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

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

学习特定微件的布局规则

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

每个微件在应用通用规则时都有很大的自由度,因此只看微件的名称是没法知道它的确切功能。

只靠猜测的话可能会猜错,所以要阅读微件的文档,或者研究其源代码,来了解微件的确切行为。

布局源码往往非常复杂,所以看源码的文档也很重要。但如果你决定要研究布局的源码,可以使用 IDE 的导航功能轻松找到它。

下面是一个示例:

  • 在你的代码中找一些 Column,然后找到其源代码(IntelliJ 中按下 Ctrl-B)。你会被带到 basic.dart 文件,由于 Column 扩展了 Flex,因此请找到 Flex 源代码(也在 basic.dart 中)。

  • 向下滚动鼠标,找到一个名为 createRenderObject 的方法,此方法返回一个 RenderFlex,这是和 Column 对应的渲染对象。然后找到 RenderFlex 的源代码,进入 flex.dart 文件。

  • 接着滚动鼠标,找到一个名为 performLayout 的方法,这就是布局 Column 的方法。

Simon Lightfoot 校对了本文,提供了本文的标题图片,还对本文的内容提出了宝贵的建议,特此感谢!

原文作者 Marcelo Glasberg
原文链接 https://medium.com/flutter-community/flutter-the-advanced-layout-rule-even-beginners-must-know-edc9516d1a2

推荐阅读
相关专栏
前端与跨平台
90 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。