4.6 层叠布局(Stack、Positioned)
📚 章节概览
层叠布局允许子组件按照代码中声明的顺序堆叠起来,本章节将学习:
- Stack - 层叠布局容器
- alignment - 对齐方式
- fit - 未定位子组件的适配方式
- Positioned - 绝对定位
- left/top/right/bottom - 四边距离
- 部分定位 - 只在某个轴上定位
- clipBehavior - 裁剪行为
- 实际应用 - 常见UI场景
🎯 核心知识点
什么是层叠布局
层叠布局类似于:
- Web 的
position: absolute - Android 的
FrameLayout - iOS 的子视图叠加
Stack(
children: [
Container(color: Colors.red), // 底层
Container(color: Colors.green), // 中层
Container(color: Colors.blue), // 顶层
],
)
Stack vs Positioned
graph TB
A[Stack] -->|容器| B[提供层叠空间]
C[Positioned] -->|定位| D[指定子组件位置]
style A fill:#e1f5ff
style C fill:#ffe1e1
1️⃣ Stack(层叠容器)
1.1 构造函数
Stack({
Key? key,
AlignmentGeometry alignment = AlignmentDirectional.topStart,
TextDirection? textDirection,
StackFit fit = StackFit.loose,
Clip clipBehavior = Clip.hardEdge,
List<Widget> children = const <Widget>[],
})
1.2 主要属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
alignment | AlignmentGeometry | topStart | 未定位/部分定位子组件的对齐方式 |
textDirection | TextDirection? | null | 文本方向(影响start/end) |
fit | StackFit | loose | 未定位子组件的适配方式 |
clipBehavior | Clip | hardEdge | 超出部分的裁剪行为 |
children | List<Widget> | [] | 子组件列表 |
1.3 基础用法
Stack(
children: [
Container(
width: 150,
height: 150,
color: Colors.red,
),
Container(
width: 120,
height: 120,
color: Colors.green,
),
Container(
width: 90,
height: 90,
color: Colors.blue,
),
],
)
效果: 三个盒子从大到小堆叠,蓝色在最上层
1.4 堆叠规则
children: [
Widget1, // 最底层(z-index = 0)
Widget2, // 中层(z-index = 1)
Widget3, // 最上层(z-index = 2)
]
规则:
- 后面的组件会覆盖前面的组件
- 类似CSS的
z-index顺序 - 第一个组件在最底层
2️⃣ alignment(对齐方式)
2.1 什么是未定位/部分定位
| 定位状态 | 说明 | 示例 |
|---|---|---|
| 未定位 | 没有使用Positioned包裹 | Container(...) |
| 部分定位 | 只在一个轴上定位 | Positioned(left: 10, child: ...) |
| 完全定位 | 两个轴都定位 | Positioned(left: 10, top: 20, child: ...) |
2.2 Alignment枚举值
// 9个预定义对齐方式
Alignment.topLeft Alignment.topCenter Alignment.topRight
Alignment.centerLeft Alignment.center Alignment.centerRight
Alignment.bottomLeft Alignment.bottomCenter Alignment.bottomRight
2.3 alignment示例
center(居中,默认)
Stack(
alignment: Alignment.center, // 居中对齐
children: [
Container(width: 100, height: 60, color: Colors.blue),
],
)
效果: 蓝色盒子在Stack中心
topLeft(左上)
Stack(
alignment: Alignment.topLeft,
children: [
Container(width: 100, height: 60, color: Colors.blue),
],
)
效果: 蓝色盒子在Stack左上角
bottomRight(右下)
Stack(
alignment: Alignment.bottomRight,
children: [
Container(width: 100, height: 60, color: Colors.blue),
],
)
效果: 蓝色盒子在Stack右下角
3️⃣ Positioned(绝对定位)
3.1 构造函数
const Positioned({
Key? key,
this.left, // 距左边的距离
this.top, // 距顶部的距离
this.right, // 距右边的距离
this.bottom, // 距底部的距离
this.width, // 宽度
this.height, // 高度
required Widget child,
})
3.2 定位属性
| 属性 | 说明 | 示例 |
|---|---|---|
left | 距Stack左边的距离 | left: 10 |
top | 距Stack顶部的距离 | top: 20 |
right | 距Stack右边的距离 | right: 10 |
bottom | 距Stack底部的距离 | bottom: 20 |
width | 子组件宽度 | width: 100 |
height | 子组件高度 | height: 50 |
3.3 四角定位
左上角
Positioned(
left: 10,
top: 10,
child: Container(
width: 60,
height: 40,
color: Colors.red,
),
)
右上角
Positioned(
right: 10,
top: 10,
child: Container(
width: 60,
height: 40,
color: Colors.green,
),
)
左下角
Positioned(
left: 10,
bottom: 10,
child: Container(
width: 60,
height: 40,
color: Colors.blue,
),
)
右下角
Positioned(
right: 10,
bottom: 10,
child: Container(
width: 60,
height: 40,
color: Colors.orange,
),
)
3.4 填充整个Stack
Positioned(
left: 0,
top: 0,
right: 0,
bottom: 0,
child: Container(color: Colors.blue),
)
// 等价于
Positioned.fill(
child: Container(color: Colors.blue),
)
3.5 width/height的特殊用法
⚠️ 约束规则
水平方向:只能指定 left、right、width 中的两个
垂直方向:只能指定 top、bottom、height 中的两个
合法的组合
// ✅ left + width
Positioned(left: 10, width: 100, child: ...)
// ✅ right + width
Positioned(right: 10, width: 100, child: ...)
// ✅ left + right(宽度自动计算)
Positioned(left: 10, right: 10, child: ...)
// ✅ top + height
Positioned(top: 20, height: 50, child: ...)
// ✅ bottom + height
Positioned(bottom: 20, height: 50, child: ...)
// ✅ top + bottom(高度自动计算)
Positioned(top: 20, bottom: 20, child: ...)
❌ 非法的组合
// ❌ 指定了left、right、width(三个都指定)
Positioned(
left: 10,
right: 10,
width: 100, // 错误!会报错
child: ...,
)
4️⃣ 部分定位
4.1 什么是部分定位
部分定位: 只在一个轴上使用定位属性,另一个轴按 alignment 对齐。
横轴定位属性:left、right
纵轴定位属性:top、bottom
4.2 示例分析
Stack(
alignment: Alignment.center, // 居中对齐
children: [
// 1. 未定位:完全按alignment居中
Container(
width: 100,
height: 60,
color: Colors.red,
child: Text('未定位\n完全居中'),
),
// 2. 只定位left:水平定位,垂直居中
Positioned(
left: 18,
child: Container(
width: 80,
height: 40,
color: Colors.green,
child: Text('left=18\n垂直居中'),
),
),
// 3. 只定位top:垂直定位,水平居中
Positioned(
top: 18,
child: Container(
width: 80,
height: 40,
color: Colors.blue,
child: Text('top=18\n水平居中'),
),
),
],
)
4.3 部分定位规则
| 定位情况 | 横轴行为 | 纵轴行为 |
|---|---|---|
| 未定位 | alignment | alignment |
| 只定位left/right | 按定位值 | alignment |
| 只定位top/bottom | alignment | 按定位值 |
| 完全定位 | 按定位值 | 按定位值 |
5️⃣ StackFit(适配方式)
5.1 StackFit枚举值
| 枚举值 | 说明 | 行为 |
|---|---|---|
| loose | 松约束(默认) | 使用子组件自己的大小 |
| expand | 扩展 | 扩展到Stack的大小 |
| passthrough | 透传 | 透传父组件的约束 |
5.2 loose vs expand
loose(默认)
Stack(
fit: StackFit.loose,
children: [
Container(
width: 100, // 使用自己的尺寸
height: 60,
color: Colors.red,
),
],
)
效果: Container保持100×60的尺寸
expand
Stack(
fit: StackFit.expand,
children: [
Container(
// width和height会被忽略
color: Colors.blue,
child: Text('填满Stack'),
),
],
)
效果: Container填满整个Stack
5.3 实际效果对比
// 假设Stack大小为 200×150
// loose:子组件保持自己的大小
Stack(fit: StackFit.loose, ...)
→ 子组件:100×60
// expand:子组件扩展到Stack大小
Stack(fit: StackFit.expand, ...)
→ 子组件:200×150
6️⃣ clipBehavior(裁剪行为)
6.1 Clip枚举值
| 枚举值 | 说明 | 性能 |
|---|---|---|
| none | 不裁剪 | ⭐⭐⭐⭐⭐ |
| hardEdge | 直接裁剪(默认) | ⭐⭐⭐⭐ |
| antiAlias | 抗锯齿裁剪 | ⭐⭐⭐ |
| antiAliasWithSaveLayer | 高质量抗锯齿 | ⭐⭐ |
6.2 示例
Stack(
clipBehavior: Clip.hardEdge, // 裁剪超出部分
children: [
Positioned(
left: -20, // 部分超出Stack范围
top: 10,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
],
)
效果: 超出Stack的部分会被裁剪
🤔 常见问题(FAQ)
Q1: Stack的大小如何确定?
A: Stack的大小取决于以下因素:
- 有父约束 → 尽可能大
- 无父约束 → 由子组件决定
- 使用ConstrainedBox → 按约束大小
// 方法1:用ConstrainedBox指定大小
ConstrainedBox(
constraints: BoxConstraints.expand(), // 占满屏幕
child: Stack(...),
)
// 方法2:用SizedBox指定大小
SizedBox(
width: 300,
height: 200,
child: Stack(...),
)
// 方法3:让Stack填满父组件
Container(
width: double.infinity,
height: double.infinity,
child: Stack(...),
)
Q2: 如何让Positioned组件居中?
A: 三种方法:
方法1:使用alignment(推荐)
Stack(
alignment: Alignment.center,
children: [
// 不用Positioned包裹,自动居中
Container(width: 100, height: 100, color: Colors.blue),
],
)
方法2:计算居中位置
LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
Positioned(
left: (constraints.maxWidth - 100) / 2,
top: (constraints.maxHeight - 100) / 2,
child: Container(width: 100, height: 100, color: Colors.blue),
),
],
);
},
)
方法3:使用Align
Stack(
children: [
Align(
alignment: Alignment.center,
child: Container(width: 100, height: 100, color: Colors.blue),
),
],
)
Q3: Positioned.fill的作用?
A: 快捷方式,填满整个Stack
// Positioned.fill
Positioned.fill(
child: Container(color: Colors.blue),
)
// 等价于
Positioned(
left: 0,
top: 0,
right: 0,
bottom: 0,
child: Container(color: Colors.blue),
)
Q4: 如何实现z-index效果?
A: 通过调整children的顺序
Stack(
children: [
Container(color: Colors.red), // z-index: 0(最底层)
Container(color: Colors.green), // z-index: 1
Container(color: Colors.blue), // z-index: 2(最上层)
],
)
// 要改变层级,只需调整顺序
Stack(
children: [
Container(color: Colors.blue), // 现在在底层
Container(color: Colors.green),
Container(color: Colors.red), // 现在在顶层
],
)
Q5: Stack中如何响应底层组件的点击?
A: 使用 IgnorePointer 或调整顺序
Stack(
children: [
// 底层按钮
ElevatedButton(
onPressed: () => print('底层按钮'),
child: Text('底层'),
),
// 顶层遮罩(忽略点击)
IgnorePointer(
child: Container(
color: Colors.black.withOpacity(0.3),
),
),
],
)
🎯 跟着做练习
练习1:实现一个带徽章的图标
目标: 创建一个右上角带数字徽章的通知图标
步骤:
- 使用Stack布局
- 底层放置Icon
- 用Positioned定位徽章到右上角
💡 查看答案
class BadgeIcon extends StatelessWidget {
final IconData icon;
final int count;
const BadgeIcon({
super.key,
required this.icon,
required this.count,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 50,
height: 50,
child: Stack(
children: [
// 主图标
Icon(icon, size: 40, color: Colors.grey[700]),
// 徽章(仅当count>0时显示)
if (count > 0)
Positioned(
right: 0,
top: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Center(
child: Text(
count > 99 ? '99+' : '$count',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
);
}
}
// 使用示例
Row(
children: [
BadgeIcon(icon: Icons.notifications, count: 5),
SizedBox(width: 24),
BadgeIcon(icon: Icons.shopping_cart, count: 12),
SizedBox(width: 24),
BadgeIcon(icon: Icons.mail, count: 99),
],
)
练习2:实现一个图片卡片(图片+遮罩+文字)
目标: 创建底部有渐变遮罩和文字的图片卡片
步骤:
- 使用Stack布局
- 底层放置图片
- 中层添加渐变遮罩
- 顶层用Positioned定位文字
💡 查看答案
class ImageCard extends StatelessWidget {
final String title;
final String subtitle;
const ImageCard({
super.key,
required this.title,
required this.subtitle,
});
@override
Widget build(BuildContext context) {
return Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: [
// 背景图片(这里用渐变代替)
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[700]!, Colors.blue[400]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
// 底部渐变遮罩
Positioned(
left: 0,
right: 0,
bottom: 0,
height: 100,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
),
// 文字内容
Positioned(
left: 16,
right: 16,
bottom: 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
],
),
),
);
}
}
// 使用示例
ImageCard(
title: 'Beautiful Landscape',
subtitle: 'A stunning view of nature',
)
📋 小结
核心概念
| 组件 | 说明 | 使用场景 |
|---|---|---|
| Stack | 层叠容器 | 组件需要重叠显示 |
| Positioned | 绝对定位 | 精确控制子组件位置 |
Stack常用属性
| 属性 | 说明 | 常用值 |
|---|---|---|
alignment | 对齐方式 | center、topLeft |
fit | 适配方式 | loose、expand |
clipBehavior | 裁剪行为 | hardEdge |
Positioned常用属性
| 属性 | 说明 | 示例 |
|---|---|---|
left | 距左边距离 | left: 10 |
top | 距顶部距离 | top: 20 |
right | 距右边距离 | right: 10 |
bottom | 距底部距离 | bottom: 20 |
定位规则
1. 未定位 → 按alignment对齐
2. 部分定位 → 一个轴定位,另一个轴按alignment
3. 完全定位 → 两个轴都按定位值
4. fit只对未定位子组件生效
记忆技巧
- 堆叠顺序:后面的在上面(z-index递增)
- 部分定位:定位的轴用定位值,未定位的轴用alignment
- fit.expand:未定位组件填满Stack
- Positioned.fill:填满整个Stack