Flutter:初学者必须知道的高级布局规则

更新:这篇文章现在是Flutter官方文档的一部分。它还被Kirill Matrosov翻译成了俄语,还被Fabricio Ernandes翻译成了葡萄牙语。

当学习Flutter的人问你,为什么一些宽度为100的部件像素不是100的宽度,默认的答案是告诉他们把这个部件放在一个 中心 里面,对吗?

不要这样做。

如果你这样做了,问题就会出个不停,问为什么某个FittedBox不工作了,为什么那个Column溢出了,或者IntrinsicWidth应该怎么做。

相反,首先告诉他们Flutter布局与HTML布局有很大的不同(这可能是他们的来历),然后让他们记住以下规则。

:point_right: 制约因素减少了 尺寸上升 位置是由父体设定的。

如果不知道这个规则,就无法真正理解Flutter布局,所以我认为每个人都应该尽早学习。

更详细请看:

  • 一个小组件从它的父体那里获得它自己的约束。一个 "约束 "只是一组4个双数:一个最小和最大的宽度,以及一个最小和最大的高度。

  • 然后,该组件会浏览它的子列表,并分别告诉子列表的约束是什么(每个子列表的约束可能是不同的),然后问每个子列表要成为的尺寸。

  • 然后,小组件逐一 定位 它的子列表(水平方向在X轴,垂直方向在Y轴)。

  • 最后,小组件告诉父体它自己的尺寸(当然是在原来的约束条件下)。

例如,如果一个组件像一个有一些填充物的柱子,并希望布局它的两个孩子。

部件 - 嘿,父体,我的限制是什么?
父体 -你必须有90到300像素的宽度,30到85的高度。
部件–嗯,因为我想有5个像素的填充,那么我的孩子最多可以有290像素的宽度和75像素的高度。
部件 - 嘿,第一个孩子,你必须有0到290像素的宽度,0到75的高度。
第一个孩子 - 好的,那么我理想宽度为290像素,高度为20像素。
部件 - 嗯,因为我想把我的第二个孩子放在第一个孩子的下面,这就只给我的第二个孩子留下了55像素的高度。
小工具 - 嘿,第二个孩子,你的宽必须在0到290之间,高在0到55之间。
第二个孩子 - 好的,我希望宽度为140像素,高度为30像素。
部件–很好。我将把我的第一个孩子放到x: 5和y: 5,第二个孩子放在x:80和y:25的位置。
部件 - 嘿,父体,我已经决定我的尺寸将是300像素宽,60像素高。

局限性

由于上述的布局规则,Flutter的布局引擎有以下几个重要的限制。

  • 一个部件只能在其父体给它的约束条件下决定自己的尺寸。这意味着一个小组件的尺寸是固定的。
  • 一个部件不能决定自己在屏幕中的位置。
  • 由于父体的大小和位置反过来也取决于它自己的父对象,如果不考虑整个树,就不可能精确定义任何部件的大小和位置。

实例

为了获得互动体验。

  • 运行下面的CodePen(你必须先点击Run Pen按钮,然后点击出现在右下方的Rerun按钮);
  • 或者,运行这个DartPad
  • 或者从这个GitHub repo获取最新的代码。

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

例一

1_6yLUHp92rQtZDSEv9aBQcA

Container(color: Colors.red)

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

所以Container填满了屏幕,它就变成了全红。

Example 2

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

The red Container wants to be 100 × 100, but it can’t, because the screen forces it to be exactly the same size of the screen.

So the Container fills the screen.

例二
1_6yLUHp92rQtZDSEv9aBQcA

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

红色的Container想成为100×100,但它不能,因为屏幕迫使它与屏幕的大小完全相同。

所以这个容器就填满了屏幕。

例三

1_Mwp8fmF4Uce1G6pxuuJNBw

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

屏幕迫使 "中心 "与屏幕的大小完全相同。所以,中心就填满了屏幕。

中心告诉Container,它可以是任何它想要的尺寸,但不能大于屏幕的尺寸。现在Container确实可以是100×100。

例四

1_GuTTQKTH8LCB1Ha343NbWQ

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

这与前面的例子不同,因为它使用了Align而不是Center。

Align也告诉Container可以是它想要的任何尺寸,但如果有空的空间,它不会将Container居中,而是将其对齐到可用空间的右下方。

例五

1_6yLUHp92rQtZDSEv9aBQcA

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

屏幕迫使 "中心 "与屏幕的大小完全相同。所以,中心就填满了屏幕。

中心告诉Container,它可以是任何它想要的大小,但不能比屏幕大。就算Container想要无限大,由于它不能比屏幕大,所以它只能填充屏幕。

例六

1_6yLUHp92rQtZDSEv9aBQcA

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

屏幕迫使 "中心 "与屏幕的大小完全相同。所以,中心就填满了屏幕。

