第4章:布局类组件 —— 4.6 层叠布局(Stack、Positioned)

67 阅读8分钟

4.6 层叠布局(Stack、Positioned)

📚 章节概览

层叠布局允许子组件按照代码中声明的顺序堆叠起来,本章节将学习:

  • Stack - 层叠布局容器
  • alignment - 对齐方式
  • fit - 未定位子组件的适配方式
  • Positioned - 绝对定位
  • left/top/right/bottom - 四边距离
  • 部分定位 - 只在某个轴上定位
  • clipBehavior - 裁剪行为
  • 实际应用 - 常见UI场景

🎯 核心知识点

什么是层叠布局

层叠布局类似于:

  • Webposition: absolute
  • AndroidFrameLayout
  • 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 主要属性

属性类型默认值说明
alignmentAlignmentGeometrytopStart未定位/部分定位子组件的对齐方式
textDirectionTextDirection?null文本方向(影响start/end)
fitStackFitloose未定位子组件的适配方式
clipBehaviorCliphardEdge超出部分的裁剪行为
childrenList<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的特殊用法

⚠️ 约束规则
水平方向:只能指定 leftrightwidth 中的两个
垂直方向:只能指定 topbottomheight 中的两个
合法的组合
// ✅ 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 对齐。

横轴定位属性:leftright
纵轴定位属性:topbottom

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 部分定位规则

定位情况横轴行为纵轴行为
未定位alignmentalignment
只定位left/right按定位值alignment
只定位top/bottomalignment按定位值
完全定位按定位值按定位值

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的大小取决于以下因素:

  1. 有父约束 → 尽可能大
  2. 无父约束 → 由子组件决定
  3. 使用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:实现一个带徽章的图标

目标: 创建一个右上角带数字徽章的通知图标

步骤:

  1. 使用Stack布局
  2. 底层放置Icon
  3. 用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:实现一个图片卡片(图片+遮罩+文字)

目标: 创建底部有渐变遮罩和文字的图片卡片

步骤:

  1. 使用Stack布局
  2. 底层放置图片
  3. 中层添加渐变遮罩
  4. 顶层用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只对未定位子组件生效

记忆技巧

  1. 堆叠顺序:后面的在上面(z-index递增)
  2. 部分定位:定位的轴用定位值,未定位的轴用alignment
  3. fit.expand:未定位组件填满Stack
  4. Positioned.fill:填满整个Stack

🔗 相关资源