第7讲:常用基础与布局Widget(二):Stack, Expanded, SizedBox

掌握层叠布局与空间分配,构建更复杂的UI界面。

你好,欢迎回到《Flutter入门到精通》专栏。在上一讲中,我们学习了Container、Row和Column这三个基础布局Widget。今天我们将继续探索另外三个极其重要的布局Widget:Stack、Expanded和SizedBox,它们将帮助您解决更复杂的布局需求。

一、层叠布局:Stack与Positioned

Stack 允许您将多个子Widget重叠在一起。这在需要创建重叠效果、浮动按钮、徽章(badge)等场景下非常有用。

1.1 Stack 的基本用法

dart

Stack(
  children: <Widget>[
    // 底层:背景图片
    Container(
      width: 300,
      height: 200,
      color: Colors.grey[300],
    ),
    // 中层:文字内容
    Positioned(
      bottom: 20,
      left: 20,
      child: Text(
        'Flutter开发',
        style: TextStyle(
          fontSize: 24,
          color: Colors.white,
          fontWeight: FontWeight.bold,
        ),
      ),
    ),
    // 上层:徽章
    Positioned(
      top: 10,
      right: 10,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          'NEW',
          style: TextStyle(color: Colors.white, fontSize: 12),
        ),
      ),
    ),
  ],
)

1.2 Stack 的对齐方式

Stack 通过 alignment 属性来控制未定位子Widget的默认对齐方式:

dart

Stack(
  alignment: Alignment.center, // 所有未定位的子Widget都会居中对齐
  children: <Widget>[
    Container(
      width: 200,
      height: 200,
      color: Colors.blue,
    ),
    // 这个Text没有使用Positioned,所以会遵循Stack的alignment
    Text(
      '居中文本',
      style: TextStyle(color: Colors.white, fontSize: 18),
    ),
  ],
)

1.3 Positioned 的精确控制

Positioned 用于在Stack中精确控制子Widget的位置:

dart

Positioned(
  top: 20,    // 距离顶部的距离
  left: 30,   // 距离左侧的距离
  right: 40,  // 距离右侧的距离
  bottom: 50, // 距离底部的距离
  width: 100, // 宽度(如果同时指定left和right,width会被忽略)
  height: 80, // 高度(如果同时指定top和bottom,height会被忽略)
  child: YourWidget(),
)

重要提示:在 Positioned 中,您可以组合使用 top/bottom 和 left/right 来实现灵活的定位。

1.4 实战:用户头像叠加效果

dart

class AvatarStack extends StatelessWidget {
  const AvatarStack({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // 第一个头像
        CircleAvatar(
          radius: 30,
          backgroundImage: NetworkImage('https://example.com/avatar1.jpg'),
        ),
        // 第二个头像,偏移显示
        Positioned(
          left: 20,
          child: CircleAvatar(
            radius: 30,
            backgroundImage: NetworkImage('https://example.com/avatar2.jpg'),
          ),
        ),
        // 第三个头像,更多偏移
        Positioned(
          left: 40,
          child: CircleAvatar(
            radius: 30,
            backgroundColor: Colors.grey[300],
            child: Text('+3', style: TextStyle(color: Colors.black)),
          ),
        ),
      ],
    );
  }
}

二、空间分配:Expanded 与 Flexible

当在 Row 或 Column 中需要子Widget按比例分配空间时,Expanded 和 Flexible 就派上用场了。

2.1 Expanded - 充满剩余空间

Expanded 让子Widget填充主轴上的剩余可用空间

dart

Row(
  children: <Widget>[
    Container(
      width: 80,
      height: 60,
      color: Colors.red,
      child: Center(child: Text('固定宽度')),
    ),
    Expanded( // 这个Container会填充Row中剩余的所有宽度
      child: Container(
        height: 60,
        color: Colors.blue,
        child: Center(child: Text('自适应宽度')),
      ),
    ),
  ],
)

2.2 按比例分配空间

多个 Expanded 可以通过 flex 属性按比例分配空间:

dart

Row(
  children: <Widget>[
    Expanded(
      flex: 2, // 占2份
      child: Container(
        height: 60,
        color: Colors.red,
        child: Center(child: Text('2/6')),
      ),
    ),
    Expanded(
      flex: 3, // 占3份
      child: Container(
        height: 60,
        color: Colors.green,
        child: Center(child: Text('3/6')),
      ),
    ),
    Expanded(
      flex: 1, // 占1份
      child: Container(
        height: 60,
        color: Colors.blue,
        child: Center(child: Text('1/6')),
      ),
    ),
  ],
)

2.3 Flexible 与 Expanded 的区别

Expanded 实际上是 Flexible 的一个特例(flex: 1, fit: FlexFit.tight)。

  • Expanded:强制子Widget填充满分配的空间

  • Flexible:允许子Widget不填满分配的空间(默认行为)

dart

Row(
  children: <Widget>[
    Flexible(
      child: Container(
        height: 60,
        color: Colors.red,
        // 这个Container只会占用实际需要的宽度,而不是全部可用宽度
        child: Center(child: Text('Flexible - 按需宽度')),
      ),
    ),
    Expanded(
      child: Container(
        height: 60,
        color: Colors.blue,
        child: Center(child: Text('Expanded - 充满宽度')),
      ),
    ),
  ],
)

