Flutter底层学习:布局

jkouu 143 0

在搭建Flutter的UI过程中,经常会出现“明明我设置了宽是200,为什么最后出来是50?"或者”为什么我的Column经常溢出?“的情况。这其实是因为我们对Flutter的布局策略和流程都还不是很熟悉。

所以,想要快速正确地搭建Flutter的UI,就必须要了解Flutter的布局是怎么来实现的。

1.Flutter的布局策略

Flutter的官网是用这么一句话来概括Flutter的布局策略的:"Constraints go down, sizes go up, parent sets position"。这句话翻译过来就是:”当约束变松时,Widget的尺寸就会变大,Widget的位置由它的父组件决定“。接下来我们就围绕着这句话来讲一下Flutter的布局策略。

首先我们要理解一个概念:约束。在Flutter中,约束指的是父组件对子组件尺寸的一组限制条件,它的组成很简单:minWidth、maxWidth、minHeight、maxHeight。这四个数值描述了父组件对子组件尺寸的限制。当子组件的长或宽小于父组件限制的最小值或者大于父组件限制的最大值时,他们会被强制收敛到父组件限制的最大值或者最小值;当组件的长或宽在父组件限制的范围内时,它们就不会受限制的影响。一般我们用松紧来描述限制的数值范围。当父组件对组件的尺寸没有限制,即范围是(0, +∞)时,我们称这个限制为loose constraint,即松限制;当父组件对组件的限制为一个指定值,即minWidth=maxWidth或minHeight=maxHeight时,我们称这个限制为tight constraint,即紧限制。

由于Flutter的UI是以树的形式来描述的,越靠近叶子节点的Widget,受到的限制范围就越紧,自然它的尺寸就越小了。

下面我们用一个例子来对Flutter的布局流程做一个解释:

Flutter底层学习:布局

上面的图是Flutter官网的一张图。灰色区域是一个Container,这里我们将其叫做Parent。它的child是另外一个Container。这个Cotainer除了有一个大小为5的Padding外,还包括了一个Column。Column中又包括了两个Text。下面是它们的布局流程:

  1. Parent告诉Container:我希望你的宽是300,高是85
  2. Container考虑到自己还要有5的Padding,于是它告诉Column:我希望你的宽不要超过290,高不要超过75
  3. Column先问FIRST CHILD:你的尺寸是多少呢?
  4. FIRST CHILD自己计算了一下:我的宽度是290,高度是20。于是它告诉Column:我的宽度是290,高度是20
  5. Column收到了FIRST CHILD的答复后又问SECOND CHILD:你的尺寸是多少呢?
  6. SECOND CHILD自己计算了一下:我的宽度是140,高度是30。于是它告诉Column:我的宽度是140,高度是30
  7. 收到了SECOND CHILD的回答后,Column已经知道了自己的子组件的所有尺寸,于是它按照代码中自己的对齐方式(在图中是居中对齐)将两个Text布局好。然后它计算了一下自己的尺寸:宽290,高50。于是它告诉Container:我的宽是290,高是50
  8. 收到了Column的回答后,Container也可以将Column布局好了。这是它再计算自己的尺寸:宽300,高60。于是它告诉Parent:我的宽是300,高是60。
  9. Parent在收到了Container的回答后,就会按照自己的布局策略(在图中是顶部对齐)对Container进行布局,最后就得到了图中的布局了。

其实如果我们写过Android,我们会发现,Flutter的布局流程和Android的布局流程是一样的。Flutter中向child传递限制并且询问child的尺寸的步骤就是Android中onMeasure方法的内容;Flutter中布局子组件的步骤就是Android中onLayout方法的内容。

2.常见的传递限制的组件

首先要说的是,Widget树的最顶端,即最外层的组件,尺寸是会被强制设置成屏幕的尺寸的。这很容易理解,所有的限制必须要有一个源头,这个源头就是屏幕尺寸。

2.1.Container

Container是最常用的传递限制的组件。当Container没有child时,它会在满足父组件限制的情况下尽可能大;当Container有child时,它会在包括child的情况下尽可能小。当然,我们可以通过设定width和height属性来改变这一点。

2.2.Align

Align是另外一种比较常见的组件。它只给出了child的布局策略,并没有对child的尺寸做额外的限制。换句话说,Align传递给child的限制,就是Align的父组件传递给Align的限制。

2.3.ConstrainedBox

ConstrainedBox是经常被误会的一个组件,它有一个constraints的属性。这个属性的值是BoxConstrants类(如果你对Container很了解的话,你会发现container也有一个这个属性)。很多人都会觉得这个ConstrainedBox的限制是绝对的,它的优先级大于父组件传递过来的限制。其实仔细想一想我们也知道这是不可能的,因为这不符合逻辑。事实上,ConstrainedBox提供的Constraints是一个额外的限制。child只有在满足了父组件传递下来的限制后,才会去考虑满足ConstrainedBox中constraints给出的额外限制。

2.4.UnconstrainedBox

顾名思义,UnconstrainedBox对child没有任何要求,它也不会将父组件给它的限制传递给child。child可以按照它自己的意愿决定尺寸,并且将尺寸告诉UnconstrainedBox。由于child没有父组件的限制,它的尺寸有可能会导致溢出。

2.5.OverflowBox

OverflowBox和UnconstrainedBox大致相同。主要区别在于,OverflowBox在遇到溢出的情况时,只会展示自己能展示的部分,对于剩下的部分不予处理,因此也不会报告溢出错误。

2.6.LimitedBox

还有一种情况是UnconstrainedBox不能处理的:在Dart中有一个常量叫做double.infinity。这是一个无限大的数值。显然这个值是不能用在UI绘制中的width和height属性上的。所以当UnconstrainedBox的child设置了这个值时,程序根本不会运行——因为这已经不是溢出的那个级别的错误了。

为了解决这个问题,Flutter创建了LimitedBox。它仅对报告回来的double.infinity值生效,将double.infinity值改为自己的width或者height值。

2.7.FittedBox

FittedBox大家都很常用。它在得到child的尺寸后,会根据父组件的限制和自己的尺寸对child按照策略进行缩放。

2.8.Flex

Flex也是我们经常用的一个组件,Row和Column都是Flex的子类。Flex不会对child做任何限制,它会先获取所有child的尺寸,然后再根据所有child的尺寸决定自己的尺寸。显然,这样有可能会导致溢出,因为它没有考虑到父组件的限制。

2.9.Expanded

Expanded经常和Flex一起使用。Expanded会忽略child的某一个方向上的属性值,让其属性值直接变成父组件限制的最大值。鉴于Flex不对child提供限制,这个限制就继续递归变成了Flex的父组件对Flex的限制。

 

发表评论 取消回复
您必须 [登录] 才能发表评论!
分享