[Flutter] 为什么我的 ListView 又双叒叕崩了 – 中,咱们从一个问题引进,并介绍了布局和束缚之间的联系,关于Flutter布局完成最中心的一个点,其实就在于束缚。

束缚一般是一个四元组构成的两个区间。即:

[minHeightValue,maxHeightValue],[minWidthValue,maxWidthValue]

咱们运用LayoutBuilder可以经过打印它的参数:constraints来直观地查看一个组件所遭到的束缚:

咱们可以经过扩展办法,封装一个extension工具,来快速地打印一个组件的BoxConstaint束缚:

extension DebugUtil on Widget {
  Widget printLayoutInformation() {
    return LayoutBuilder(builder: (ctx, constraints) {
      print(
          "flutter_experiment_log: ${this.runtimeType}'s constraints:$constraints");
      return this;
    });
  }
}

运用时,咱们只需要:

Column(...).printLayoutInformation();

咱们就可以在日志中,看到Column所遭到的外部束缚了:

I/flutter (26629): flutter_experiment_log: Column's constraints:BoxConstraints (0.0<=w<=392.7, 0.0<=h<=734.5)

一、常见的一些束缚

1.1 屏幕(RenderView)束缚

一如Android的Activity会承受来自屏幕的尺度束缚,例如1920×1080的像素分辨率;

Flutter的最外层组件一样会遭到一个屏幕外部的束缚,通常状况下咱们会在最外层运用一个MateriApp组件,经过打印它的束缚,咱们可以看到,这个束缚的数值是:

I/flutter (31221): flutter_experiment_log: MaterialApp's constraints:BoxConstraints(w=392.7, h=825.5)

因而,最外层的组件会被「屏幕」设置一个强制束缚,宽度被强制束缚为392.7,高度被强制束缚为825.5。

经过堆栈剖析,咱们可以看到这个强制束缚最早来自RenderView.performLayout,其中经过BoxConstraints.tight(_size)办法供给了一个以_size尺度为数值的强制束缚。而_size则是在Flutter初始化的时分,经过createViewConfiguration办法创建的ViewConfiguration,其中的size便是设备的物理分辨率 / 缩放份额得到的结果:

rendering/binding.dart中:

[Flutter] infinity与可翻滚布局

总之,屏幕会给最外层一个强制束缚,束缚为屏幕尺度自身对应的DPI数值。

1.2 Scaffold

在如下的结构中:

MaterialApp -> Scaffold -> AppBar
                        -> Column

Scaffold会遭到来自MaterialApp传递的束缚,与此同时,又会对Column进行束缚

属实是一个承上启下的进程。

1. MaterialApp

MateriApp直承遭到来自屏幕的强制束缚,宽度/高度数值分别为:392.7/825.5,它并不会改动布局的束缚行为,而是将所遭到的束缚完整地传递给下一层:Scaffold。

2. Scaffold

Scaffold遭到来MaterialApp的束缚,明显也是一个强制束缚,392.7/825.5,这会让Scaffold被强制撑满整个屏幕,重点来了,此刻Scaffold向子Widget传递的将不再是强制束缚,而是一个宽松束缚,例如AppBar所遭到的束缚是:392.7和[0,90.9]。

这个束缚标明AppBar的宽度必定是392.7,即撑满整个屏幕宽度;而高度可以在0到90.0之间恣意取值和浮动。

而Scaffold的body部分,则遭到了来自水平缓笔直轴两个方向的宽松束缚,其内部组件可以在

  • 水平轴:[0,392.7]的尺度束缚,它的宽度可以在这个区间内恣意取值;
  • 笔直轴:[0,734.5]的尺度束缚,它的高度可以在这个区间内恣意取值;

两个束缚内进行设置尺度。

3.Column

从中,咱们可以知道,Column收到了来自:

  • 水平轴:[0,392.7]的尺度束缚,它的宽度可以在这个区间内恣意取值;
  • 笔直轴:[0,734.5]的尺度束缚,它的高度可以在这个区间内恣意取值;

