一文彻底理解Flutter布局约束

Posted 牧羊人.阿标

tags:

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

前言

我们在写Flutter UI的时候,经常碰到明明widget设置width或者height,但是看上去就是无效呢。明明没有设置某个widget的大小,反而却呈现出我们想要的大小。又或者是各种莫名奇妙的overflow warning

其实要搞清楚这些问题的原因,我们必须要了解Flutter的布局约束。

约束布局规则

了解Flutter布局约束之前,我们必须要先了解它的规则:

  • 上层widget向下层widget传递约束条件
  • 下层widget想上层widget传递大小信息
  • 上册widget决定下层widget位置

如果我们在开发中不能熟练的运用这条规则,那么在布局的时候就会遇到各种麻烦,终究不能理解其原理。

细节描述:

  • Widget会通过他的父级获得自身的约束。约束实际上就是四个浮点类型的集合:最大、最小宽度,最大、最小高度。
  • 然后,这个widget会逐渐遍历他的children列表。向子级传递约束信息(子级之间的约束可能会有所不同),然后询问它的每个子级需要用于布局的大小。
  • 然后这个widget就会对它的子级的children逐个进行布局。
  • 最后,widget将会把它的大小信息向上传递至父widget(包括其原始约束条件)。

重要的限制

正如上述所介绍的布局规则所说的那样,Flutter的布局引擎有一些重要的限制:

  • 一个widget仅仅在其父级给的约束的情况下才能决定自身的大小。这意味着widget通常情况下不能任意获得其想要的大小。
  • 一个widget无法知道,也不需要决定其在屏幕中的位置。因为它的位置由其父级决定的。
  • 当轮到父级决定其大小和位置的时候,同样的也取决于它自身的父级。所以,在不考虑整棵树的情况下,几乎不可能精确确定任何widget的大小和位置
  • 如果子级想要拥有和父级不同的大小,然而父级没有足够的空间对齐进行布局的话,子级的设置的大小可能会不生效。这时候需要明确指定它的对齐方式

样例解释

所有样例代码都是比较简单,主要就是为了解释场景

我们来看看统一的样例dart代码

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
		// 隐藏状态栏和底部操作栏
    SystemChrome.setEnabledSystemUIOverlays([]);
    //样例布局代码
    return Container(
      color: Colors.red,
    );
  }
}

一下样例都是修改样例布局代码块来实现效果解释

样例1

Container(color: red);

整个屏幕作为 Container 的父级,并且强制 Container 变成和屏幕一样的大小。所以这个 Container 充满了整个屏幕,并绘制成红色。

样例2

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

红色的 Container 想要变成 100 x 100 的大小,但是它无法变成,因为屏幕强制它变成和屏幕一样的大小。所以 Container 充满了整个屏幕。

样例3

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

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满了屏幕。

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。现在,Container 可以真正变成 100 × 100 大小了。

样例4

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

与上一个样例不同的是,我们使用了 Align 而不是 Center

Align 同样也告诉 Container,你可以变成任意大小。但是,如果还留有空白空间的话,它不会居中 Container。相反,它将会在允许的空间内,把 Container 放在右下角(bottomRight)。

样例5

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

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。现在,Container 想要无限的大小,但是由于它不能比屏幕更大,所以就仅充满屏幕。

样例6

Center(
  child: Container(color: red),
)

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。由于 Container 没有子级而且没有固定大小,所以它决定能有多大就有多大,所以它充满了整个屏幕。

但是,为什么 Container 做出了这个决定?非常简单,因为这个决定是由 Container widget 的创建者决定的。可能会因创造者而异,而且你还得阅读 Container 文档 来理解不同场景下它的行为。

样例7

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

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

然后 Center 告诉红色的 Container 可以变成任意大小,但是不能超出屏幕。由于 Container 没有固定大小但是有子级,所以它决定变成它 child 的大小。

然后红色的 Container 告诉它的 child 可以变成任意大小,但是不能超出屏幕。

而它的 child 是一个想要 30 × 30 大小绿色的 Container。由于红色的 Container 和其子级一样大,所以也变为 30 × 30。由于绿色的 Container 完全覆盖了红色 Container,所以你看不见它了。

