Flutter布局指南之深入理解BoxConstraints

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter布局指南之深入理解BoxConstraints相关的知识,希望对你有一定的参考价值。

作者:徐宜生

不管你是android开发,还是Flutter开发,当你开始使用Flutter茫茫多的Widget时,可能会猜测Widget在屏幕上的尺寸和位置,但事实上,你会经历多次错误和失败,Flutter的Widget并不会总是像你想象的那样进行布局。

如果不了解Widget的约束条件是如何应用的,就很难预测Widget的尺寸。很多时候,你根本不知道为什么一个Widget的尺寸比你预期的要大,或者比你想象的要小。因此,在这篇文章中,让我们试着了解约束条件是如何工作的,以及对Widget尺寸的影响。

那么,Flutter中的约束究竟是什么?

Flutter中的约束是对一个Widget的宽度和高度的简单限制

这些限制是通过BoxConstraints对象指定的。在BoxConstraints对象中,尺寸限制被设置为minWidth、maxWidth、minHeight和maxHeight属性。

这4个宽度和高度属性可以有从0到double.infinity的任何数值。double.infinity这个值意味着Widget可以有无限的尺寸。

你可能会遇到有界和无界约束这两个术语。有界意味着有限的约束,即一些特定的尺寸,而无界约束意味着无限的尺寸,即无穷大。

为了设置你想要的约束,你可以使用BoxConstraints构造函数。

BoxConstraints( double minWidth: 0.0, double maxWidth: double.infinity, double minHeight: 0.0, double maxHeight: double.infinity )

现在,在我们开始讨论Flutter中的约束如何工作之前,让我们先理清一些关于约束的重要术语。

Tight constraints、Loose constraints和Unbounded constraints

假设我们有一个Widget,其BoxConstraints的maxWidth和maxHeight分别等于屏幕宽度和屏幕高度。而现在,如果我们想强迫这个Widget填满整个屏幕的宽度和高度,我们必须将Widget的BoxConstraints的minWidth等于屏幕宽度,minHeight等于屏幕高度。所以在这种情况下,当我们通过保持其minWidth、maxWidth等于目标填充宽度,保持其minHeight、maxHeight等于目标填充高度来强制一个Widget填充一个特定的尺寸时,我们说我们已经对该Widget设置了Tight约束。

另一方面,如果我们让Widget在其minWidth到maxWidth,minHeight到maxHeight的范围内拥有任何宽度和高度,那么我们就说我们对Widget设置了Loose约束。

所以现在这意味着为了应用Tight约束,你必须将BoxConstraints的minWidth设置为maxWidth,minHeight设置为maxHeight。你也可以单独为宽度或高度应用Tight约束。我们称其为应用tightWidth或tightHeight。在文章的其余部分,Tight约束一词将指Tight宽度、Tight高度或两者都是。

如果我们将maxWidth、maxHeight或两者都设置为double.infinity,那么我们就说我们在一个widget上设置了Unbounded约束。如果你把minWidth分别设置为double.infinity或0,那么一个Unbounded约束也可以同时是一个Tight束或Loose约束。

我们可以使用BoxConstraints构造函数来设置Tight约束、Loose约束和Unbounded约束。

❝ BoxConstraints.tight( Size size )❞

这将把minWidth, maxWidth设置为size.width,minHeight, maxHeight设置为size.height。因此,现在任何应用了这些约束的Widget都将被强制填充到size.width和size.height的精确尺寸中。

❝ BoxConstraints.tightFor( double width, double height ) ❞

你可以使用这个构造函数并传递宽度或高度来分别设置Tight宽度或Tight高度,或者同时传递宽度和高度来设置两者的Tight约束。这个构造函数有一个变种,叫做BoxConstraints.tightForFinite()。只有当你没有传递无限大的宽度或高度时,才会设置Tight约束。

❝ BoxConstraints.loose( Size size ) ❞

这个构造函数设置了Loose约束,最小宽度和最小高度为0,最大宽度和最大高度为size对象所提供的,也就是说,一个Widget可以在0.0到size.width和0到size.height范围内自由选择任何尺寸。

❝ BoxConstraints.expand() ❞

对传递给它的宽度或高度设置Tight约束,并对未传递给构造函数的宽度或高度参数设置Unbounded约束,即double.infinity。

要设置Unbounded约束,你也可以使用默认的BoxConstraints构造函数,并将maxWidth或maxHeight或两者都设置为double.infinity。

约束的工作原理

让我们举个简单的例子,一个带有Container的MaterialApp,代码如下所示。