三、尺寸控制:SizedBox

SizedBox 是一个简单的盒子,它可以用来:

  1. 设置固定的尺寸

  2. 强制子Widget具有特定尺寸

  3. 在两个Widget之间添加间距

3.1 作为固定尺寸容器

dart

SizedBox(
  width: 100,
  height: 100,
  child: ElevatedButton(
    onPressed: () {},
    child: Text('按钮'),
  ),
)

3.2 作为间距控件

这是 SizedBox 最常见的用法之一:

dart

Column(
  children: <Widget>[
    Text('第一行文本'),
    SizedBox(height: 20), // 添加20像素的垂直间距
    Text('第二行文本'),
    SizedBox(height: 30), // 添加30像素的垂直间距
    Text('第三行文本'),
  ],
)

3.3 占位空间

使用 SizedBox.shrink() 创建一个零尺寸的占位Widget,或者用固定尺寸占位:

dart

Row(
  children: <Widget>[
    Text('开始'),
    SizedBox(width: 50), // 占位50像素宽度
    Text('结束'),
  ],
)

// 或者使用 Expanded + SizedBox 实现灵活的占位
Row(
  children: <Widget>[
    Text('左侧内容'),
    Expanded( // 这个SizedBox会填充所有剩余空间
      child: SizedBox.shrink(), // 零尺寸,但占用空间
    ),
    Text('右侧内容'),
  ],
)

四、综合实战:构建一个复杂的卡片布局

现在让我们综合运用今天学到的所有Widget,构建一个复杂的电商产品卡片:

dart

class ComplexProductCard extends StatelessWidget {
  const ComplexProductCard({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(16),
      width: 280,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.3),
            blurRadius: 10,
            offset: Offset(0, 4),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 图片区域 - 使用Stack实现叠加效果
          Stack(
            children: [
              // 产品图片
              Container(
                height: 180,
                width: double.infinity,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
                  image: DecorationImage(
                    image: NetworkImage('https://example.com/product.jpg'),
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              // 折扣标签
              Positioned(
                top: 12,
                left: 12,
                child: Container(
                  padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: Colors.red,
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Text(
                    '30% OFF',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 12,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
              // 收藏按钮
              Positioned(
                top: 12,
                right: 12,
                child: Container(
                  width: 36,
                  height: 36,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    shape: BoxShape.circle,
                    boxShadow: [
                      BoxShadow(
                        color: Colors.grey.withOpacity(0.3),
                        blurRadius: 4,
                      ),
                    ],
                  ),
                  child: Icon(Icons.favorite_border, size: 18),
                ),
              ),
            ],
          ),
          
          SizedBox(height: 12),
          
          // 内容区域
          Padding(
            padding: EdgeInsets.symmetric(horizontal: 16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '高端无线蓝牙耳机',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                
                SizedBox(height: 8),
                
                Text(
                  '沉浸式音效,超长续航,智能降噪',
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[600],
                  ),
                ),
                
                SizedBox(height: 12),
                
                // 价格和评分行 - 使用Expanded实现自动对齐
                Row(
                  children: [
                    Expanded(
                      child: Text(
                        '\$199.99',
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                          color: Colors.blue,
                        ),
                      ),
                    ),
                    Row(
                      children: [
                        Icon(Icons.star, color: Colors.orange, size: 16),
                        SizedBox(width: 4),
                        Text('4.8', style: TextStyle(fontSize: 14)),
                      ],
                    ),
                  ],
                ),
                
                SizedBox(height: 16),
                
                // 按钮行 - 使用Expanded实现等宽按钮
                Row(
                  children: [
                    Expanded(
                      flex: 2,
                      child: OutlinedButton(
                        onPressed: () {},
                        child: Text('加入购物车'),
                      ),
                    ),
                    SizedBox(width: 12),
                    Expanded(
                      flex: 3,
                      child: ElevatedButton(
                        onPressed: () {},
                        child: Text('立即购买'),
                      ),
                    ),
                  ],
                ),
                
                SizedBox(height: 16),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

布局技巧总结

  1. 使用 Stack 时:注意子Widget的叠加顺序,后面的Widget会覆盖前面的Widget。

  2. 使用 Expanded 时:确保父Widget(Row/Column)有明确的主轴尺寸约束。

  3. 使用 SizedBox 时:相比于使用空的Container添加间距,SizedBox 性能更好且语义更清晰。

  4. 组合使用Expanded + SizedBox 可以创建灵活的间距,Stack + Positioned 可以创建复杂的重叠效果。

结语

恭喜!通过本讲的学习,你已经掌握了Flutter中另外三个关键的布局Widget。现在你已具备了构建绝大多数常见UI界面所需的核心布局能力:

  • Stack 用于创建层叠布局

  • Expanded 和 Flexible 用于空间分配

  • SizedBox 用于精确尺寸控制和间距

在下一讲中,我们将进行一个完整的实战项目——构建一个登录页面,将前七讲学到的所有知识综合运用到一个实际场景中。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

移动端开发者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值