likes
comments
collection
share

[Flutter] 为什么我的 ListView 又双叒叕崩了

作者站长头像
站长
· 阅读数 12

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

作为Flutter开发的你,一定对这些东西非常熟悉:

[Flutter] 为什么我的 ListView 又双叒叕崩了

在写完布局,满怀期待按下ctrl + s,短暂的热重载之后。控制台里的信息清晰可见:ListView又崩溃了。为什么要用又?因为在实现和Row、Column这一类布局的搭配实现布局的时候,只要遇到了ListView之间的配合,稍有不注意,UI就不见了或者抛出一行红红的错误,然后就会在控制台中看到这一类的报错。例如:

return Column(
  children: [
    Text("列表"),
    ListView.builder(
      itemBuilder: (ctx, index) => Text("$index"),
      itemCount: 20,
    )
  ],
);

这是一个再普通不过的布局了,但是它就是显示不出来,一顿百度,我们会为ListView.builder加上这个属性:shrinkWrap:true,问题解决了。

一. 约束

要知道这个错误的原因,首先我们得看看Flutter是如何确定 Widget的尺寸大小的,作为一颗树形结构的树,我们的Widget在进行布局时,是依靠RenderObject来管理它的渲染、绘制的,这其中有一个非常重要的属性:约束(Constraint)

约束,是父Widget对下层Widget的一个尺寸建议区间。它的形式通常是这样的:

const BoxConstraints({
  this.minWidth = 0.0,
  this.maxWidth = double.infinity,
  this.minHeight = 0.0,
  this.maxHeight = double.infinity,
})

本质上,它是一个区间:[minWidth,maxWidth]和[minHeight,maxHeight]就是父Widget,对子Widget的一个尺寸的建议,并且,子Widget必须采纳这个建议,即从区间中,取值,进行大小的确定

父Widget给与的约束区间是宽度[0,360],高度是[0,720],那么子Widget就会根据自身功能的特性,进行取值,例如:

  • Align、Center这一类的辅助定位组件,它们就会采取最大值,即宽为360、高为720,再在其中,帮助子组件进行定位,并且给子组件传递一个相同的约束。(宽度[0,360],高度是[0,720])。
  • Text、Image这一类的组件,因为本身就是一个可渲染的元素,而不是帮助定位的辅助Widget,通常来说,它们都会有自己的尺寸,所以,面对这些约束,它唯一要遵循的一点,就是宽度不超过360,高度不超过720,他可以在这个范围内根据自己的情况取值。

细心的你肯定能发现,上述的第一种情况,Align的下一个Widget,大概率就会是一个Text或者Image,即使不是,那么渲染的叶子节点也一定会是一个Text或者是Image这一类的直接渲染组件。

所以,我们可以做一个猜测:渲染的末端的尺寸绝大多数的情况下都是大致确定的,只需要遵循父组件的约束即可确定最终的渲染尺寸。

我们将区间左右端点值不同的约束,叫做松约束(loose constraint),而左右端点值相同的约束,叫做强制约束(tight constraint),上述的例子中的[0,360]和[0,720]就是一个松约束,子Widget的尺寸选择非常地自由,而[360,360]和[720,720]这一类的叫强制约束,即子Widget的尺寸此时已经被定死了:宽为360,高度为720。

既然Text这类组件有自己的尺寸,那么如果强制约束碰到Text会发生什么呢?答案是Text会强制撑开到强制约束给定的尺寸,所以,约束是必须服从的,即使你Text可能只有十几个像素,最终也会被强制约束撑开。

通常情况下,父Widget向子Widget传递约束,子Widget决定自己的几何信息,并返回给父Widget,递归这个过程,我们就能完成整棵视图树的测量。

二. 约束在Flex组件中的实践

上述的情况,都是基于一个前提条件:一对一的情况。Align和Center它们都只有一个子Widget的情况。但是一些其他的组件:Flex以及通常使用的子类Column和Row的情况就比较特殊了。

2.1 Column

按照惯例,Column、Row的外层或许给它们附加一个松约束,或者给它们附加一个强制约束。

比较常见地,施加强制约束的方法包括:

  • ConstraintBox
  • 设置Container的高度和宽度
  • 最外层控件收到的约束就是强制约束

这时有两个重要的属性就要悄悄地生效了:mainAxisSizemainAxisAlignment。分别是主轴尺寸主轴对齐方式注意,主轴尺寸:mainAxisSize仅在收到父布局的松约束时,才会生效,它会根据min和max分别取到约束区间的两个端点,来设置Column的高度或者是Row的宽度。

如下,你可以修改mainAxisSize为min或者max,你会发现是生效的,但是如果你删掉Center,再去调整,你会发现怎么调都没用了,Column永远会是撑开强制约束的空间。

Widget _buildBody() {
  // 松约束
  return Center(
    child: Column(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: const [
        Text("列表"),
        Text("列表"),
        Text("列表"),
      ],
    ),
  );
}

