Flutter你竟是这样的布局

3,990 阅读15分钟

对于Flutter学习者来说,掌握Flutter的布局行为,直接决定了开发者在布局的时候是否能做到高效、快速的开发,但是初学者面对茫茫多的Widget以及各种无法预料的布局行为,总是很难将心中所想,转化为Flutter的代码。

本文翻译整理自https://flutter.dev/docs/development/ui/layout/constraints


顺便插句话,我的开源项目Flutter_dojo,刚发布了2.0,欢迎大家体验。 Flutter_dojo

在这里插入图片描述
欢迎大家体验。


当学习Flutter的人问你,为什么宽度为100的某些小部件在显示的时候,宽度不为100像素时,你的默认答案是告诉他们将小部件放在Center内,对吗?

不要这样做。如果这样做,他们会一次又一次地回来,询问为什么某些FittedBox不起作用,为什么Column溢出了,或者IntrinsicWidth应该做什么。

相反,请先告诉他们Flutter布局与HTML布局(可能是他们非常熟悉的)有很大不同,然后让他们记住以下规则:

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

如果不了解此规则,就无法真正理解Flutter的布局,因此Flutter开发人员应尽早学习。

更详细地: Widget从其父级获得自己的约束。约束只是一组4个双精度数:

  • 最小和最大宽度
  • 最小和最大高度

然后Widget遍历它的所有子Widget。Widget一个接一个地告诉其孩子约束(每个孩子可能有所不同),然后询问每个孩子想要的大小,然后,Widget将其孩子定位(水平地在x轴上布局,垂直地在y轴上布局),最后,该小部件将其自身的大小告诉父级(当然,在原始约束内)。

例如,如果一个组合Widget包含带有一些Padding和Column,并且希望如图所示布置其两个Widget:

谈判是这样的:

  • Widget:嗨,Parent,我的约束是什么?
  • Parent Widget:你的宽度必须在80到300像素之间,而高度必须在30到85像素之间。
  • Widget:嗯,因为我要有5像素的Padding,所以我的子Widget最多可以有290像素的宽度和75像素的高度。
  • Widget:嗨,第一个子Widget,你的宽度必须在0到290像素之间,并且必须在0到75高之间。
  • First child:好,那我希望宽290像素,高20像素。
  • Widget:嗯,由于我想将第二个子Widget放到第一个子Widget下面,所以第二个子Widget只剩下55像素的高度。
  • Widget:嗨,第二个子Widget,你的高度必须在0到290之间,并且必须在0到55高之间。
  • Second child:好吧,我希望宽140像素,高30像素。
  • Widget:很好。我的第一个孩子的位置x:5和y:5,第二个孩子的位置x:80和y:25。
  • Widget:亲爱的父母,我决定将尺寸设为300像素宽,60像素高。

Limitations

由于上述布局规则,Flutter的布局引擎具有一些重要限制:

  • Widget只能在其父级赋予的限制内决定其自身大小。这意味着Widget通常不能具有所需的任何大小。布局是自上而下,当前widget会有基本的一些约束(来自它的父元素),主要是关于宽高的最小值和最大值
  • Widget无法知道也不决定其在屏幕上的位置,因为Widget的父级决定小部件的位置。它会依次询问子元素关于布局的基本限制要求,让子元素上报期望的布局结果,然后根据现状和自己布局算法的特点,告诉子元素应该放到那儿,占多大空间

由于父级的大小和位置又取决于其父级,因此在不考虑整个树的情况下就无法精确定义任何小部件的大小和位置。

每个widget不一定会得到它期望的布局大小,这方面显著的例子是ConstrainedBox,很容易让人困惑。 每个widget不能决定在屏幕中的位置,由父元素决定 因为这种布局逻辑需要层层考虑上层元素,所以一个元素的最终布局需要考虑整个UI里widget树。 如果为了精确局部布局,Container和ConstrainedBox会是一个可行的修饰布局。

Examples

下面的29个示例,将演示Flutter的布局思想。

github.com/marcglasber…

Example 1

在这里插入图片描述

Container(color: Colors.red)

屏幕是Container的父级,它强制容器与屏幕的尺寸完全相同。 因此,容器将屏幕填满并涂成红色。