中心告诉Container,它可以自由地成为它想要的任何尺寸,但不能大于屏幕。由于Container没有子列表,也没有固定的尺寸,它决定自己要尽可能大,所以它适合整个屏幕。

但为什么Container会这样决定呢?很简单,因为这是创建Container小组件的人的一个设计决定。它可以以不同的方式创建,实际上你必须阅读Container的文档来了解它在不同情况下会做什么。

例七

1_AYOkoZFkYhmmmmQMo3hrRw

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

屏幕迫使 "中心 "与屏幕的大小完全相同。所以中心点就填满了屏幕。

中心告诉红色Container,它可以是任何它想要的大小,但不能比屏幕大。由于红色容器没有大小,但有一个子体,它决定要和它的子体一样大。

红色Container告诉它的子体,它可以是任何它想要的大小,但不能比屏幕大。

这个子体恰好是一个绿色的Container,它希望是30×30。如前所述,红色Container将根据其子代的大小调整自己的大小,所以它也将是30×30。没有红色是可见的,因为绿色Container将占据红色容器的所有位置。

例八

1_c3fwXjxtfHl34QyG1MU2ng

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

红色的Container将根据子体的大小来确定自己的大小,但它会考虑到自己的填充。因此,它将是70×70(=30×30加上所有边上的20像素的填充)。由于填充的原因,红色将是可见的,而绿色的Container将有与前面的例子相同的尺寸。

例九

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也假定为屏幕的尺寸,从而忽略其约束参数。

例十

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将从其约束参数中给它的子体施加额外的约束。

所以Container必须在70到150像素之间。它想有10个像素,所以它最终会有70个(最小值)。

例十一

1_aZuAYE68PZuUeUBmtI2dNw

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

中心将允许ConstrainedBox成为任何尺寸,直至屏幕尺寸。ConstrainedBox会给它的子体施加来自其约束参数的额外约束。

所以Container必须在70到150像素之间。理想中是越大越好,所以它最终会有150个(最大)。

例十二

1_Mwp8fmF4Uce1G6pxuuJNBw

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

中心将允许ConstrainedBox成为任何尺寸,直至屏幕尺寸。ConstrainedBox会给它的子体施加来自其约束参数的额外约束。

所以Container必须在70到150像素之间。它想拥有100像素,这就是它将拥有的尺寸,因为这是在70和150之间。

例十三

1_brAZzN2-S_fDGXiMDUZheQ

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

屏幕迫使UnconstrainedBox的大小与屏幕完全相同。然而,UnconstrainedBox允许它的Container子体拥有它想要的任何尺寸。

例十四

1_OWv20n8bQInTHZAP1CxMHA

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

屏幕迫使UnconstrainedBox的尺寸与屏幕完全相同,而UnconstrainedBox让它的Container子代拥有它想要的任何尺寸。

不幸的是,在这种情况下,Container有4000像素的宽度, 这太大了,无法装入UnconstrainedBox,所以UnconstrainedBox会显示可怕的 “溢出警告”。

例十五

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子体拥有任何它想要的大小。

OverflowBox,像这样使用,类似于UnconstrainedBox,不同的是,如果子体不适合空间,它不会显示任何警告。

在这个例子中,Container有4000像素的宽度,太大了,无法装入OverflowBox,但OverflowBox会简单地显示它能显示的东西,不给警告。

例十六

1_mpooMmLzFAQfKkQpuF_T_g

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

这不会渲染任何东西,而且你会在控制台中得到一个错误。

UnconstrainedBox可以让它的子体拥有它想要的任何尺寸,然而它的子体是一个具有无限尺寸的Container。

Flutter不能渲染无限大的尺寸,所以它会抛出一个错误,信息如下。BoxConstraints强制要求无限的宽度。

例十七

1_Mwp8fmF4Uce1G6pxuuJNBw

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

在这里你不会再得到一个错误,因为当LimitedBox被UnconstrainedBox赋予无限的尺寸时,它将把最大宽度100传递给它的子体。

注意,如果你把UnconstrainedBox改为Center widget,LimitedBox将不再应用它的限制(因为它的限制只在它得到无限的约束时应用),容器的宽度将被允许增长到100以上。

这就明确了LimitedBox和ConstrainedBox之间的区别。

例十八

1_zu9CkTZLLcFEzErsxMVwzA

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

屏幕迫使FittedBox的大小与屏幕完全相同。文本会有一些自然的宽度(也叫内在宽度),这取决于文本的数量、字体大小等。

FittedBox会让文本拥有它想要的任何尺寸,但在文本告诉FittedBox它的尺寸后,FittedBox会缩放它,直到它填满所有的可用宽度。

例十九

1_VBIPl_EXOQVCx7LBCBsXDg

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

但是,如果我们把FittedBox放在Center里面会怎么样呢?中心会让FittedBox拥有它想要的任何尺寸,直到屏幕尺寸。

