在[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
中:
总之,屏幕会给最外层一个强制束缚,束缚为屏幕尺度自身对应的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一个无束缚的高度束缚。
二、组件束缚分类
明显,从上面的比如中,咱们看到了几种不同的束缚传递行为:
-
MaterialApp,明显这一类组件和布局、尺度并没有直接的联系,例如StatefulWidget、StatelessWidget这一类功能型的组件(乃至包括Container),它们所做的便是将自己遭到的束缚,原封不动地传递给下一层。
-
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 )
- Column,这一类的组件因为特别的场景,它们天然可能会很长,所以不会束缚子Widget在mainAxis(主轴)上加以束缚,可是也因而增加了不确认性,可是咱们要分清楚两个东西:
首先是Column自身遭到的束缚; 它会影响到mainAxisSize特点的体现。 假如设置为mainAxisSize : MainAxisSize.min,那么Column尽可能地缩小,直到束缚[a,b]的左端点,也便是最小值a,假如Column的内容高度很短,乃至比束缚的左端点a还短,这个时分Column的高度依然会是a:
赤色区域的尺度是宽高为30的正方形,也便是Column的布局方位,外部包裹了一个ConstrainedBox,供给了一个[30,300],[30,300]的松束缚。
而Container是通明的蓝色,位于Column当中,明显Column不会强行缩小到和它唯一子Widget的高度一致,只会缩小到它的最小束缚30.假如设置为mainAxisSize : MainAxisSize.max,那么Column将会至少撑开到右端点b,假如Column的内容很长,现已超过b了,那么咱们就会看到这么个东西:
同理,赤色区域的尺度是宽高为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()
咱们会取得这样的一个报错:
假如咱们把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,相关的报错依然会存在:
道理很简单,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
咱们经过**CustomScrollView
与SliverFillRemaining
**的配合,就可以完成问题剖析中,说到的作用。
Widget _buildShrinkWrap() => ColoredBox(
color: Colors.black12,
child: CustomScrollView(
slivers: [
SliverFillRemaining(
child: Column(
children: [
Expanded(child: commonText())
],
),
)
],
),
);
因为篇幅的原因,这儿只给出代码。要害之处就在SliverFillRemaining,它可以撑满CustomScrollView所可以触达的最大束缚,可是和Expanded + Column之间的特性上仍是有一些差异,可是在一些场景下,咱们仍是可以用这个组件来完成咱们的需求。
而具体是如何完成的,鄙人一章咱们将会引进一个和Flutter翻滚相关的新概念:Sliver。