Files
2026-09-04 15:22:20 +08:00

120 lines
4.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【Demo】Stack + Positioned 层叠布局
> 源文件:`lib/pages/demo/stack_demo.dart` | 分类:基础布局
> 一句话:UI 重叠 = CSS 的 `position: absolute`,本页附赠一个真实踩过的"全 Positioned 尺寸崩溃"大坑。
---
## 一、这个 Demo 解决什么问题
做"头像 + 右上角角标"、"卡片 + 半透明蒙层"、"悬浮按钮"这类**元素重叠**的 UI。
Flutter 用 `Stack` 实现:
- `Stack` = 图层容器(**后写的子组件盖在上面**);
- `Positioned` = 给子组件做绝对定位(**只能直接放在 Stack 的 children 里**)。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: StackDemoPage()));
```
页面演示 5 块内容:
1. 基础重叠:蓝色盒子 + 右上角悬空角标 + 居中文字;
2. `Positioned.fill` 半透明蒙层;
3. 坑①复现:全 Positioned 时 Stack 尺寸异常;
4. 修复方案 1:SizedBox 固定宽高;
5. 修复方案 2:非 Positioned 底层组件撑尺寸。
---
## 三、核心知识点(新手向)
### 1. Stack 的两个核心参数
```dart
Stack(
alignment: Alignment.topLeft, // 对【没被 Positioned 包裹】的子组件做默认对齐
clipBehavior: Clip.none, // 允许子组件绘制超出 Stack 边界(默认 Clip.hardEdge 会裁剪)
children: [...],
)
```
### 2. Positioned 的用法
```dart
Positioned(
top: 10, // 距 Stack 上边
right: -10, // 负值 = 超出边界(配合 clipBehavior: Clip.none 才能显示)
child: ...,
)
Positioned.fill( // 填满整个 Stack —— 做蒙层最方便
child: Container(color: Colors.black54),
)
```
### 3. 布局规则一句话
Stack 的尺寸 = **非 Positioned 子组件**决定的(取其中最大的那个);
Positioned 组件只是"贴"在上面,不参与 Stack 自身尺寸计算。
---
## 四、关键代码剖析(含大坑)
### 坑①:全部子组件都是 Positioned → Stack 尺寸异常
```dart
// 真实行为:Stack 尺寸不由内容决定,直接取父约束最大值(constraints.biggest)
Stack(
children: [
Positioned(top: 10, left: 10, child: Text("A")),
],
)
```
- 放在**有界约束**里(如 SizedBox 300×60)→ Stack 占满整个 300×60,而不是收缩到文字大小;
- 裸放在 **Column 等无界高度**里 → debug 模式直接断言崩溃:
`A Stack requires bounded constraints`,**整个 body 都画不出来**(只看到标题栏)。
### 修复方案 1:给 Stack 套 SizedBox 固定宽高
```dart
SizedBox(
width: 300.w,
height: 80.h,
child: Stack(children: [Positioned(top: 10, left: 10, child: Text("A"))]),
)
```
### 修复方案 2:放一个非 Positioned 的底层组件撑起大小
```dart
Stack(
children: [
ColoredBox( // 非 Positioned 底层组件:由它决定 Stack 尺寸
color: Color(0xFFE8E8E8),
child: SizedBox(width: 300.w, height: 80.h),
),
Positioned(top: 10, left: 10, child: Text("A")),
],
)
```
> 经验:**Stack 里永远留一个"地基"**(非 Positioned 的底层组件或外层 SizedBox),
> 既避免崩溃,也方便后续加 Positioned 不担心尺寸突变。
---
## 五、踩坑记录
1. **Positioned 只能直接作为 Stack 的 children**,中间套了其他 Widget 会失效(编译警告)。
2. **做悬浮角标务必 `clipBehavior: Clip.none`**,否则负偏移部分被裁掉看不见。
3. **全 Positioned 的 Stack 在无界约束下崩溃**(见上),这是本页作者真实遇到过的 bug:页面只剩标题。
4. `Container` 在 Flutter 3.44 已不是 const 构造,外层 `Center` 别写 `const`(编译报错)。
5. 想让内容很短也能拉动滚动(演示页效果),用 `physics: BouncingScrollPhysics()` / `AlwaysScrollableScrollPhysics()`。
---
## 六、扩展思考
- 蒙层进阶:`Positioned.fill` + 半透明色 + 点击手势 = 简易弹窗/引导层;
- 角标进阶:红点角标(Badge 组件)本质也是 Stack + Positioned 的封装;
- 与 [list_demo.md](list_demo.md) 对照:列表右下角的"回到顶部"FAB 就是 `Stack + Positioned` 实现的;
- 与 [smooth_sheet_demo.md](smooth_sheet_demo.md) 对照:地图背景 + 底部面板就是"Stack 背景层 + 面板层"的组合。