flutter尺寸解惑

前言

最近,笔者在写布局的时候,发现诸如此类的报错:

1
2
Vertical viewport was given unbounded height.
Vertical viewport was given unbounded width.

大概意思就是指没有限定视图的高度,宽度。
典型的场景如下:

1
2
3
4
5
6
7
8
9
10
Column(
children: <Widget>[
ListView(
children: <Widget>[
Container(color:Colors.red, child:Text("1")),
Container(color:Colors.orange, child:Text("2")),
],
),
],
)

在列视图的子视图添加了ListView, 就会报上面的错:

Vertical viewport was given unbounded height.

分析

对于Column来讲,主轴长度即垂直方向的高度是由MainAxisSize决定的,MainAxisSize有两种类型,分别是min和max,我们到源码看看他们的描述:

注意红色部分,如果Column的child的constraint是unbounded的话,就无法给出真实大小。而对于ListView,垂直高度为double.infinity,即无限制,所以ListView的constraint是unbounded的。那么有什么办法可以解决呢,我们到ListView的源码去一探究竟吧。
在ListView的源码搜索unbound关键字,很快发现有这么一个属性shrinkWrap:

其中有一句很关键的注释, If the scroll view has unbounded constraints in the [scrollDirection], then [shrinkWrap] must be true. 。如果在滚动方向上,约束没有限制的话,那么shrinkWrap应该设置为true。
回头看看,ListView的外层是Column,同样在垂直高度也为unbounded,所以Column的constraint也为unbounded,所以我们按照提示把ListView的shrinkWrap设置为true。结果不言而喻,自然是显示正常。
那为什么把属性设置为true就可以呢,继续啃源码,可以注意到有这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (shrinkWrap) {
return ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
);
}
return Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
center: center,
anchor: anchor,
);

原来是根据shrinkWrap来选择不同的ViewPort。

shrikWrap为true的情况:
进入ViewPort之后,发现真实的渲染对象为RenderViewPort,so,继续前进。

1
2
3
4
5
6
7
8
9
10
@override
RenderViewport createRenderObject(BuildContext context) {
return (
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
cacheExtent: cacheExtent,
);
}

因为是在测量阶段,所以我们先找到performResize,如下:

猜猜我们看到了什么,前言提到的报错 Vertical viewport was given unbounded height.。接着再看判断条件的代码:
bool get hasBoundedHeight => maxHeight < double.infinity;

由于ListView的maxHeight为double.infinity,所以自然返回了false,所以才会导致上述的报错。

shrikWrap为true的情况:
可以看到使用了RenderShrinkWrappingViewport这个类,这个类的官方介绍部分如下:

A render object that is bigger on the inside and shrink wraps its children in the main axis.

收缩主轴上的子视图,看到这应该就可以大概理解了,ListView原来的主轴即垂直方向是unbounded的,而RenderShrinkWrappingViewport通过把主轴进行收缩,这样一来就可以使得主轴方向是确定的,从而解决问题。
当然除了这种方式,我们还可以直接通过在ListView嵌套一层确定高度的布局来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 100.0, child: ListView(
shrinkWrap: false,
children: <Widget>[
Container(color:Colors.red, child:Text("1")),
Container(color:Colors.orange, child:Text("2")),
],
)
)]
))

所以本质上,解决问题的关键就在于:确定ListView的高度, shrinkWrap也好,container也好,都是为了给出ListView的具体高度。
其实上面分析了那么多,牵涉到Flutter的一个基本概念,那就是Constraint,意为约束。
Flutter的约束是从父节点传到子节点,子节点根据约束重新调整自身的大小。举个最简单例子:

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
 return Scaffold(
appBar: AppBar(title: Text("布局测试")),
backgroundColor: Colors.green,
body: Container(
color: Colors.amber,
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
color: Colors.blue,
height: 300.0,
child: Row(mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: <Widget>[
Container(color: Colors.teal, width: 100, height: 100),
Container(color: Colors.purple, width: 100, height: 100),
]),
),

Container(
height: 300,
width: 200,
color: Colors.lime,
child: Column(
children: <Widget>[
Container(color: Colors.purple, width: 100, height: 100),
Container(color: Colors.teal, width: 100, height: 100),
],
),
)
],
)));
}

效果如下:

对于最外层的Column而言,它的父节点约束是屏幕,加上它的垂直方向是max,可以看到最外层的Column高度是整个屏幕高度(橙色部分),而里面的Column的约束则是height为300的Container,所以它的高度是300(黄色部分)。
而对于Row,它的约束条件也是高度300,虽然在水平方向是无限制,但是由于水平方向用了min,所以Row的宽度跟随了子节点的总宽度,即200。如果水平方向用了max,那么Row的宽度则为屏幕宽度。

参考

Flutter盒约束
Flutter 约束知识

仓库

点击flutter_demo,查看完整代码。