理解Flutter Constraints

image.png

如果有人问你Flutter中某个widget设置了属性width: 100,但是显示的并不是100像素。通常最直接的答案就是把这个widget放到 Center 中。但这样真的对吗?
不要这样回答!
如果这样回答,那么后续别人会不停地来问为什么FittedBox 表现不正常,Column 为什么显示overflow了,或者IntrinsicWidth 是干什么用的?

正确的答案是,首先告诉他们Flutter的布局方式和HTML不同(如果对方碰巧是前端开发),然后告诉他们记住下面的规则:

约束向下传递。尺寸向上传递。父控件设置位置

理解上面这个规则是理解Flutter布局的关键,Flutter开发者应当尽早的理解上述规则。
详细说明:

  • widget接收来自父widget的约束。约束由4个double组成:最大、最小宽度和最大、最小高度。
  • widget遍历自己的子widget。将每个相应的约束传递给相应的子widget,然后询问每个子widget本身想要的大小。
  • 然后,widget通过x,y坐标将子widget放到布局中
  • 最后,widget告诉他的父widget自身的大小(当然,还有开始的布局约束)

举例,如果一个widget组合包含一个有padding的column里面有两个child:


image.png

那么整个布局和尺寸的确定流程如下:

  • Column:“Hey,父widget,我的约束是多少?”
  • Column的父Widget: “你的宽度是0-300像素,高度是0-85像素”
  • Column:“我自身有5个像素的padding,那么我的child最多可以有290像素宽,75像素高”
  • Column:“First Child,你的约束是0-290像素宽,0-75像素高”
  • First Child:“好的,我自己想要的size是290像素宽,20像素高”
  • Column:“我还有一个child,那么他的约束是0-290像素宽,55像素高(75-20 =55)”
  • Column:“Second Child, 你的约束是0-290像素宽,0-55像素高”
  • Second Child:“OK,我自己想要的size是140像素宽,30像素高”
  • Column:“好的,那么我的First Child的位置是x:5,y:5 ,我的Second Child的位置是x:80,y:25
  • Column:"我的父Widget,我确定了自身的大小是300像素宽,60像素高"

限制

Flutter的布局引擎被设计成单次完成布局。虽然这种方法效率非常高,但是也会存在一些限制:

  • Widget只能根据父Widget给出的约束来确定自身的大小。这会导致widget无法完全按自身的逻辑设置自身的大小
  • Widget无法知道也无法决定自身在屏幕中的位置,因为只有父widget才能确定子widget的位置。
  • 由于父Widget也依赖父父Widget确定自身的大小和位置,只有在遍历整个布局树后才能精确获取Widget的大小和位置。
  • 如果子Widget的size和父Widget的size不同,父Widget又没有足够的信息来对齐子Widget,那么子Widget的size会被忽略。定义aligment的时候需要当心

Flutter中,Widget由底层的RenderBox 渲染。Flutter中的许多box,特别是只有一个child的box,都会将约束传递给他们的child。
通常根据处理约束的方式不同分为三种box:

  • 自身尽可能大的类型。例如CenterListView 使用的box。
  • 保持和children一样大的类型。例如TransformOpacity使用的box。
  • 使用确定size的类型。例如ImageText 使用的box。

有一些Widget比如Container,会根据构造函数传参的不同产生不同的约束处理方式。比如,如果使用默认构造函数,Container 会使用自身尽可能大的类型,但是如果传了width、height 那么会尽量使用确定size类型。

举例

接下来将通过29个例子来具体分析之前讲的理论。不用担心例子过多,例子之间的逻辑关联最终会串联他们之间的关系并推导出简洁的结论。

例1

image.png
Container(color: red)

屏幕尺寸就是Container 的父Widget尺寸,所以Container 和屏幕的size一样大。

例2

image.png
Container(width: 100, height: 100, color: red)

红色的Container 想要自身大小为100*100,但是实际的大小被限制为屏幕大小。

例3

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

屏幕限制Center 为屏幕大小,所以Center占满屏幕。
Center 限制Container 为任何大小,但是不要超过屏幕。这时,Container 大小是100*100。

例4

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

这里和前一个例子用Center 不同的是使用了Align
AlignCenter 类似,Container 可以是任何大小 。但是布局在父容器的右下角。

例5

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

屏幕限制Center 为屏幕大小,所以Center 占满屏幕。
Center 限制Container 可以是不超过屏幕size的任何size。Container 想要无限大,但是受到不超过屏幕size的限制,所以只能是屏幕size。

Example6

image.png
Center(child: Container(color: red))