mainAxisAlignment,只在有剩余空间的情况下才会改变children的排列。

而Flex在默认情况下,给与子Widget的约束是什么约束呢?

这里我们可以借助一个工具Widget:LayoutBuilder查询约束的参数(当然,你也可以直接使用Layout Inspector)

LayoutBuilder(builder: (context, constraints) {
  print('约束信息$constraints');
  return Text("列表");
}

这样一来,我们可以知道,Column会给予子Widget一个横向的最大约束,纵向的无限约束:

flutter: 约束信息BoxConstraints(0.0<=w<=375.0, 0.0<=h<=Infinity)

这是符合预期的,因为Column既然要在纵轴上自由容纳多个Widget,那么就不能为纵轴加上一个具体的约束数值,而是采用Infinity的方式,而横轴则没有这个限制;同理Row的横轴约束就是[0,Infinity],而纵轴则是所能达到的最大的高度。

2.2 Flexible

Flexible是必须嵌入在Flex中的组件。顾名思义,它的名称所具有的含义就是:

能适应新情况的; 灵活的; 可变动的; 柔韧的; 可弯曲的; 有弹性的;

它可以直接使用,不过我们使用的更多的是它的一个子类:Expanded,上文提到,以Column为例,Flex会给与子Widget,在主轴上为[0,infinity]的约束,换句话说,子Widget不会受到来自Column的具体的数值约束

Flexible的作用就相当于一个分隔容器,根据flex属性设置的数值,由Flex为他分配具体的高度约束,比如Column中套了两个Flexible,它们的flex都为1,那么我们就可以得到Flexible受到的约束为:

flutter: 约束信息BoxConstraints(0.0<=w<=375.0, h=364.0)
flutter: 约束信息BoxConstraints(0.0<=w<=375.0, h=364.0)

如果一个的flex为1,另一个设置为2,那么:

flutter: 约束信息BoxConstraints(0.0<=w<=375.0, 0.0<=h<=242.7)
flutter: 约束信息BoxConstraints(0.0<=w<=375.0, 0.0<=h<=485.3)

总之,Flexible的作用,就是按照比例划分父布局(Flex)的可用空间,而划分Flex空间的实质,是向下传递一个新的约束。

那么问题又来了,Flexible向下传递的布局是松还是强制约束呢?

这取决于它的另一个参数:fit,fit通常出现在图片控件中,用作当图片和Image控件尺寸不同时的一种适配处理方案,这里的Flexible的fit,作用就是说当前的Flexible,给予子Widget的约束是松约束,还是紧约束。例如下图中的Widget4,我们包裹在一个Flexible.loose中,那么它的高度就可以从[0,剩余高]中自由选择,所以,看起来并没有变化;如果包裹在一个Flexible.tight当中,他就会强制撑开Flexible的空间。

[Flutter] 为什么我的 ListView 又双叒叕崩了

三. ListView

前面我们介绍了约束和一些约束的实践,这时,我们就要回来看看我们最开始的问题:

Vertical viewport was given unbounded height.

翻译过来,就是:垂直的Viewport被赋予了未确定的高度

结合「被赋予」、「高度」和之前的内容,我们可以大胆地猜测原因:ListView没有被赋予一个确定的纵向约束所导致的,所以,我们可以在外层套一个SizedBox并设置数值,这是可行的;也可以套一个Expanded,给与一个尽可能撑开的约束,这也是可行的。

但是,我们如果想让ListView能够按照自身高度进行自适应,而不是拘束于SizedBox当中(因为这样可能会因为一些机型的字体设置过大导致区域内文字被截断等等问题)。

这时,我们就需要一个shrinkWrap属性。它其实是ListView的父类:ScrollView的属性,在shrinkWrap的注释当中,我们可以清楚地看到如下的描述:

滚动视图的范围是否应由正在查看的内容决定.

如果ScrollView不被设置shrink wrap,这时ScrollView会在滑动方向对应的轴上,默认展开到他所拥有的最大尺寸;如果ScrollView在最其滚动轴上并没有设置一个边界的约束,那么就必须设置shrink wrap为true。

总而言之,对ListView来说,你如果不为它设置约束(例如Expanded、SizeBox),那么你就必须设置shrinkWrap = true。而一旦设置了之后,正如Shrink Wrap这个名字一样,收缩 + 包裹,ListView会收缩,缩小到其子Widget的总高度,就像这样,居于Column的开始位置,如下图1中的深色区域。

注意,这里的收缩并不能违反父布局的强制约束,如果父布局给与了具体的尺寸,比如高度是:812的强制约束,那么ListView并不会收缩,而是被强制约束设置为812的尺寸(如下图2),Flutter中的强制约束是不可改变的,只有受到松约束才存在收缩这回事。

[Flutter] 为什么我的 ListView 又双叒叕崩了

[Flutter] 为什么我的 ListView 又双叒叕崩了

~End