Example 2

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

想要红色的容器为100×100,但不是,因为屏幕会强制使其尺寸与屏幕完全相同。 因此,容器充满了屏幕。

Example 3

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

屏幕会强制Center与屏幕完全相同,因此Center会填满整个屏幕。 Center告诉Container它可以是所需的任何大小,但不能大于屏幕大小。 所以现在容器确实可以是100×100。

Example 4

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

这与上一个示例不同,因为它使用Align而不是Center。 Align同样告诉Container它可以是任何所需的大小,同时会在剩余的可用空间中bottom-right对齐。

Example 5

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

屏幕会强制Center与屏幕完全相同,因此Center会填满整个屏幕。 Center告诉Container它可以是所需的任何大小,但不能大于屏幕大小。 容器希望具有无限大小,但由于不能大于屏幕,因此只能填充屏幕。

Example 6

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

屏幕会强制Center与屏幕完全相同,因此Center会填满整个屏幕。 Center告诉Container它可以是所需的任何大小,但不能大于屏幕大小。 由于该Container没有Child且没有固定的大小,因此它决定要尽可能大,因此将其填满整个屏幕。 但是Container为什么要这样决定呢?仅仅是因为这是创建Container的人的设计决定。 其它的Widget的创建方式可能有所不同,具体取决于情况。

Example 7

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

屏幕会强制Center与屏幕完全相同,因此Center会填满整个屏幕。 Center告诉红色Container它可以是所需的任何大小,但不大于屏幕。 由于红色的Container没有大小,但是有一个Child,因此它决定要与孩子的大小相同。 红色的Container告诉其子项可以是它想要的任何大小,但不能大于屏幕大小。 这个Child是一个绿色的Container,它希望是30×30。考虑到红色Container的大小与其孩子的大小相同,它也是30×30,所以红色是不可见的,因为绿色的Container会完全覆盖红色Container。

Example 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×30加上padding。 由于有padding,因此可以看到红色,绿色Container与上一个示例中的大小相同。

Example 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也假定屏幕大小,从而忽略了其约束参数。

Example 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将来自其约束参数的附加约束施加到其子对象上。 Container必须介于70到150像素之间。 它希望有10个像素,所以最终有70个像素(最小)。

Example 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将来自其约束参数的附加约束施加到其子对象上。 Container必须介于70到150像素之间。 它希望有1000个像素,所以最终有150个(最大)。

Example 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将来自其约束参数的附加约束施加到其子对象上。 Container必须介于70到150像素之间。它希望有100像素,这就是它的大小,因为它介于70到150之间。

Example 13

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

屏幕强制UnconstrainedBox与屏幕大小完全相同。 但是,UnconstrainedBox允许其子Container设置任意大小。

Example 14

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

屏幕强制UnconstrainedBox与屏幕大小完全相同,UnconstrainedBox将其子Container设为任意大小。 不幸的是,在这种情况下,容器的宽度为4000像素,太大而无法容纳在UnconstrainedBox中,因此UnconstrainedBox显示溢出警告。

Example 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允许其子容器设置为任意大小。 OverflowBox与UnconstrainedBox类似,但不同的是,如果Child不适合该空间,它将不会显示任何警告。 在这种情况下,容器的宽度为4000像素,并且太大而无法容纳在OverflowBox中,但是OverflowBox会尽可能地显示尽可能多的内容,而不会发出警告。

Example 16

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

你会在控制台中看到错误。 UnconstrainedBox可以让它的子Widget具有所需的任何大小,但是其子Widget是一个具有无限大小的Container。 Flutter无法呈现无限大小,因此会出现以下错误消息:BoxConstraints forces an infinite width.

Example 17

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

这样就不会再出现错误,因为当UnconstrainedBox为LimitedBox赋予无限大小时,它向下传递的约束为最大宽度是100像素。 如果你将UnconstrainedBox替换为Center,则LimitedBox将不再应用其限制(因为其限制仅在获得无限约束时才适用),并且容器的宽度允许超过100。 这解释了LimitedBox和ConstrainedBox之间的区别。

Example 18

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