屏幕限制Center的大小为屏幕尺寸,所以Center 占满屏幕。
Center 限制Container 的大小为任何不超过屏幕的大小。因为Container没有限制大小也没有任何child,所以这里会尽可能大,占满屏幕。

例7

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

屏幕限制Center的大小为屏幕尺寸,所以Center 占满屏幕。
Center 限制Container 的大小为任何不超过屏幕的大小。Container没有指定的size但是有一个child,所以会把自身的大小设置为child的大小。
红色的Container 限制child可以是不超过屏幕的任何大小。
绿色的Container child大小是30X30。所以红色的Container 大小是30X30。这里红色的Container不可见是因为被绿色的Container正好盖住了。

例8

image.png
Center(
  child: Container(
    padding: const EdgeInsets.all(20),
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

红色的Container的size就是child的size加上padding。即size是70X70。

例9

image.png
ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 70,
    minHeight: 70,
    maxWidth: 150,
    maxHeight: 150,
  ),
  child: Container(color: red, width: 10, height: 10),
)

根据代码字面上的意思是Container 的size是在70X70到150X150之间,但是这是错误的。ConstrainedBox 的constraints参数是在父Widget传来的约束上附加的额外约束。
这里ConstrainedBox 大小是屏幕的大小,所以它传递给child Container 的约束也是屏幕的大小,所以constraints 参数被忽略了。

例10

image.png
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 10, height: 10),
  ),
)

CenterConstrainedBox的大小限制不超过 屏幕大小。ConstrainedBoxconstraints中获取附加限制并传递给它的child。
Container 的长宽都必须在70-150像素之间。自身想要的大小是10X10,最终结果是70X70。

例11

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

CenterConstrainedBox的大小限制不超过 屏幕大小。ConstrainedBoxconstraints中获取附加限制并传递给它的child。
Container 的长宽都必须在70-150像素之间。自身想要的大小是1000X1000,最终结果是150X150。

例12

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

CenterConstrainedBox的大小限制不超过 屏幕大小。ConstrainedBoxconstraints中获取附加限制并传递给它的child。
Container 的长宽都必须在70-150像素之间。自身想要的大小是100X100,在限制范围内,最终结果是100X100。

例13

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

UnconstrainedBox 的大小是屏幕大小。但是UnconstrainedBox 的child可以是任何大小。

例14

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

UnconstrainedBox 的大小是屏幕大小。UnconstrainedBox 的child Container可以是任何大小。
但是这里Container宽度是4000像素,超过了UnconstrainedBox的大小,所以UnconstrainedBox显示了“overflow warining”。

例15

image.png
OverflowBox(
  minWidth: 0,
  minHeight: 0,
  maxWidth: double.infinity,
  maxHeight: double.infinity,
  child: Container(color: red, width: 4000, height: 50),
)

OverflowBox 的大小为屏幕的大小,OverflowBox 的child可以是任何大小。
OverflowBoxUnconstrainedBox 很像,区别是OverflowBox 的child如果size超过了OverflowBox 的大小不会显示overflow警告。

例16

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

这里不会渲染任何内容,并且会在console中看到错误log输出。
UnconstrainedBox 的child可以是任何大小,但是child的宽度是无限。Flutter不能渲染无限大小的Widget,所以会抛出BoxConstraints forces an infinite width 异常。

例17

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

和例16相比,这里不会报错,因为LimitedBoxUnconstrainedBox 限制上给出了一个有限的限制,宽度最多100。
如果 用 Center 取代UnconstrainedBoxLimitedBox不会再附加任何限制,因为LimitedBox 收到了一个有限制的约束,LimitedBox 本身的限制不会生效,所以Container的宽度允许超过100。
这里显示了UnconstrainedBoxLimitedBox 区别。

例18

image.png
const FittedBox(child: Text('Some Example Text.'))

FittedBox 的大小是屏幕的大小。Text 的大小由自身显示内容及性质决定。
FittedBox对于Text 的大小没有限制,但是在Text 确定了大小后,FittedBox会缩放Text 直到占满剩下空间。

例19

image.png
const Center(child: FittedBox(child: Text('Some Example Text.')))

如果将FittedBox放到 Center 里面呢? Center 限制FittedBox 的最大大小是屏幕大小。
FittedBox 根据Text 的size确定自身的size。这样FittedBoxText 大小相同,无需缩放。

例20

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

如果FittedBoxCenter 里面,但是Text 的内容非常长呢?
FittedBox 尝试根据Text 的大小来设置自身的大小,但是不能超过屏幕的大小。最终采用的是屏幕的size来resize Text 的大小。

例21

image.png
const 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

image.png
FittedBox(
  child: Container(height: 20, width: double.infinity, color: Colors.red),
)