然后,FittedBox将根据文本的大小调整自己的尺寸,并让文本拥有它想要的任何尺寸。由于FittedBox和Text都有相同的尺寸,所以不会发生缩放。

例二十

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将尝试根据文本的大小调整自己的尺寸,但它不可能比屏幕大。然后,它将假定屏幕的大小,并调整文本的大小,使其也适合屏幕。

例二十一

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.'),
)

例二十二

1_mpooMmLzFAQfKkQpuF_T_g

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

注意FittedBox只能缩放一个有边界的部件(具有非无限的宽度和高度)。否则,它不会渲染任何东西,你会在控制台得到一个错误结果。

例二十三

1_eFm7Ka9zwK1rAmyoQc1IkA

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

屏幕迫使行的大小与屏幕完全相同。

就像UnconstrainedBox一样,Row不会对它的子节点施加任何约束,而是让它们拥有任何想要的尺寸。然后,Row会把它们并排放在一起,任何多余的空间都会保持空旷。

例二十四

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!')),
   ]
)

由于该行不会对其子节点施加任何约束,所以很有可能子节点太大,无法适应该行的可用宽度。在这种情况下,就像UnconstrainedBox一样,该行将显示 “溢出警告”。

例二十五

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!')),
   ]
)

当一个行的子体被包裹在一个扩展的部件中时,该行将不再让这个子体定义它自己的宽度。

相反,它将根据其他的子体来定义 "扩展 "的宽度。

而只有在这时,"扩展 "部件才会强迫原来的子体拥有 "扩展 "的宽度。

换句话说,一旦你使用了Expanded,原来的子体的宽度就变得无关紧要,并且会被忽略。

例二十六

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!’),
      ),
   ]
)

如果所有行的子体都被包裹在 "扩展 "部件中,每个 "扩展 "将有一个与它的flex参数成比例的尺寸,只有这样,每个 "扩展 "部件才会强迫他们的子体有 "扩展 "的宽度。

换句话说,Expanded忽略了他们的子体的首选宽度。

例二十七

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!’))),
  ]
)

如果你使用 "灵活 "而不是 “扩展”,唯一的区别是 "灵活 "会让它的子体拥有与 "灵活 "本身相同或更小的宽度,而 "扩展 "则迫使它的孩子拥有与 "扩展 "完全相同的宽度。

但 "扩展 "和 "灵活 "在确定自己的尺寸时都会忽略其子体的宽度。

注意,这意味着不可能按比例扩展行的子项的大小。当你使用 "扩展 "或 "灵活 "时,该行要么使用确切的子项,要么完全忽略它。

无耻的插件:我的软件包assorted_layout_widgets有一个特殊的行部件,它将按比例调整其单元格的大小,以适应每个孩子的宽度。

例二十八

1_V3mGIoK_py3zWf_eZkKxzg

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

屏幕迫使Scaffold与屏幕的大小完全相同。所以脚手架填满了屏幕。

Scaffold告诉Container它可以是任何它想要的大小,但不能比屏幕大。

注意:当一个部件告诉它的子部件它可以小于某个尺寸时,我们说这个部件给它的子部件提供了 "松散的 "约束。稍后会有更多关于这个的内容。

例二十九

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,它告诉我们,屏幕强迫红色的Container与屏幕的大小完全一致。当然,屏幕是通过向Container传递严格的约束来做到这一点的。

另一方面,一个松散的Container设置了最大的宽度/高度,但允许小部件随心所欲地变小。换句话说,一个宽松约束的最小宽度/高度都等于零:

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

如果你重温一下例3,它告诉我们,中心让红色的Container比屏幕小,但不比屏幕大。当然,"中心 "是通过向 "Container "传递松散的约束来做到这一点的。最终,"中心 "的目的是把它从父体(屏幕)得到的严格约束转化为子体(容器)的宽松约束。

学习特定部件的布局规则

了解一般的布局规则是必要的,但这是不够的。

每个小组件在应用一般规则时都有很大的自由度,所以仅仅通过阅读小组件的名称,是无法知道它将做什么的。

如果你试图猜测,你可能会猜错。除非你读了它的文档,或者研究了它的源代码,否则你不可能确切地知道一个小组件会有什么表现。

布局源码通常很复杂,所以最好只阅读文档。然而,如果你决定研究布局源码,你可以通过使用IDE的导航功能轻松找到它。

具体如下:

  • 在你的代码中找到一些Column并导航到它的源代码(在IntelliJ中按Ctrl-B)。你会被带到basic.dart文件。由于Column扩展了Flex,导航到Flex源代码(也在basic.dart)。
  • 现在向下滚动,直到你找到一个叫做createRenderObject的方法。你可以看到,这个方法返回一个RenderFlex。这是对应于柱子的渲染对象。现在导航到RenderFlex的源代码,这将带你到flex.dart文件。
  • 现在向下滚动,直到你找到一个叫做 performLayout 的方法。这是一个为Column进行布局的方法。


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

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