屏幕将强制FittedBox与屏幕完全相同。 文本将根据宽度调整自有的宽度属性,字体属性等。 FittedBox允许文本的尺寸为任意大小,但在将文本告知FittedBox大小后,FittedBox缩放文本直到填满所有可用宽度。

Example 19

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

但是,如果将FittedBox放在Center内会怎样?Center会将FittedBox设置为所需的任何大小,直至屏幕大小。 然后,将FittedBox调整为Text大小,并让Text为所需的任何大小。 由于FittedBox和Text具有相同的大小,因此不会发生缩放。

Example 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中,但文本太大而无法容纳屏幕,会发生什么? FittedBox会尝试根据文本大小调整大小,但不能大于屏幕大小。然后假定屏幕大小,并调整文本的大小以使其也适合屏幕。

Example 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从屏幕上获取其最大宽度,并在合适 的地方换行。

Example 22

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

FittedBox只能在有限制的宽高中进行Child的缩放(宽度和高度非无限大)。 否则,它将无法呈现任何内容,并且你会在控制台中看到错误。

Example 23

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

屏幕强制行与屏幕大小完全相同。 就像UnconstrainedBox一样,Row不会对其子代施加任何约束,而是让它们成为所需的任意大小。Row然后将它们并排放置,任何多余的空间都将保持空白。

Example 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不会对其子级施加任何约束,因此子Widget很有可能太大而无法容纳Row的可用宽度。 在这种情况下,就像UnconstrainedBox一样,Row会显示溢出警告。

Example 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的子Child被包裹在Expanded中时,Row将不再让该Child定义自己的宽度。 取而代之的是,Row会根据所有Expanded的Child来计算其该有的宽度。 换句话说,一旦您使用Expanded,原始Widget的宽度就变得无关紧要,并且会被忽略。

Example 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的子Widget都包装在Expeded中,则每个Expeded的大小均与其flex参数成比例,子Child会设置为计算的Expanded宽度。 换句话说,Expanded忽略了其子Widget宽度。

Example 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使其子元素的宽度等于或小于其自身的宽度,而Expanded强制其子元素具有与Expeded完全相同的宽度。 但是,在调整尺寸时,Expanded和Flexible的都忽略了孩子的宽度。

注意:这意味着,Row要么使用子Child的宽度,要么使用Expanded和Flexible从而忽略Child的宽度。

Example 28

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

屏幕会强制设置Scaffold与屏幕大小完全相同,因此Scaffold会填满屏幕。 Scaffold告诉容器它可以是所需的任何大小,但不能大于屏幕大小。

注意:当Widget告诉其子Widget它可以小于特定大小时,我们说该Widget为其Child提供了loose约束。

Example 29

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

如果你希望Scaffold的子Widget与自己的Scaffold大小完全相同,则可以使用SizedBox.expand包装其Child。

注意:当小部件告诉其子级必须具有一定大小时,我们说该小部件为其子级提供了tight约束。

Tight vs loose constraints

前面经常提到一些约束是tight或loose,所以你值得知道这是什么意思。

tight constraint提供了一种可能性,即确切的大小。换句话说,tight constraint的最大宽度等于其最小宽度。 并且其最大高度等于其最小高度。

如果转到Flutter的box.dart文件并搜索BoxConstraints构造函数,则会发现以下内容:

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

如果你重新查看上面的示例2,它将告诉我们屏幕强制红色Container与屏幕完全相同。 当然,屏幕是通过将tight constraint传递给Container来实现的。

另一方面,宽松的约束设置了最大宽度和高度,但使小部件尽可能小。 换句话说,宽松约束的最小宽度和高度都等于零:

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

如果您重新查看示例3,它将告诉我们Center使红色Container变得更小,但不大于屏幕。 Center通过向Container传递loose constraint来做到这一点。 最终,Center的主要目的是将其从父级(屏幕)获得的tight constraint转换为对其子级(容器)的loose constraint。

Learning the layout rules for specific widgets

知道一般的布局规则是必要的,但这还不够。 每个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的渲染对象。现在导航到RenderFlex的源代码,将您带到flex.dart文件。 向下滚动,直到找到一个名为performLayout()的方法。 这是执行列布局的方法。

对Flutter感兴趣的朋友可以加入我的Flutter修仙群。

在这里插入图片描述