runApp()方法将MyAppWidget设置为Root Widget。当framework渲染MyApp时,它在布局过程中被赋予约束,迫使它填满整个屏幕。换句话说,MyApp被赋予了与屏幕宽度和高度相等的尺寸的Tight约束。然后,MyApp在它的孩子MaterialAppWidget上设置约束,而后者又在它的孩子ContainerWidget上设置约束。

在这里,Container从它的父组件MaterialApp收到了关于屏幕尺寸的Tight约束。因此,即使Container被声明为具有100像素的特定宽度和高度,它也被强迫填满整个屏幕。

上面的示例代码是在一个宽度为392.7像素,高度为737.5像素的设备上运行的。(注意:这些是逻辑像素)。下图中突出显示的部分显示了ContainerWidget收到的严格约束,BoxConstraints(w=392.7, h=737.5)和Container的最终尺寸为392.7宽和737.5,同时忽略了它的额外约束w=100.0, h=100.0。你可以在Flutter Visual Inspector -> Widgets下查看这些内容。

现在让我们把Container包在一个Scaffold里面,如下面的代码所示。当我们运行这段代码时,我们会得到尺寸为w=100.0, h=100.0的Container。

那么为什么Container现在改变了它的大小呢?

这是因为Scaffold对Container设置了Loose约束,即使Scaffold本身从它的父级接受了Tight约束。由于Container有Loose约束,它可以自由地选择最小和最大约束之间的任何尺寸,在这种情况下,它的尺寸是0到屏幕尺寸。但是Container本身有额外的约束,宽度为100,高度为100。所以Container选择了100x100,因为它是在Loose约束下。

当约束条件从父代传递到子代时会发生什么?

上面的例子表明,一个父Widget不可能简单地将它收到的约束传递给它的孩子。相反,父Widget可以将其子Widget的约束从Tight变为Loose,反之亦然。如果父Widget设置了Tight约束,那么子Widget别无选择,只能填补其约束中设置的精确尺寸。另一方面,如果父方设置了宽松的约束,那么子Widget就可以自由地选择自己的尺寸,直到最大宽度或最大高度。在Loose约束下,一些Widget占用了允许的最大尺寸,而另一些Widget只占用了它需要的最小尺寸。

这使得Widget的约束行为变得有些复杂,因为现在我们需要了解每个Widget在不同条件下的具体行为。

一个Widget最终可能具有的三种尺寸类型

一般来说,最终的Widget尺寸可能最终成为以下三种尺寸之一。

  • 在Loose约束条件下,它可能变得尽可能大。
  • 在Loose约束条件下,它可能会变得尽可能的小。
  • 在Tight约束下,它可能成为一个特定的尺寸。

那么,如何预测屏幕上最终的Widget尺寸?

好吧,首先,你应该知道在不同的条件下,如Tight约束、Loose约束、Unbounded约束、它有一个孩子或它没有更多的孩子或有多个孩子,特定的Widget会选择上述三个选择中的哪一个。

我们必须了解到每个布局Widget的具体行为。所以最好研究一下Flutter的常见布局组件,了解每个Widget在不同条件下的行为。

这里有一些问题可以帮助您预测Widget的大小。

  • 父Widget是否对其子Widget设置了Tight或Loose约束?
  • 子Widget是否有自己的额外约束。如果是这样,由父和子约束产生的综合约束是什么?
  • 子Widget是否覆盖了父Widget的约束?
  • 如果来自父代和子代的综合约束导致子代Widget有Loose约束,那么我们应该检查子Widget的具体行为,它是否会选择变得尽可能大或尽可能小。
  • 是否有来自父Widget的Unbounded约束,子Widget是否也有相同方向的Unbounded约束?

由于布局组件有自己的特定行为,为了正确预测一个Widget的最终尺寸,我们不仅要注意一般的规则,还要注意布局组件的特定约束规则。

最常用的布局Widget之一是Container。Container作为一个父Widget,对其子Widget传递相同的Loose或Tight约束。

下面是Container在不同条件下的最终尺寸:

案例:Container有无限制的父约束,没有孩子,没有对齐。

❝ Container试图根据它给定的高度和宽度尽可能地缩小尺寸。 ❞

案例:有父约束、自我约束,如特定的高度、宽度,但没有孩子,没有对齐。

❝ Container试图根据它的父约束和它自己的约束所产生的综合约束来确定尽可能小的尺寸。 ❞

案例:有边界的父约束,没有自我约束,没有孩子,没有对齐。

❝ Container扩展以适应父代提供的约束,即Container试图尽可能大的尺寸。 ❞

案例:有无界的父约束,无自我约束,有孩子,有对齐。

❝ Container试图将自己的大小围绕着孩子。 ❞

案例:有边界的父约束,没有自我约束,有孩子,有对齐。

