120 lines
4.2 KiB
Markdown
120 lines
4.2 KiB
Markdown
# 【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 背景层 + 面板层"的组合。
|