宽高?限制?

今天分享的内容主要是 flutter 中很基础的一个概念:尺寸与限制。

当然,这里并不是说尺寸、限制的实现,在 flutter 中就是采用 Container,这里只是以 Container 为例。下面将跳出与 web 的比较,直接讲述 flutter 中的尺寸限制。

基础铺垫

flutter 中的布局与浏览器中的布局行为是类似的(flutter 中负责布局、渲染这一块的负责人也是 chrome 浏览器中负责页面布局、渲染的主要开发者),因此理解尺寸、限制对于布局是尤为重要的。

尺寸

这里的尺寸就是普通意义上的宽度、高度。常用的组件有:

  • Container: 可设置 width/height
  • SizedBox: 可设置 width/height

它看似表面上是以 width/height 定死了容器的宽高,但在 flutter 内部,它会转换为 限制!这句话怎么理解呢?比如如下代码:

1
2
3
4
5
6
7
8
9
10
class MyComponent1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.blue,
);
}
}

这里定义了一个 MyComponent1 组件,它只是组合了其他组件(widget 的分类详见本专题其他文章),内部使用了 Container 作为容器,宽高都为 100 物理像素(这个先埋个伏笔:这个蓝框框的实际大小会是 100*100 吗?)。我们看一下 Container 的构造函数(出于简化目的,之后都会省略非关键代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Container extends StatelessWidget {
Container({
Key key,
this.alignment,
this.padding,
Color color,
Decoration decoration,
this.foregroundDecoration,
// 非类实例属性
double width,
double height,
BoxConstraints constraints,
this.margin,
this.transform,
this.child,
}) : decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null),
constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key);
}

可以很明显地发现 widthheight 只是参数,不是类实例属性,在初始化列表中利用 widthheight 对类实例属性 constraints 赋值。这里暂不讨论方法 #tighten#tightFor

限制

限制就是对宽高的限制,比如最大宽度,最小高度等。常用的限制类组件有:

  • Container: 可设置 constraints
  • LimitedBox: 可设置 maxWidth/maxHeight;
  • ConstrainedBox: 可设置 constraints

其实限制类的组件并没有很多。像刚才列举的 Container 可以作为限制容器,其实也是因为内部使用了 ConstrainedBox 作为包裹,才有了限制的功能。

Container 的注释

其实从 Container 的注释上,我们能发现一些端倪。这里我把关于布局相关的注释翻译一下,同时优先级也是按照从上到下的顺序:

  1. 如果没有设置 child, width, height, constraints, 并且父组件提供无限制的限制(有点拗口,可理解为无限大的限制),则该 Container 会尽可能小;
  2. 如果没有 child 和 alignment,但是至少有 width/height/constraints 的其中之一,当前限制会与父组件传递的限制组合为新的限制,该 Container 会在满足新的限制条件下尽可能小(Note: 这里不确定是翻译问题,还是理解问题,我得出的结论与官方相反。我的理解:应该是满足新的限制条件下尽可能大);
  3. 如果没有 child、height、width、contraints、alignment,但是父组件提供了有限的限制,那么该 Container 就会扩展去满足父组件的(最大)限制;
  4. 如果有 alignment,并且父组件提供无限制的限制,那么该 Container 会围绕 child 来调整它的大小;
  5. 如果有 alignment,父组件提供有限的限制,那么该 Container 会先扩展扩大去满足父组件的限制,然后按照对齐方式定位子元素。
  6. 否则,只有 child,没有 height、width、constraints、alignment,那么该 Container 会将父组件的限制传递给 child,然后调整该 Container 的大小去匹配 child 的大小。

看到这里,建议各位读者先仔细看几遍,然后再继续。后文将主要通过一些例子来验证上述限制的逻辑,同时解答一些常见的 overflow 报错。

验证 1

如果没有设置 child, width, height, constraints, 并且父组件提供无限制的限制(有点拗口,可理解为无限大的限制),则该 Container 会尽可能小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 启动
void main() => runApp(MyComponent1());

class MyComponent1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
RenderBox rb = context.findRenderObject();
rb?.constraints;
return UnconstrainedBox(
child: Test(),
);
}
}

class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 这里使用 Builder 获取到子组件创建时的 context
return Builder(
builder: (BuildContext childContext) {
Timer.run(() {
RenderBox rb = context.findRenderObject();
// 打印 Test 组件的限制
print('Test constraints: ' + rb?.constraints.toString());

RenderBox rbc = childContext.findRenderObject();
// 打印蓝框框的大小
print('Child size: ' + rbc?.size.toString());

});
return Container(
color: Colors.blue,
);
},);
}
}

输出结果:

1
2
I/flutter (20090): Test constraints: BoxConstraints(unconstrained)
I/flutter (20090): Child size: Size(0.0, 0.0)