而Column对子组件,则给予了如下的束缚:

  • 水平轴:[0,392.7]的尺度束缚,它的宽度可以在这个区间内恣意取值;
  • 笔直轴:[0,Infinity]的尺度束缚,它的高度可以在这个区间内恣意取值;

言下之意是,水平轴上,Column并没有什么特别的处理,而是选择照搬父Widget给与的束缚,所以一个Widget,假如是文中Column的子Widget,它所遭到宽度束缚其实是来自父布局Scaffold的宽松束缚,该Widget可以恣意在[0,392.7]这个束缚区间内取值作为宽度。

而在笔直轴上,Column自身为线性布局,理论上可以包容无限的高度(参考Android中的LinearLayout),因而,Column会给予子Widget一个无束缚的高度束缚。

二、组件束缚分类

明显,从上面的比如中,咱们看到了几种不同的束缚传递行为:

  1. MaterialApp,明显这一类组件和布局、尺度并没有直接的联系,例如StatefulWidget、StatelessWidget这一类功能型的组件(乃至包括Container),它们所做的便是将自己遭到的束缚,原封不动地传递给下一层。

  2. Scaffold,Scaffold会遭到来自父布局的束缚,并向下传递一个宽松束缚,Scaffold遭到的外层束缚可能是: a.宽松束缚[0,a],[0,b],此刻Scaffold会向下一个宽松束缚:[0,a],[0,b] b.强制束缚:[a],[b],此刻Scaffold会向下一个宽松束缚:[0,a],[0,b]

所以Scaffold在这儿的行为便是将束缚,转为一个宽松束缚。不光是Scaffold,绝大多数的定位辅助组件,例如Align、Center等等,它们自身无论是遭到强制束缚,仍是宽松束缚,向下传递的都是一个宽松束缚,但不会超过所遭到束缚的最大值。

例如一个Center自身所受的束缚是:[20,40],[20,40],即宽高所遭到的束缚都是20,40的松束缚,此刻Center内部的组件收到的束缚的值将会是:[0,40]和[0,40],Center不会将左端点值20也强制束缚到child上:

        ConstrainedBox(
          constraints: BoxConstraints(
              minWidth: 20, minHeight: 20, maxHeight: 40, maxWidth: 40),
          child: Center(child: commonText().printLayoutInformation()),
        )
        // output
        Center's constraints:BoxConstraints(20.0<=w<=40.0, 20.0<=h<=40.0) 
        Text's constraints:BoxConstraints ( 0.0 <=w<=40.0 , 0.0 <=h<=40.0 ) 
  1. Column,这一类的组件因为特别的场景,它们天然可能会很长,所以不会束缚子Widget在mainAxis(主轴)上加以束缚,可是也因而增加了不确认性,可是咱们要分清楚两个东西:

首先是Column自身遭到的束缚; 它会影响到mainAxisSize特点的体现。 假如设置为mainAxisSize : MainAxisSize.min,那么Column尽可能地缩小,直到束缚[a,b]的左端点,也便是最小值a,假如Column的内容高度很短,乃至比束缚的左端点a还短,这个时分Column的高度依然会是a:

[Flutter] infinity与可翻滚布局

赤色区域的尺度是宽高为30的正方形,也便是Column的布局方位,外部包裹了一个ConstrainedBox,供给了一个[30,300],[30,300]的松束缚。

而Container是通明的蓝色,位于Column当中,明显Column不会强行缩小到和它唯一子Widget的高度一致,只会缩小到它的最小束缚30.假如设置为mainAxisSize : MainAxisSize.max,那么Column将会至少撑开到右端点b,假如Column的内容很长,现已超过b了,那么咱们就会看到这么个东西:

[Flutter] infinity与可翻滚布局

同理,赤色区域的尺度是宽高为300的正方形,也便是Column的布局宽度,外部包裹了一个ConstrainedBox,供给了一个[30,300],[30,300]的松束缚。