❝ Container试图扩大以适应父体,然后按照排列方式将子体置于自身之内。 ❞

案例:有父约束,无自约束,有子约束

❝ Container将父方的约束传递给子方,并将自己的大小与子方相匹配。 ❞

正如你所看到的,如果不了解最常用的布局Widget的具体行为,要预测一个Widget的最终尺寸并不容易。

如何覆盖父约束并控制子Widget的尺寸

Flutter为我们提供了一些有用的小工具Widget,以覆盖父方对子方传递的约束。

案例:删除父Widget在其子Widget上设置的所有约束条件

❝ 用UnconstrainedBox包住子Widget。 ❞

案例:在父约束边界内为子Widget设置新的尺寸约束

❝ 用SizedBox包裹子Widget。我们还可以使用SizedBox的变体,如FractionallySizedBox来设置子Widget的尺寸为总可用空间的一部分,SizedOverflowBox来设置一个特定的尺寸,并允许子Widget溢出。 ❞

案例:在父Widget设置的约束条件的同时添加额外的约束条件

❝ 用ConstrainedBox包住子Widget ❞

案例:在滚动的父Widget内限制一个子Widget的大小,在其滚动方向上有无限制的约束

❝ 用LimitedBox来包裹子Widget ❞

案例:用新的约束覆盖父级约束,甚至允许孩子溢出父级而没有黑色和黄色的条纹警告

❝ 在一个OverflowBox中包裹子Widget ❞

案例:缩放子Widget

❝ 在一个FittedBox中包裹子Widget ❞

案例:控制行或列Widget内的子Widget尺寸

❝ 将每个子Widget包裹在一个Flexible或Expanded中 ❞

常见的约束问题和解决方案

❝ Error: BoxConstraints forces an infinite width. ❞

如果有任何来自父方的Unbounded约束,并且子方也有相同方向的Unbounded约束,即宽度或高度方向的Unbounded约束,那么它将导致上述的错误。这个错误是针对宽度的。这是因为Flutter不能渲染无限的尺寸。父方或子方都必须设置一个边界,以便框架知道它需要渲染的尺寸。

像ListView这样的滚动Widget在其滚动方向上有Unbounded约束。因此,如果你给它一个在滚动方向上也有Unbounded约束的子对象,那么同样的错误也会产生。为了解决这个错误,可以使用LimitedBox来包裹子Widget。

❝ Error: RenderFlex children have non-zero flex but incoming height constraints are unbounded ❞

这个错误可能发生在像column这样的Flex Widget中,例如,列的父Widget对它设置了Unbounded约束,而这个column中的一个子Widget的高度被设置为double.infinity,即无界高度约束,那么Flutter将出错,因为它无法确定子Widget的确切尺寸。可以通过使用Flexible或Expanded来包装每个子Widget来解决这个问题。

❝ Black and yellow stripes shown on screen overflow ❞

通常情况下,当文本大小或图像大小不适合在父约束中,它们就会溢出。在这种情况下,你可以使用一个FittedBox来解决这个问题。

Column或Row也可能在它们的子代不适合其主轴时溢出。你可以通过使用Flexible或Expanded来包裹每个子Widget来解决这个问题。或者把column或row改成一个Listview。

总结

一般来说,有三种类型的约束。Tight、Loose的和Unbounded约束。

屏幕将Tight约束传递给根Widget,使其与设备屏幕一样大。然后再往后,每个父Widget都会向其子Widget传递约束。

布局Widget有它们自己的特定行为:

  • 当把约束传递给子代时,父代可以把Tight约束改为Loose约束,或者不加改变地传递。
  • Widget的尺寸在不同的条件下可能是不同的。这取决于各种因素,如它的子尺寸、它的父尺寸、它自己的约束、父约束等。
  • 一般来说,一个Widget会尽可能的大,或者尽可能的小,或者一个特定的尺寸。
  • 使用BoxConstraints构造函数设置约束。
  • 我们也可以使用一些Box Widget来覆盖父级约束,如UnconstrainedBox, SizedBox, ConstrainedBox等。
  • 父约束和子约束中存在的无约束约束会导致渲染错误。Flutter不能渲染无限大的尺寸。

本文部分翻译自 https://medium.com/@naresh.idiga/a-deep-dive-into-flutter-constraints-abd3d4c93a6

以上是关于Flutter布局指南之深入理解BoxConstraints的主要内容,如果未能解决你的问题,请参考以下文章

Flutter布局指南之Box套盒子

Flutter布局指南之约束和尺寸

Flutter核心类分析深入理解RenderObject

Flutter核心类分析深入理解RenderObject

Flutter核心类分析深入理解RenderObject

深入JVM之理解JVM内存区域与对象创建内存布局