Test 接收到的限制为 unconstrained(最大宽度、高度都是无穷大),内部蓝框框不设置 child, width, height, constraints,发现打印出的蓝框框大小就是 0,符合 1.

我们将限制只放在单一方向上,比如只让垂直方向无限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 启动
void main() => runApp(MyComponent1());

class MyComponent1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
RenderBox rb = context.findRenderObject();
rb?.constraints;
return UnconstrainedBox(
child: Test(),
// 保持水平的限制,即只让垂直方向无限制
constrainedAxis: Axis.horizontal,
);
}
}

// Test 不变
class Test extends StatelessWidget {}

输出:

1
2
I/flutter (20090): Test constraints: BoxConstraints(w=392.7, 0.0<=h<=Infinity)
I/flutter (20090): Child size: Size(392.7, 0.0)

发现水平方向其实是有大小的,为 Test 组件的宽度大小。但垂直方向变为最小的。同样符合 1.

验证 2

如果没有 child 和 alignment,但是至少有 width/height/constraints 的其中之一,当前限制会与父组件传递的限制组合为新的限制,该 Container 会在满足新的限制条件下尽可能小(Note: 这里不确定是翻译问题,还是理解问题,我得出的结论与官方相反。我的理解:应该是满足新的限制条件下尽可能大):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

import 'dart:async';

import 'package:flutter/material.dart';

class MyComponent2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
RenderBox rb = context.findRenderObject();
rb?.constraints;
// 先包一层 UnconstrainedBox 以产生无限制
return UnconstrainedBox(
// 再包一层 ConstrainedBox 以产生需要的限制
child: ConstrainedBox(
child: Test(),
constraints: BoxConstraints(maxWidth: 300, minWidth: 100, maxHeight: 300, minHeight: 100),
),
);
}
}

class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {

return Builder(
builder: (BuildContext childContext) {
Timer.run(() {
RenderBox rb = context.findRenderObject();
print('Test constraints: ' + rb?.constraints.toString());

RenderBox rbc = childContext.findRenderObject();
print('Child size: ' + rbc?.size.toString());

});
return Container(
width: 150,
height: 150,
color: Colors.blue,
);
},);
}
}

Test 组件的外层限制应该是最大宽度、高度为 300,最小宽度、高度为 100。我们看看输出:

1
2
I/flutter (22330): Test constraints: BoxConstraints(100.0<=w<=300.0, 100.0<=h<=300.0)
I/flutter (22330): Child size: Size(150.0, 150.0)

如果将蓝框框的宽高都设为 80 呢?看输出:

1
2
I/flutter (22330): Test constraints: BoxConstraints(100.0<=w<=300.0, 100.0<=h<=300.0)
I/flutter (22330): Child size: Size(100.0, 100.0)

不是设置的 80 了,而是满足当前限制与父组件的组合。父组件都说了,最小 100,最大 300,你自己设置的 80 我可不认,最小 100!就是这么霸道!

我们不用固定值,我们来使用限制呢?

1
2
3
4
Container(
constraints: BoxConstraints(minHeight: 100, maxHeight: 110, minWidth: 120, maxWidth: 220),
color: Colors.blue,
);

仅去掉蓝框框的 width/height,增加修改蓝框框的约束,看看结果呢?

1
2
I/flutter (22330): Test constraints: BoxConstraints(100.0<=w<=300.0, 100.0<=h<=300.0)
I/flutter (22330): Child size: Size(220.0, 110.0)

发现,大小并不是组合限制(本例中为 120<=w<=220, 100<=h<=110)中的最小值 (120, 100),而是最大值 (220, 110)。正如 2 中括号注释的一样(官方文档没有,笔者自己领悟的),当为紧约束时,为紧约束的大小;当为松约束时,为松约束的最大值。什么是紧送约束?

松紧约束

直接看代码:

1
2
3
4
5
// BoxConstraints 中
bool get hasTightWidth => minWidth >= maxWidth;
bool get hasTightHeight => minHeight >= maxHeight;

bool get isTight => hasTightWidth && hasTightHeight;

很简单:紧(tight)约束就是最小宽度等于最大宽度 && 最小高度等于最大高度。也就是说,当我们设置:

1
2
width: 100,
height: 100,

时,会被转换成:

1
2
constraints = BoxContraints.tightFor(width: 100, height: 100)
// constraints == BoxContraints(minHeight: 100, maxHeight: 100, minWidth: 100, maxWidth: 100)

此时就说该限制是紧的。相反,当最小宽度、高度分别小于最大宽度、高度,此时就说是松约束。
那么松、紧约束在此条件下,为什么表现不一致呢?看一下 Container 的 build 方法:

1
2
3
4
5
6
7
8
9
10
11
12
Widget build(BuildContext context) {
Widget current = child;

if (child == null && (constraints == null || !constraints.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
// ...
}

当 child 为 null 并且(限制为 null 或者是松约束),则包一层 LimitedBox。这个 LimitedBox 有啥作用?因为它不是本文主要内容,这里就简单讲解一下:

1
2
3
4
5
6
const LimitedBox({
Key key,
this.maxWidth = double.infinity,
this.maxHeight = double.infinity,
Widget child,
})

LimitedBox 作用:当父组件给的限制是无限制的时候,则把对 child 的限制改为 maxWidthmaxHeight。当 child 为 null 时,在 Container 的最内层使用了 LimitedBox(width: 0, height: 0, child: ConstrainedBox(constraints: const BoxConstraints.expand()), ),意思是:当父组件给的限制为无限制时,大小就为 0(这个是 LimitedBox 的作用),这个符合验证 1;当父组件给的限制为有限限制时,大小就为 BoxConstraints.expand (),也即满足限制的最大尺寸。这就解释了为什么验证 2 中我添加的注释。如果大家有疑问,可以留言。

验证 3

如果没有 child、height、width、contraints、alignment,但是父组件提供了有限的限制,那么该 Container 就会扩展去满足父组件的(最大)限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

import 'dart:async';

import 'package:flutter/material.dart';

class MyComponent3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
RenderBox rb = context.findRenderObject();
rb?.constraints;
return UnconstrainedBox(
child: ConstrainedBox(
child: Test(),
constraints: BoxConstraints(maxWidth: 300, minWidth: 100, maxHeight: 300, minHeight: 100),
),
);
}
}

class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {

return Builder(
builder: (BuildContext childContext) {
Timer.run(() {
RenderBox rb = context.findRenderObject();
print('Test constraints: ' + rb?.constraints.toString());

RenderBox rbc = childContext.findRenderObject();
print('Child size: ' + rbc?.size.toString());

});
return Container(
color: Colors.blue,
);
},);
}

}

输出:

1
2
I/flutter (26984): Test constraints: BoxConstraints(100.0<=w<=300.0, 100.0<=h<=300.0)
I/flutter (26984): Child size: Size(300.0, 300.0)

原因也很明显了,验证 2 中最后就拿根本原因 LimitedBox 解释过。因为父组件给的是有限限制,所以 LimitedBox 不生效,但 LimitedBox 中的 child 的限制却生效了:BoxConstraints.expand (),即满足限制条件的最大。

验证 4

如果有 alignment,并且父组件提供无限制的限制,那么该 Container 会围绕 child 来调整它的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

import 'dart:async';

import 'package:flutter/material.dart';

class MyComponent4 extends StatelessWidget {
@override
Widget build(BuildContext context) {
RenderBox rb = context.findRenderObject();
rb?.constraints;
return UnconstrainedBox(
child: Test(),
);
}
}

class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {

return Builder(
builder: (BuildContext childContext) {
Timer.run(() {
RenderBox rb = context.findRenderObject();
print('Test constraints: ' + rb?.constraints.toString());

RenderBox rbc = childContext.findRenderObject();
print('Child size: ' + rbc?.size.toString());

});
return Container(
color: Colors.blue,
child: SizedBox(
width: 100,
height: 120,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.yellow),
),
)
);
},);
}
}

输出:

1
2
I/flutter (26984): Test constraints: BoxConstraints(unconstrained)
I/flutter (26984): Child size: Size(100.0, 120.0)

Container 的大小是 SizedBox 的大小(一定会是这样吗?),虽然没有设置 alignment(也没必要,毕竟该 Container 的大小就为 child 的大小,定位是无意义的),但也符合 4。此时看一下页面显示:
显示

下面看一下,分几种情况,我们应用 alignment

1
2
3
4
5
6
7
8
9
10
11
12
13
Container(
color: Colors.blue,
width: 200,

// alignment: Alignment.centerLeft,
child: SizedBox(
width: 100,
height: 120,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.yellow),
),
),
)

不设置 alignment,此时讲道理,蓝框框会比黄的大。但此时页面:
展示
输出:

1
2
I/flutter (26984): Test constraints: BoxConstraints(unconstrained)
I/flutter (26984): Child size: Size(200.0, 120.0)

黄框框完全遮挡住了蓝框框。为什么呢?因为父组件给的限制是紧宽度,松高度,即使 SizedBox 限制了宽度,由于父组件的绝对限制,导致宽度变成了父组件的宽度。

当设置对齐方式时,比如 center (水平垂直都居中):

1
2
3
4
5
6
7
8
9
10
11
12
13
Container(
color: Colors.blue,
width: 200,
height: 200,
alignment: Alignment.center,
child: SizedBox(
width: 100,
height: 100,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.yellow),
),
)
);

此时,页面展示:
 展示
输出:

1
2
I/flutter (26984): Test constraints: BoxConstraints(unconstrained)
I/flutter (26984): Child size: Size(200.0, 200.0)

符合预期。外层 Container 宽高为 200,内层 SizedBox 宽高为 100,设置的对齐方式为水平垂直居中。

验证 5

如果有 alignment,父组件提供有限的限制,那么该 Container 会先扩展扩大去满足父组件的限制,然后按照对齐方式定位子元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

class MyComponent5 extends StatelessWidget {
@override
Widget build(BuildContext context) {
RenderBox rb = context.findRenderObject();
rb?.constraints;
return UnconstrainedBox(
child: ConstrainedBox(
constraints: BoxConstraints.loose(Size(300, 300)),
child: Test(),
),
);
}
}

class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {

return Builder(
builder: (BuildContext childContext) {
Timer.run(() {
RenderBox rb = context.findRenderObject();
print('Test constraints: ' + rb?.constraints.toString());

RenderBox rbc = childContext.findRenderObject();
print('Child size: ' + rbc?.size.toString());

});
return Container(
color: Colors.blue,
alignment: Alignment.center,
child: SizedBox(
width: 100,
height: 120,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.yellow),
),
)
);
},);
}
}

输出:

1
2
I/flutter (26984): Test constraints: BoxConstraints(0.0<=w<=300.0, 0.0<=h<=300.0)
I/flutter (26984): Child size: Size(300.0, 300.0)

页面:
展示
Container 的大小即是扩展到父组件的最大,即 300*300,然后根据 alignment 去定位 child。

验证 6

否则,只有 child,没有 height、width、constraints、alignment,那么该 Container 会将父组件的限制传递给 child,然后调整该 Container 的大小去匹配 child 的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

import 'dart:async';

import 'package:flutter/material.dart';

class MyComponent6 extends StatelessWidget {
@override
Widget build(BuildContext context) {
RenderBox rb = context.findRenderObject();
rb?.constraints;
return UnconstrainedBox(
child: ConstrainedBox(
constraints: BoxConstraints.loose(Size(300, 300)),
child: Test(),
),
);
}
}

class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {

return Builder(
builder: (BuildContext childContext) {
Timer.run(() {
RenderBox rb = context.findRenderObject();
print('Test constraints: ' + rb?.constraints.toString());

RenderBox rbc = childContext.findRenderObject();
print('Child size: ' + rbc?.size.toString());

});
return Container(
color: Colors.blue,
child: SizedBox(
width: 100,
height: 120,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.yellow),
),
)
);
},);
}

}

输出:

1
2
I/flutter (26984): Test constraints: BoxConstraints(0.0<=w<=300.0, 0.0<=h<=300.0)
I/flutter (26984): Child size: Size(100.0, 120.0)

看到 Container 的大小就是 child 的大小。

当我们修改父组件的限制时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MyComponent6 extends StatelessWidget {
@override
Widget build(BuildContext context) {
RenderBox rb = context.findRenderObject();
rb?.constraints;
return UnconstrainedBox(
child: ConstrainedBox(
// 改动点,最小宽高为200,最大无穷
constraints: BoxConstraints(minWidth: 200, minHeight: 200),
child: Test(),
),
);
}
}

class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {

return Builder(
builder: (BuildContext childContext) {
Timer.run(() {
RenderBox rb = context.findRenderObject();
print('Test constraints: ' + rb?.constraints.toString());

RenderBox rbc = childContext.findRenderObject();
print('Child size: ' + rbc?.size.toString());

});
return Container(
color: Colors.blue,
child: SizedBox(
width: 100,
height: 120,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.yellow),
),
)
);
},);
}
}

输出:

1
2
I/flutter (26984): Test constraints: BoxConstraints(200.0<=w<=Infinity, 200.0<=h<=Infinity)
I/flutter (26984): Child size: Size(200.0, 200.0)

看到 Container 的大小是 200*200 了(当然 SizedBox 也是 200*200,而不会管 100*120,原因上面也说过,就是父组件的限制强于自己的限制)。也就是说这种情况下将父组件的限制透传给了 child。作用于 SizedBox 的限制是 BoxConstraints(200.0<=w<=Infinity, 200.0<=h<=Infinity)

总结 1

  1. flutter 中的限制是从顶层(比如 MaterialApp 这样的功能组件)传入底层(任意分支),上一层的限制会限制这一层的限制。举个例:
    限制组合

COMPONENT2 会接收其父组件 COMPONENT1 传递的约束 1,同时自身也有约束 2,那么新的约束条件就是 3。图中的约束 3 所表示的宽高不一定是实际情况(根据组件功能的不同,情况会有变化),主要看对应 RenderObjectperformLayout 中怎么生成新的组合限制。
2. flutter 中的 size 是从底层回传到顶层。