Flutter学习篇(四)—— 尺寸解惑

8,053 阅读4分钟

导航

前言

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

Vertical viewport was given unbounded height.

Vertical viewport was given unbounded width.

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

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就可以呢,继续啃源码,可以注意到有这么一段代码:

    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,继续前进。
  @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嵌套一层确定高度的布局来解决这个问题:

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的约束是从父节点传到子节点,子节点根据约束重新调整自身的大小。举个最简单例子 :

   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,查看完整代码。