【译】Flutter | 深刻理解布局约束

原文:Flutter: The Advanced Layout Rule Even Beginners Must Knowhtml

做者:Marcelo Glasberggit

译者:Vadaskigithub

校对:Luke ChengAlexapi

前言

这篇文章最初来自于 Marcelo Glasberg 在 Medium 发表的 Flutter: The Advanced Layout Rule Even Beginners Must Know。后被 Flutter Team 发现并收录到 flutter.devapp

在认真阅读完这篇文章后,我认为它对 Flutter 开发者来讲具备至关的 指导意义,每一位 Flutter 开发都应该认真理解其中的布局约束过程,是很是必要的!所以,在翻译本文的过程当中,咱们对译文反复打磨,尽量保留原文向传递给读者的内容。但愿让每一位看到此文的开发者都可以有所收获。布局

深刻理解布局约束

咱们会常常听到一些开发者在学习 Flutter 时的疑惑:为何我设置了 width:100, 可是看上去却不是 100 像素宽呢。(注意,本文中的“像素”均指的是逻辑像素) 一般你会回答,将这个 Widget 放进 Center 中,对吧?学习

别这么干。字体

若是你这样作了,他们会不断找你询问这样的问题:为何 FittedBox 又不起做用了? 为何 Column 又溢出边界,亦或是 IntrinsicWidth 应该作什么。flex

其实咱们首先应该作的,是告诉他们 Flutter 的布局方式与 HTML 的布局差别至关大 (这些开发者极可能是 Web 开发),而后要让他们熟记这条规则:ui

  • 首先,上层 widget 向下层 widget 传递约束条件。

  • 而后,下层 widget 向上层 widget 传递大小信息。

  • 最后,上层 widget 决定下层 widget 的位置。

若是咱们在开发时没法熟练运用这条规则,在布局时就不能彻底理解其原理,因此越早掌握这条规则越好!

更多细节:

  • Widget 会经过它的 父级 得到自身的约束。 约束实际上就是 4 个浮点类型的集合: 最大/最小宽度,以及最大/最小高度。

  • 而后,这个 widget 将会逐个遍历它的 children 列表。向子级传递 约束(子级之间的约束可能会有所不一样),而后询问它的每个子级须要用于布局的大小。

  • 而后,这个 widget 就会对它子级的 children 逐个进行布局。 (水平方向是 x 轴,竖直是 y 轴)

  • 最后,widget 将会把它的大小信息向上传递至父 widget(包括其原始约束条件)。

例如,若是一个 widget 中包含了一个具备 padding 的 Column, 而且要对 Column 的子 widget 进行以下的布局:

那么谈判将会像这样:

Widget: "嘿!个人父级。个人约束是多少?"

Parent: "你的宽度必须在 80300 像素之间,高度必须在 3085 之间。"

Widget: "嗯...我想要 5 个像素的内边距,这样个人子级能最多拥有 290 个像素宽度和 75 个像素高度。"

Widget: "嘿,个人第一个子级,你的宽度必需要在 0290,长度在 075 之间。"

First child: "OK,那我想要 290 像素的宽度,20 个像素的长度。"

Widget: "嗯...因为我想要将个人第二个子级放在第一个子级下面,因此咱们仅剩 55 个像素的高度给第二个子级了。"

Widget: "嘿,个人第二个子级,你的宽度必需要在 0290,长度在 055 之间。"

Second child: "OK,那我想要 140 像素的宽度,30 个像素的长度。"

Widget: "很好。个人第一个子级将被放在 x: 5 & y: 5 的位置, 而个人第二个子级将在 x: 80 & y: 25 的位置。"

Widget: "嘿,个人父级,我决定个人大小为 300 像素宽度,60 像素高度。"

限制

正如上述所介绍的布局规则中所说的那样, Flutter 的布局引擎有一些重要限制:

  • 一个 widget 仅在其父级给其约束的状况下才能决定自身的大小。 这意味着 widget 一般状况下 不能任意得到其想要的大小

  • 一个 widget 没法知道,也不须要决定其在屏幕中的位置。 由于它的位置是由其父级决定的。

  • 当轮到父级决定其大小和位置的时候,一样的也取决于它自身的父级。 因此,在不考虑整棵树的状况下,几乎不可能精肯定义任何 widget 的大小和位置。

样例

下面的示例由 DartPad 提供,具备良好的交互体验。 使用下面水平滚动条的编号切换 29 个不一样的示例。

你能够在 flutter.cn 上找到该源码。

若是你愿意的话,你还能够在 这个 Github 仓库中 获取其代码。

如下各节将介绍这些示例。

样例 1

Container(color: Colors.red)
复制代码

整个屏幕做为 Container 的父级,而且强制 Container 变成和屏幕同样的大小。

因此这个 Container 充满了整个屏幕,并绘制成红色。

样例 2

Container(width: 100, height: 100, color: Colors.red)
复制代码

红色的 Container 想要变成 100 x 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。 相反,它将会在容许的空间内,把 Container 放在右下角(bottomRight)。

样例 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 没有固定大小可是有子级,因此它决定变成它 child 的大小。

而后红色的 Container 告诉它的 child 能够变成任意大小,可是不能超出屏幕。

而它的 child 是一个想要 30 × 30 大小绿色的 Container。因为红色的 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 带入了约束的计算中。 因此它有一个 30 x 30 的外边距。因为这个外边距,因此如今你能看见红色了。 而绿色的 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 仅对其从其父级接收到的约束下施加其余约束。