蓝色区域则是一个320×320的正方形,它在纵轴超出了Column的高度20个单位;而横轴则没有,这是因为强制束缚的原因,划定的320宽度在强制束缚下失效了,被Column强制设置成了300。

三、Column子Widget束缚与infinity

Column和Row并没有给与主轴上一个确认的束缚,而是给了一个Infinity,这对一些其他的组件来说,可能会导致问题。

3.1 Expanded

咱们通常会运用Expanded来占满一个Row或者Column的剩下的空间,假如有多个Expanded那么则默许会平分Row、Column的空间。

以Column为例,单个的Expanded会撑满Column所剩下的空间,这儿要留意的一点是,咱们前面说到了,Column会给予子Widget一个无束缚的高度束缚,可是理想状况下,Column自身又会遭到来自它父Widget的束缚,在如下的一棵Widget树中:

MaterialApp -> Scaffold -> Column -> Expanded

Column会遭到Scaffold下发的非强制束缚,例如:

Scaffold's constraints:BoxConstraints(w=384.0, h=592.0)
Column's constraints:BoxConstraints(0.0<=w<=384.0, 0.0<=h<=592.0)

这儿的592.0便是Column的剩下空间,Expanded默许会撑满这个剩下空间,假如Expanded有其余的同级别Widget,例如一个高度为20的Text,那么这个剩下空间就剩下592.0-20 = 572.0,Expanded则会撑满这个剩下高度,即572。

可是,并非一切状况都是这种理想状况,假如Column自身收到一个infinity的束缚,就会导致Column内的Expanded无法正确取得剩下高度,例如如下的结构:

Column(
  children: [
    Column(
      children: [
        Expanded(child: commonText().printLayoutInformation()),
      ],
    ).printLayoutInformation(),
  ],
).printLayoutInformation()

咱们会取得这样的一个报错:

[Flutter] infinity与可翻滚布局

假如咱们把Expanded删掉,直接用Text来展现,咱们就可以看到内层的Column的高度束缚了:

Scaffold's constraints:BoxConstraints(w=384.0, h=592.0)
Column's constraints:BoxConstraints(0.0<=w<=384.0, 0.0<=h<=592.0)
Column's constraints:BoxConstraints(0.0<=w<=384.0, 0.0<=h<=Infinity)
Text's constraints:BoxConstraints(0.0<=w<=384.0, 0.0<=h<=Infinity)

然而,不光是Column套Column会呈现这种状况,ListView嵌套Column也会呈现这种状况,内层的Column遭到来自ListView的Infinity的高度束缚时,此刻Expanded对于遭到的笔直轴方向上的束缚会被认为是unbounded的,无法正确地去获取剩下高度,处理Expanded行为。

总而言之,运用Expanded的时分,有必要要保证其父组件,以Column为例,它的主轴高度有必要是可知的,不可以是infinity。

其次,在考虑一些布局 & 尺度相关问题的时分,需要留意这个问题究竟是由Widget的束缚决议的,仍是子Widget所遭到的束缚决议的。

3.2 ListView

[Flutter] 为什么我的 ListView 又双叒叕崩了 – 中,咱们说到过:

Vertical viewport was given unbounded height.

翻译过来,便是:笔直的Viewport被赋予了未确认的高度

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

回忆一下问题自身,是因为呈现了这种层次的结构嵌套导致的:

Column -> ListView

ListView组件有一个很重要的概念,那便是Viewport。

什么是Viewport?

假如你熟悉Android开发,在运用ScrollView和LinearLayout进行组合构建可翻滚布局的时分,你必定会发现,ScrollView的高度一般是固定的,而LinearLayout的高度会特别长,咱们实际上是在ScrollView供给的一个确认的区域内,滑动显示LinearLayout的内容。

所以,Viewport的尺度有必要是确认的。