FittedBox 只能缩放有固定宽高的widget,否则不会渲染内容,并在console中输出错误。

例23

image.png
Row(
  children: [
    Container(color: red, child: const Text('Hello!', style: big)),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

Row的大小就是屏幕的大小。和UnconstrainedBox 一样,Row 不会限制child的大小,所以child可以是自身想要的任何大小。

例24

image.png
Row(
  children: [
    Container(
      color: red,
      child: const Text(
        'This is a very long text that '
        'won\'t fit the line.',
        style: big,
      ),
    ),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

因为Row 不会给child任何限制,所以child很容易就因为太长超出Row 的宽度。所以这里和 UnconstrainedBox 一样,显示了"overflow warning"。

例25

image.png
Row(
  children: [
    Expanded(
      child: Center(
        child: Container(
          color: red,
          child: const Text(
            'This is a very long text that won\'t fit the line.',
            style: big,
          ),
        ),
      ),
    ),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

Row 的child被Expanded 包裹的时候,Row 就不会让child自身决定自身的大小了。这时,会根据其他child的大小来确定Expanded 的宽度,然后Expanded 会强制被包裹的child使用Expanded 的大小。
换句话说,如果使用了Expanded ,内部包裹的child的宽度会被忽略。

例26

image.png
Row(
  children: [
    Expanded(
      child: Container(
        color: red,
        child: const Text(
          'This is a very long text that won\'t fit the line.',
          style: big,
        ),
      ),
    ),
    Expanded(
      child: Container(
        color: green,
        child: const Text('Goodbye!', style: big),
      ),
    ),
  ],
)

如果Row 所有的child都用Expanded 包裹,那么每个Expanded 的size和它的flex参数大小正比,并且就是每个Expanded 包裹的child大小。换言之,Expanded 忽略了child自身的大小。

例27

image.png
Row(
  children: [
    Flexible(
      child: Container(
        color: red,
        child: const Text(
          'This is a very long text that won\'t fit the line.',
          style: big,
        ),
      ),
    ),
    Flexible(
      child: Container(
        color: green,
        child: const Text('Goodbye!', style: big),
      ),
    ),
  ],
)

这里唯一的区别就是使用Flexible 代替了Expanded ,这样child的宽度就可以小于等于Flexible 的宽度,而不是像Expanded 一样强制child的宽度和自己一样。但是FlexibleExpanded 在测量自身宽度的时候都会忽略child的宽度。

例28

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

Scaffold 被强制使用屏幕的大小,所以Scaffold 会占满屏幕。ScaffoldContainer 的限制是不超过屏幕的任何大小。

注意
当约束是不超过某个确定的大小时,就是所谓的loose constraints。

例29

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

如果想要Scaffold 的child和Scaffold 大小一样,那么可以使用SizedBox.expand 包裹这个child。

Tight vs loose约束

Tight 约束

所谓的tight约束就是有精确的大小。换句话说就是tight约束的最大宽/高等于最小宽/高。
最常见的例子就是包含在RenderView类中的App widget:应用的build 方法返回的box,使用的就是tight约束,大小就是屏幕的大小。
另一个例子是,如果你在应用的render tree的root中嵌套box,每个box都被强制使用tight 约束,这样就能完全填充。
如果看一下box.dart的源码,搜索BoxConstraints 构造函数:

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

和前面讲的例2一样,屏幕强制Container 使用屏幕的大小,所以Container的大小被忽略了。

Loose 约束

loose约束就是最大宽/高不等于0,最小宽/高等于0。
例如Center 这样的box,就会loose从父控件收到的约束。例3中,Center 的child Container就可以小于Center的大小(但是也不能超过屏幕传递个Center的大小)。

Unbounded 约束

在某些情况下,box的约束是unbounded 或者infinite 的。也就是说宽或者高被设为 double.infinity
如果一个自身为尽可能大的box的约束是Unbounded约束,那么在debug 模式下,会抛出异常。
最常见的例子就是在flex box(例如Row或者Column)或者可滚动区域(例如LIstView或者其他ScrollView子类)里面有一个Unbounded约束的render box。

Flex

具体到Flex box(例如Row或者Column)Unbounded约束的影响还取决于是否在发生在主方向上(Row的宽,Column的高)。
Flex box在bounded约束的情况下,在主方向上尽可能大。
Flex box在Unbounded约束的情况下,会尽可能满足child在这个方向上的大小。每个child的flex 值都需要被设置为0,也就是说无法在Unbounded约束的flex box或者scrollable里使用Expanded,否则会抛出异常。
Flex box的交叉方向(Row的高,Column的宽)必须是bounded,否则无法布局内部的child。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容