样例8

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

红色 Container 变为其子级的大小,但是它将其 padding 带入了约束的计算中。所以它有一个 30 x 30 的外边距。由于这个外边距,所以现在你能看见红色了。而绿色的 Container 则还是和之前一样。

样例9

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

你可能会猜想 Container 的尺寸会在 70 到 150 像素之间,但并不是这样。 ConstrainedBox 仅对其从其父级接收到的约束下施加其他约束。

在这里,屏幕迫使 ConstrainedBox 与屏幕大小完全相同,因此它告诉其子 Widget 也以屏幕大小作为约束,从而忽略了其 constraints 参数带来的影响。

样例10

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

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。虽然它希望自己有 10 个像素大小,但最终获得了 70 个像素(最小为 70)。

样例11

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

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。虽然它希望自己有 1000 个像素大小,但最终获得了 150 个像素(最大为 150)。

样例12

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

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。 ConstrainedBoxconstraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。虽然它希望自己有 100 个像素大小,因为 100 介于 70 至 150 的范围内,所以最终获得了 100 个像素。

样例13

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

屏幕强制 UnconstrainedBox 变得和屏幕一样大,而 UnconstrainedBox 允许其子级的 Container 可以变为任意大小。

样例14

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

屏幕强制 UnconstrainedBox 变得和屏幕一样大,而 UnconstrainedBox 允许其子级的 Container 可以变为任意大小。

不幸的是,在这种情况下,容器的宽度为 4000 像素,这实在是太大,以至于无法容纳在 UnconstrainedBox 中,因此 UnconstrainedBox 将显示溢出警告(overflow warning)。

样例15

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

屏幕强制 OverflowBox 变得和屏幕一样大,并且 OverflowBox 允许其子容器设置为任意大小。

OverflowBoxUnconstrainedBox 类似,但不同的是,如果其子级超出该空间,它将不会显示任何警告。

在这种情况下,容器的宽度为 4000 像素,并且太大而无法容纳在 OverflowBox 中,但是 OverflowBox 会全部显示,而不会发出警告。

样例16

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

这将不会渲染任何东西,而且你能在控制台看到错误信息。

UnconstrainedBox 让它的子级决定成为任何大小,但是其子级是一个具有无限大小的 Container

Flutter 无法渲染无限大的东西,所以它抛出以下错误: BoxConstraints forces an infinite width.(盒子约束强制使用了无限的宽度)

样例17

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

这次你就不会遇到报错了。 UnconstrainedBoxLimitedBox 一个无限的大小;但它向其子级传递了最大为 100 的约束。

如果你将 UnconstrainedBox 替换为 Center,则LimitedBox 将不再应用其限制(因为其限制仅在获得无限约束时才适用),并且容器的宽度允许超过 100。

上面的样例解释了 LimitedBoxConstrainedBox 之间的区别。

样例18

FittedBox(
      child: Text('Some Example Text.',textDirection: TextDirection.ltr,)
);

屏幕强制 FittedBox 变得和屏幕一样大,而 Text 则是有一个自然宽度(也被称作 intrinsic 宽度),它取决于文本数量,字体大小等因素。

FittedBoxText 可以变为任意大小。但是在 Text 告诉 FittedBox 其大小后, FittedBox 缩放文本直到填满所有可用宽度。

样例19

const Center(
  child: FittedBox(
    child: Text('Some Example Text.', textDirection: TextDirection.ltr),
  ),
)

但如果你将 FittedBox 放进 Center widget 中会发生什么? Center 将会让 FittedBox 能够变为任意大小,取决于屏幕大小。

FittedBox 然后会根据 Text 调整自己的大小,然后让 Text 可以变为所需的任意大小,由于二者具有同一大小,因此不会发生缩放。

样例20

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.'),
    		textDirection: TextDirection.ltr,
  ),
)

然而,如果 FittedBox 位于 Center 中,但 Text 太大而超出屏幕,会发生什么?

FittedBox 会尝试根据 Text 大小调整大小,但不能大于屏幕大小。然后假定屏幕大小,并调整 Text 的大小以使其也适合屏幕。