可是在Column中,直接嵌套一个ListView,则无法直接确认Viewport的高度

而假如咱们运用了shrinkWrap会产生什么呢?

if (shrinkWrap) {
  return ShrinkWrappingViewport(
    axisDirection: axisDirection,
    offset: offset,
    slivers: slivers,
    clipBehavior: clipBehavior,
  );
}
return Viewport(...);

明显,ShrinkWrappingViewport是一个特别的Viewport,和一般Viewport不同之处在于,这种动态的Viewport会去测量其子Widget们的尺度,并收缩到子Widget的尺度。

可是子Widget的高度随时可能产生改动,假如翻滚进程中需要对Viewport中展现的Header头进行折叠(相似SliverHeader的行为),就会导致Viewport的尺度也改动,是一种潜在的消耗性能的操作。

而一般Viewport则需要一开始就确认高度,相比之下它愈加高效,可是也具有必定的局限性,有必要显式地去确认内容的高度。

ShrinkWrap不是万能的

ShrinkWrap自身仅仅确认了Viewport的类型为一种动态测量子Widget高度的可变的贵重Viewport,可是ShrinkWrap供给的ShrinkWrappingViewport之后,ListView.children的子Widget,在笔直轴方向上,遭到的束缚依然是Infinity。

这就意味着,之前说到的在ListView中,运用Column的场景下,即便设置了ShrinkWrap,咱们依然无法运用Expanded,相关的报错依然会存在:

[Flutter] infinity与可翻滚布局

道理很简单,shrinkWrap并不改动高度束缚的值。一个Column,只要在ListView当中,那么它遭到的高度束缚便是[0,infinity],Column内的Expanded就无法知道它的高度最大可以是多少。

ListView的替代者

  • 问题剖析

那么在这种场景下,咱们既要滑动,又要撑满屏幕空间,咱们该如何处理呢?

其实,这儿的中心矛盾点就在于,Column直承遭到了来自ListView的Infinity束缚,导致Expanded无法获取到bounded的高度,假如咱们要处理这种场景,咱们只需要在Column外面套一层SizedBox,高度设置为具体的数值即可:

ListView(
children:[
    SizedBox(
        height:200,
        Column(...)
    )
]);

这样一来,SizedBox会遭到来自ListView的[0,infinifty]高度束缚,而且,SizedBox会将这个高度上的宽松束缚,强制转换成一个强制束缚,即SizedBox内部的高度束缚变成了:[200,200],也便是说Column的高度只能是200,此刻Column内的Expanded就可以取到Column的最大高度了。

但这种强制赋值的处理计划总让人感觉差点意思。事实也是不可能每次都预先知道SizedBox的长度,也便是说,必定会有一种场景,是需要咱们的可滑动布局的内部组件,一个特别的Widget,可以「再向上探一探」,撑满可滑动布局的可用空间,例如ListView所遭到的高度束缚为[0,a],其中a不为Infinity,这个特别的Widget的高度就可以自动设置为a,而不需要咱们再手动去探索它的高度应该是多少。

换句话说,就对应着Column中的Expanded。

  • CustomScrollView与SliverFillRemaining

咱们经过**CustomScrollViewSliverFillRemaining**的配合,就可以完成问题剖析中,说到的作用。

Widget _buildShrinkWrap() => ColoredBox(
      color: Colors.black12,
      child: CustomScrollView(
        slivers: [
          SliverFillRemaining(
            child: Column(
              children: [
                Expanded(child: commonText())
              ],
            ),
          )
        ],
      ),
    );

因为篇幅的原因,这儿只给出代码。要害之处就在SliverFillRemaining,它可以撑满CustomScrollView所可以触达的最大束缚,可是和Expanded + Column之间的特性上仍是有一些差异,可是在一些场景下,咱们仍是可以用这个组件来完成咱们的需求。

而具体是如何完成的,鄙人一章咱们将会引进一个和Flutter翻滚相关的新概念:Sliver。