1.2.4 Stack
除了前面介绍的Column和Row组件都可以将一系列子组件由一个方向依次摆放外,在布局中还有一种较为常见的情况,就是叠放。例如,在图片上加水印、在用户头像加上一对猫耳朵、把54张扑克牌的图片叠在一起变成一副牌等。
Stack就是一个可将子组件叠在一起显示的容器组件。由于它需要同时叠放多个子组件,所以传入子组件的属性不是child,而是复数形式children,代码如下:
图1-21 用Stack将2个FluterLogo组件叠放显示
这样就可以把2个FlutterLogo组件叠放在一起,运行效果如图1-21所示。
1.布局算法
仔细观察上例不难发现,对于不同尺寸的组件,Stack默认将它们沿着左上角对齐。实战中一般需要精确地控制叠放的位置,这就涉及Stack的组件布局算法了,但在此之前,不得不先提一个与Stack密切相关的组件:Positioned。实际上,Stack里的子组件在布局时会被分为“有位置”和“无位置”这2大类,而其中“有位置”特指被Positioned包裹的子组件。
布局时,Stack首先会找到所有“无位置”的子组件,并向它们传入fit属性所设置的布局约束,如“不超过父级组件尺寸”等,允许它们一定程度内自由选择自身的尺寸,并让它们依次汇报最终确定的尺寸结果。在得到全部“无位置”子组件所确定的最终尺寸后,Stack会把自身尺寸匹配到其中最大的子组件的尺寸,再把其他(可能同等尺寸,或者较小)的子组件按照alignment属性设置的对齐方式摆放。如果没有设置对齐方式,则默认为左上角(在从右到左阅读习惯的设备上默认为右上角)。如果Stack里不存在“无位置”的子组件,即全部子组件都是Positioned组件,则Stack会尽量将自身尺寸设置为父级布局约束所允许的最大尺寸,为对齐children创造条件。
一旦全部“无位置”的子组件都安置到位,同时Stack自身尺寸也确定完毕后,接下来就可以安排“有位置”的Positioned子组件了。具体布局算法将在1.2.5节介绍Positioned组件时详细讲解。
1)fit属性
尺寸适配属性用于控制Stack如何将自己的父级组件的尺寸约束传达给“无位置”的子组件,类型是StackFit,默认值是StackFit. loose(宽松)。例如,某Stack的父级组件要求Stack的尺寸为200×200~500×500单位,在默认的宽松状态下,Stack可以允许其children在不违背父级约束的前提下,自由选择尺寸,即可在0×0~500×500单位任意选择。相反,如果传入fit:StackFit. expand(扩张),则Stack会要求所有“无位置”的children必须占满父级约束的最大空间,即尺寸必须为500×500单位。最后,当传入StackFit. passthrough(穿透)时,Stack会将自己父级组件的尺寸约束直接传给子组件,即保持原有的200×200~500×500单位的约束。这些“尺寸约束”的内容相对复杂,初学者也可等阅读完第6章“进阶布局”后再来回顾这部分内容。
有些初学者容易误认为fit属性用于控制Stack的尺寸,StackFit. expand似乎是让Stack自身“扩张”到最大尺寸。实际上,fit属性只是帮助约束Stack中“无位置”的那些子组件的尺寸。前面提到,在布局的过程中,Stack最终会将自身尺寸适配到“无位置”子组件中的最大的那个,因此,当这里fit参数被设置为StackFit. expand时,实际上是Stack要求了“无位置”子组件尽量放大,自己再适配变大后的子组件,看起来就好像是Stack自身尺寸直接变大,但实际上Stack自身尺寸变大只是子组件变大后的一个副作用。
2)alignment属性
如果子组件的尺寸小于Stack本身,alignment属性可用来指定对齐方式。类型为Alignment类,需同时指定水平和垂直2个维度的对齐,如Alignment.topCenter等。对此不熟悉的读者可参考本章之前Container组件中同名属性的相关介绍。
这里需要指出的是,如果子组件已经使用了Positioned对齐,但没有设置完整,则默认的维度依然会按照这里alignment属性所设置的方式对齐。例如,这里通过Positioned组件要求底边留白10单位,而不特别说明水平方向的对齐情况,代码如下:
上述代码中,若不设置alignment 属性,则Stack依然会按照默认的左上对齐,再遵循Positioned对底边(垂直方向)的要求,将组件绘制在左下角,并与底边保留10单位的空白,如图1-22(a)所示,但因为上述代码通过alignment属性修改了默认对齐方式为右上,实际运行时再结合底边留白10单位的要求,最终Positioned的叠放位置就会变成右下角,如图1-22(b)所示。
图1-22 确定底边留白后默认对齐仍有作用
2.叠放次序
Stack在渲染时会将子组件按照children属性里的列表顺序依次绘制并覆盖叠放,因此列表里第一个组件会首先被绘制,于是出现在最底层,容易被其他组件覆盖,而列表里最后一个组件则会被绘制到最顶层,可能遮住其他组件,但自身不会被Stack中的其他子组件遮挡。
3.溢出
默认情况下,溢出Stack尺寸边界的子组件会被裁剪,不予显示。这一行为是由clipBehavior属性控制的,默认为Clip. hardEdge,即迅速地裁剪(不启用抗锯齿)多余部分。若需显示溢出的组件,则开发者可将clipBehavior的属性修改为Clip. none,要求不裁剪,代码如下:
图1-23 Stack子组件溢出部分的裁剪行为
图1-23展示了子组件溢出部分“裁剪”(图(a))和“不裁剪”(图(b))的区别。
这里需要注意的是,虽然溢出Stack的内容可以通过修改裁剪行为绘制出来,但这些区域依然不会被算作触摸手势的识别范围。例如某个按钮一半在内一半在外,则只有在Stack范围内的那半个按钮可响应用户的单击事件。Flutter框架的这部分行为目前稍有争议(5),也许未来某个Flutter版本中会改变,但就目前来讲,如果溢出的组件是如按钮等需要用户交互的界面元素,则一定要注意测试这一行为,或者尽量想办法通过更好地设置Stack本身尺寸避免出现子组件溢出。
另外,读者可能会在一些旧版本的Flutter代码中见到Stack组件的Overflow属性,包括Overflow. clip(裁边)与Overflow. visible(可见)这2种值。这是旧版本的遗留属性,用于设置同样的行为,但也可能会在未来版本中被移除,因此目前推荐使用这里介绍的clipBehavior属性。