欢迎关注微信公众号:FSA全栈行动 👋
在做 Flutter UI 时,我们最常见的布局方式是 Row(水平排列)、Column(垂直排列)或者 Wrap(自动换行)。这些布局逻辑都遵循一个基本原则:组件之间是"排排坐"的,各占各的空间。
但现实开发中,很多 UI 效果是必须要"叠"在一起的。比如:
-
在
Avatar(头像)的角落放一个红色的消息角标。 -
在视频缩略图的中心位置覆盖一个播放按钮。
-
在一个列表项的上方悬浮一个标签。
这时候,Row 和 Column 就搞不定了,这时候你需要的是 Stack 和 Positioned。这两个组件虽然看起来简单,但如果不理解其底层逻辑,写起来非常容易遇到布局报错或者尺寸异常。
二、核心逻辑
1、什么是 Stack?
Stack 的逻辑非常直观,就像你在 Photoshop 里开图层一样。Stack 会把它的子组件一层一层地叠上去。
这里有一个非常关键的特性:声明顺序决定叠放顺序。
-
列表里的第一个子组件在最底层。
-
列表里的最后一个子组件在最顶层。
这里不需要 zIndex 这种概念,直接调整 children 列表里的顺序就能实现"谁压在谁上面"。
Dart
Stack(
children: [
Container(width: 200, height: 200, color: Colors.blue),
const Text('我在上面'), // 这个 Text 会盖在蓝色方块上面
],
)
如果不给子组件指定具体的定位参数,默认情况下,所有子组件都会对齐到 Stack 的左上角。
2、如何调整对齐方式
如果你想让所有子组件都居中,或者统一靠右对齐,可以直接设置 Stack 的 alignment 属性:
Dart
Stack(
alignment: Alignment.center, // 所有非定位子组件都会居中
children: [
Container(width: 200, height: 200, color: Colors.blue),
const Text('我在中间'),
],
)
但如果你希望不同的组件在不同的位置,比如一个在左上角,一个在右下角,这时候就需要用到 Align 组件了。
3、Positioned:实现绝对定位
Positioned 是专门用来控制组件在 Stack 内部精确坐标的。你可以通过 top、right、bottom、left 来指定它相对于父容器边距的距离。
Dart
Stack(
children: [
Container(width: 200, height: 200, color: Colors.blue),
Positioned(
top: 8,
right: 8,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
)
这段代码会在右上角放一个距离边缘 8 像素的红点,这正是实现"消息角标"的标准做法。
小技巧: 如果你同时设置了 left: 0 和 right: 0,那么这个子组件就会在水平方向上被"拉伸"到填满整个 Stack 的宽度。
三、进阶细节与踩坑指南
在使用 Stack 的过程中,有几个非常容易踩到的"深坑",我整理了一下,大家开发时一定要留意。
1、Stack 的尺寸由谁决定?
这是一个非常经典的错误。默认情况下(即 StackFit.loose),Stack 的尺寸是由它最大的 非定位(non-positioned) 子组件决定的。
如果你在 Stack 里只放了 Positioned 组件,而没有放其他的组件,那么 Stack 会发现自己没有任何尺寸参考,最终结果就是------Stack 的尺寸会塌陷为 0。
解决方案:
-
给
Stack加一个非定位的子组件作为"撑开基准"。 -
或者给
Stack外层套一个SizedBox指定尺寸。 -
或者设置
fit: StackFit.expand让Stack直接撑满父容器。
2、Positioned 的"唯一合法位置"
这是新手最常遇到的 Assertion Error。Positioned 组件只能作为 Stack 的直接子组件。
如果你试图这样做:
Dart
Stack(
children: [
Padding( // 错误!Padding 不是 Stack 的直接子组件
padding: EdgeInsets.all(8),
child: Positioned(top: 0, child: Text('Error!')),
),
],
)
这会直接导致运行时报错。因为 Positioned 依赖于 Stack 提供的特殊上下文数据(ParentData),一旦中间隔了 Padding 或 Container 等组件,这种关系就断了。
3、溢出与裁剪
默认情况下,如果子组件的尺寸超过了 Stack 的范围,多出的部分会被直接切掉(clipBehavior: Clip.hardEdge)。
如果你要做一些"悬浮"效果,比如一个角标需要超出头像的边界,记得设置:
Dart
Stack(
clipBehavior: Clip.none, // 允许子组件绘制到 Stack 边界之外
children: [...],
)
| 属性 | 效果 | 适用场景 |
|---|---|---|
alignment |
统一控制所有非定位组件的对齐 | 简单居中或统一靠边 |
Align |
允许每个组件有不同的对齐方式 | 灵活的相对位置控制 |
Positioned |
像素级的绝对定位 | 精确的角标、遮盖层 |
四、实战演练:做一个媒体卡片
下面我用 Stack 组合一下,做一个常见的视频媒体卡片。这种布局集成了图片填充、渐变遮罩和文字覆盖,是实战中非常高频的场景。
Dart
Stack(
children: [
// 1. 底层:全屏填充的图片
// Positioned.fill 是一个便捷写法,等同于 left/right/top/bottom 全设为 0
Positioned.fill(
child: Image.network(
'https://example.com/video.jpg',
fit: BoxFit.cover,
),
),
// 2. 中间层:半透明黑底渐变,为了保证白色文字在浅色图片上也能看清
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 80,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black87],
),
),
),
),
// 3. 顶层:左下角的标题文字
const Positioned(
left: 16,
right: 16,
bottom: 16,
child: Text(
'这是一段很棒的视频标题',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
// 4. 顶层:居中的播放按钮
const Positioned.fill(
child: Center(
child: Icon(Icons.play_circle_fill, size: 48, color: Colors.white),
),
),
],
)
五、最后
总结一下,Stack 的设计初衷就是为了处理"重叠"。
-
想实现相对位置 (如居中、靠边),用
Align或Stack.alignment。 -
想实现绝对像素位置 ,用
Positioned。 -
如果发现布局塌陷,检查有没有非定位的子组件在撑开尺寸。
-
如果发现组件被切断 ,检查
clipBehavior。
掌握了这套逻辑,Flutter 里的 UI 重叠问题基本上就迎刃而解了。
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~