样例21

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.',
  		textDirection: TextDirection.ltr),
)

然而,如果你删除了 FittedBoxText 则会从屏幕上获取其最大宽度,并在合适的地方换行。

样例22

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

FittedBox 只能在有限制的宽高中对子 widget 进行缩放(宽度和高度不会变得无限大)。否则,它将无法渲染任何内容,并且你会在控制台中看到错误。

样例23

Row(
      textDirection: TextDirection.ltr,
      children: [
        Container(color: red, child: const Text('Hello!', textDirection: TextDirection.ltr,)),
        Container(color: green, child: const Text('Goodbye!', textDirection: TextDirection.ltr,)),
      ],
    )

屏幕强制 Row 变得和屏幕一样大,所以 Row 充满屏幕。

UnconstrainedBox 一样, Row 也不会对其子代施加任何约束,而是让它们成为所需的任意大小。 Row 然后将它们并排放置,任何多余的空间都将保持空白。

样例24

Row(
      textDirection: TextDirection.ltr,
      children: [
        Container(
          color: red,
          child: const Text(
            'This is a very long text that '
            'won\\'t fit the line.',
            textDirection: TextDirection.ltr,
          ),
        ),
        Container(
            color: green,
            child: const Text(
              'Goodbye flutter ui !!!!!!',
              textDirection: TextDirection.ltr,
            )),
      ],
    )

由于 Row 不会对其子级施加任何约束,因此它的 children 很有可能太大而超出 Row 的可用宽度。在这种情况下, Row 会和 UnconstrainedBox 一样显示溢出警告。

样例25

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

Row 的子级被包裹在了 Expanded widget 之后, Row 将不会再让其决定自身的宽度了。

取而代之的是,Row 会根据所有 Expanded 的子级来计算其该有的宽度。

换句话说,一旦你使用 Expanded,子级自身的宽度就变得无关紧要,直接会被忽略掉。

样例26

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

如果所有 Row 的子级都被包裹了 Expanded widget,每一个 Expanded 大小都会与其 flex 因子成比例,并且 Expanded widget 将会强制其子级具有与 Expanded 相同的宽度。

换句话说,Expanded 忽略了其子 Widget 想要的宽度。

样例27

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

如果你使用 Flexible 而不是 Expanded 的话,唯一的区别是,Flexible 会让其子级具有与 Flexible 相同或者更小的宽度。而 Expanded 将会强制其子级具有和 Expanded 相同的宽度。但无论是 Expanded 还是 Flexible 在它们决定子级大小时都会忽略其宽度。

这意味着,Row 要么使用子级的宽度,要么使用ExpandedFlexible 从而忽略子级的宽度。

样例28

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

屏幕强制 Scaffold 变得和屏幕一样大,所以 Scaffold 充满屏幕。然后 Scaffold 告诉 Container 可以变为任意大小,但不能超出屏幕。

当一个 widget 告诉其子级可以比自身更小的话,我们通常称这个 widget 对其子级使用 宽松约束(loose)

样例29

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

如果你想要 Scaffold 的子级变得和 Scaffold 本身一样大的话,你可以将这个子级外包裹一个 SizedBox.expand

当一个 widget 告诉它的子级必须变成某个大小的时候,我们通常称这个 widget 对其子级使用 严格约束(tight)

严格约束(Tight)VS宽松约束(Loose)

以后你经常会听到一些约束为严格约束或宽松约束,你花点时间来弄明白它们是值得的。

严格约束给你了一种获得确切大小的选择。换句话来说就是,它的最大/最小宽度是一致的,高度也一样。

一个 宽松 约束,换句话来说就是设置了最大宽度/高度,但是让允许其子 widget 获得比它更小的任意大小。换句话来说,宽松约束的最小宽度/高度为 0

以上是关于一文彻底理解Flutter布局约束的主要内容,如果未能解决你的问题,请参考以下文章

Flutter布局指南之约束和尺寸

在约束布局中查看片段的绑定不起作用

Flutter布局指南之深入理解BoxConstraints

一文让你彻底理解SELECT语句的执行逻辑

Flutter 布局备忘录

一文让你彻底理解SQL关联子查询