在这里,屏幕迫使 ConstrainedBox 与屏幕大小彻底相同, 所以它告诉其子 Widget 也以屏幕大小做为约束, 从而忽略了其 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 达到屏幕可容许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。虽然它但愿本身有 10 个像素大小, 但最终得到了 70 个像素(最小为 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 达到屏幕可容许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。 虽然它但愿本身有 1000 个像素大小, 但最终得到了 150 个像素(最大为 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 达到屏幕可容许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。 虽然它但愿本身有 100 个像素大小, 由于 100 介于 70 至 150 的范围内,因此最终得到了 100 个像素。

样例 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 能够变为任意大小。

不幸的是,在这种状况下,容器的宽度为 4000 像素, 这实在是太大,以致于没法容纳在 UnconstrainedBox 中, 所以 UnconstrainedBox 将显示溢出警告(overflow warning)。

样例 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 容许其子容器设置为任意大小。

OverflowBoxUnconstrainedBox 相似,但不一样的是, 若是其子级超出该空间,它将不会显示任何警告。

在这种状况下,容器的宽度为 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,
      )
   )
)
复制代码

此次你就不会遇到报错了。 UnconstrainedBoxLimitedBox 一个无限的大小; 但它向其子级传递了最大为 100 的约束。

若是你将 UnconstrainedBox 替换为 Center, 则LimitedBox 将再也不应用其限制(由于其限制仅在得到无限约束时才适用), 而且容器的宽度容许超过 100。

上面的样例解释了 LimitedBoxConstrainedBox 之间的区别。

样例 18

FittedBox(
   child: Text('Some Example Text.'),
)
复制代码

屏幕强制 FittedBox 变得和屏幕同样大, 而 Text 则是有一个天然宽度(也被称做 intrinsic 宽度), 它取决于文本数量,字体大小等因素。

FittedBoxText 能够变为任意大小。 可是在 Text 告诉 FittedBox 其大小后, FittedBox 缩放文本直到填满全部可用宽度。

样例 19

Center(
   child: FittedBox(
      child: Text('Some Example Text.'),
   )
)
复制代码

但若是你将 FittedBox 放进 Center widget 中会发生什么? Center 将会让 FittedBox 可以变为任意大小, 取决于屏幕大小。

FittedBox 而后会根据 Text 调整本身的大小, 而后让 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.'),
)
复制代码

然而,若是你删除了 FittedBoxText 则会从屏幕上获取其最大宽度, 并在合适的地方换行。

样例 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 变得和屏幕同样大,因此 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 不会对其子级施加任何约束, 所以它的 children 颇有可能太大 而超出 Row 的可用宽度。在这种状况下, Row 会和 UnconstrainedBox 同样显示溢出警告。

样例 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 将不会再让其决定自身的宽度了。

取而代之的是,Row 会根据全部 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 忽略了其子 Widget 想要的宽度。

样例 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 要么使用子级的宽度, 要么使用ExpandedFlexible 从而忽略子级的宽度。

样例 28

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

屏幕强制 Scaffold 变得和屏幕同样大, 因此 Scaffold 充满屏幕。 而后 Scaffold 告诉 Container 能够变为任意大小, 但不能超出屏幕。

当一个 widget 告诉其子级能够比自身更小的话, 咱们一般称这个 widget 对其子级使用 宽松约束(loose)

样例 29

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

若是你想要 Scaffold 的子级变得和 Scaffold 自己同样大的话, 你能够将这个子级外包裹一个 SizedBox.expand

当一个 widget 告诉它的子级必须变成某个大小的时候, 咱们一般称这个 widget 对其子级使用 严格约束(tight)

严格约束(Tight) vs 宽松约束(loose)

之后你常常会听到一些约束为严格约束或宽松约束, 你花点时间来弄明白它们是值得的。

严格约束给你了一种得到确切大小的选择。 换句话来讲就是,它的最大/最小宽度是一致的,高度也同样。

若是你到 Flutter 的 box.dart 文件中搜索 BoxConstraints 构造器,你会发现如下内容:

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

若是你从新阅读 样例 2, 它告诉咱们屏幕强制 Container 变得和屏幕同样大。 为什么屏幕可以作到这一点, 缘由就是给 Container 传递了严格约束。

一个宽松约束换句话来讲就是设置了最大宽度/高度, 可是让容许其子 widget 得到比它更小的任意大小。 换句话来讲,宽松约束的最小宽度/高度为 0

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 并跟进到它的源代码。 为此,请在 (Android Studio/IntelliJ) 中使用 command+B(macOS)或 control+B(Windows/Linux)。 你将跳到 basic.dart 文件中。因为 Column 扩展了 Flex, 请导航至 Flex 源代码(也位于 basic.dart 中)。

  • 向下滚动直到找到一个名为 createRenderObject() 的方法。 如你所见,此方法返回一个 RenderFlex。 它是 Column 的渲染对象, 如今导航到 flex.dart 文件中的 RenderFlex 的源代码。

  • 向下滚动,直到找到 performLayout() 方法, 由该方法执行列布局。


最后,十分感谢参与校对的程路Alex,以及帮助打磨译文的 CaiJingLong任宇杰孙恺 以上几位同窗,谢谢!

但愿看完这篇文章,可以对你有所收获。若是你遇到任何疑惑,或者想要与我讨论,欢迎在底部评论区一块儿交流,或是经过邮箱与我联系。Happy coding!

中文连接:debug.flutter.cn/docs/develo…

相关文章
相关标签/搜索