Flutter 学习笔记 (3):布局初探 —— Row、Column、Stack 与 Container

1、前言

在掌握Widget的基本概念和有状态和无状态组件的区别后,接下来要面对的是任何一个APP都绕不开的问题:如何把各种组件按照设计稿摆在正确的位置上?

Flutter 的布局方案和其他平台不太一样,它不依赖XML或Storyboard,而是完全使用Widget组合来实现布局。也就是说,布局也是一种Widget。

这一篇我们集中学习几个最基础也最常用的布局组件,最后亲手搭建一个类似Instagram帕子卡片的简单界面。

2、核心布局组件速览

Flutter 提供了几十种布局组件,但80%的日常布局只需要下面这四个:

组件 作用 适用场景
Row 水平方向排列子组件 导航栏图标 + 标题、按钮组
Column 垂直方向排列子组件 表单、列表项、卡片内容区
Stack 允许子组件重叠定位 头像上加徽章、图片上浮文字
Container 集定位、尺寸、装饰于一体 设置宽高、背景色、圆角、边距

3、Row和Column - 线性布局的双子星

RowColumn 用法几乎一样,只是方向不同。它们的核心属性:

mainAxisAlignment : 主轴方向的对齐方式(Row的主轴是水平,Column的主轴是垂直)

crossAxisAlignment : 交叉轴方向的对齐方式

示例:一个简单的水平按钮组

less 复制代码
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Greeting Widget')),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(
              onPressed: () {},
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.blue,
                foregroundColor: Colors.white,
                textStyle: TextStyle(color: Colors.white),
              ),
              child: Text('确认'),
            ),
            ElevatedButton(onPressed: () {}, child: Text('取消')),
          ],
        ),
      ),
    );
  }

避免常见错误 :当Row 或 Column 的内容超出屏幕范围时,会报溢出错误(黄色条纹)。

解决方案

  • Expanded 让子组件填充剩余空间。
  • SingleChildScrollView 包裹使其可滚动。

Expanded 使用示例:

less 复制代码
Container(
    alignment: Alignment.center,
    padding: EdgeInsets.all(20),
    child: Row(
      children: [
        Icon(Icons.ac_unit),
        Expanded(
          child: Text('这是一段可能很长的文字,Expanded 会把它限制在剩余宽度内,这是一段可能很长的'),
        ),
        Icon(Icons.arrow_forward),
      ],
    ),
),

4、Container - 最灵活的"盒子"

Container 可以理解为一个多功能的矩形区域,它能同时做到:

  • 设置宽度(width、height);
  • 设置内边距(padding)和外边距(margin);
  • 设置背景色(color)或背景图片(decoration)
  • 设置边框、圆角、阴影(通过decoration的BoxDecoration)
  • 限制子组件大小(alignment控制子组件在内部的位置)

示例:一个带圆角和阴影的卡片容器

less 复制代码
 @override
  Widget build(BuildContext context) {
    return Container(
      width: 200,
      height: 200,
      margin: EdgeInsets.all(10),
      padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(10),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.5),
            spreadRadius: 5,
            blurRadius: 7,
            offset: Offset(0, 3),
          ),
        ],
      ),
      child: Text(content, style: TextStyle(color: Colors.white, fontSize: 16)),
    );
  }

提示 :如果同时设置了 colordecoration,会报错。因为 decoration 已经包含了颜色信息,应该把颜色写在 decorationcolor 属性里。

5、Stack - 实现重叠效果

Stack 允许子组件按照 "后进先上"的原则层叠放置。通常配合 Positioned 组件精确定位。

示例:图片右下角加一个徽章

less 复制代码
 @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        ClipRRect(
          borderRadius: BorderRadius.circular(5),
          child: Image.network(
            'https://picsum.photos/200/300',
            scale: 2.0,
            width: 150,
            height: 150,
            fit: BoxFit.cover,
          ),
        ),
        Positioned(
          bottom: 0,
          right: 0,
          child: Container(
            padding: EdgeInsets.all(5),
            decoration: BoxDecoration(
              color: Colors.white.withOpacity(0.8),
              borderRadius: BorderRadius.circular(5),
            ),
            child: Text('这是一张图片'),
          ),
        ),
      ],
    );
  }

6. 实战:搭建一个"个人名片"卡片

我们把学到的组件组合起来,做一个简单的名片卡片:头像、名字、职业、两个按钮、一行标签图标。

代码(完整的 StatelessWidget)

less 复制代码
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 320,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(24),
          boxShadow: [
            BoxShadow(
              color: Colors.grey.shade200,
              blurRadius: 20,
              offset: Offset(0, 8),
            ),
          ],
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              margin: EdgeInsets.only(top: 24),
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                boxShadow: [
                  BoxShadow(
                    color: Colors.grey.shade100,
                    blurRadius: 12,
                    spreadRadius: 2,
                  ),
                ],
              ),
              child: CircleAvatar(
                backgroundImage: NetworkImage(
                  'https://via.placeholder.com/100',
                ),
                radius: 55,
              ),
            ),
            SizedBox(height: 16),
            Text(
              'Flutter 开发者',
              style: TextStyle(
                fontSize: 22,
                fontWeight: FontWeight.w600,
                color: Colors.grey.shade800,
              ),
            ),
            SizedBox(height: 6),
            Text(
              'Gitrocr',
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w500,
                color: Colors.grey.shade600,
              ),
            ),
            SizedBox(height: 16),
            Column(
              children: [
                _buildContactRow(Icons.email, 'xiaoming@example.com'),
                SizedBox(height: 12),
                _buildContactRow(Icons.phone, '+86 123 4567'),
                SizedBox(height: 12),
                _buildContactRow(Icons.location_on, '中国 北京'),
              ],
            ),
            SizedBox(height: 24),
            Padding(
              padding: EdgeInsets.symmetric(horizontal: 24),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  Expanded(
                    child: ElevatedButton.icon(
                      icon: Icon(Icons.message, size: 18),
                      onPressed: () {},
                      label: Text('私信', style: TextStyle(fontSize: 14)),
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.blueAccent,
                        foregroundColor: Colors.white,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(30),
                        ),
                        padding: EdgeInsets.symmetric(vertical: 12),
                        textStyle: TextStyle(color: Colors.white, fontSize: 12),
                      ),
                    ),
                  ),
                  SizedBox(width: 12),
                  Expanded(
                    child: ElevatedButton.icon(
                      icon: Icon(Icons.add, size: 18),
                      onPressed: () {},
                      label: Text('关注', style: TextStyle(fontSize: 14)),
                      style: OutlinedButton.styleFrom(
                        foregroundColor: Colors.blueAccent,
                        side: BorderSide(color: Colors.blueAccent),
                        padding: EdgeInsets.symmetric(vertical: 12),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(30),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
            SizedBox(height: 16),
          ],
        ),
      ),
    );
  }
}

// 辅助方法:构建统一的联系人行
Widget _buildContactRow(IconData icon, String text) {
  return Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Icon(icon, size: 18, color: Colors.blueGrey.shade400),
      SizedBox(width: 8),
      Text(text, style: TextStyle(fontSize: 14, color: Colors.grey.shade700)),
    ],
  );
}

将上面的 BusinessCard 放到 MyApphome 中运行,就能看到一个居中显示的精致名片卡片。你可以尝试修改头像图片、颜色,或者把按钮改成其他样式。

7. 布局调试技巧

当布局不是你想要的样子时,可以用这些方法快速定位问题:

  • 给 Container 加上明显的背景色 :例如 color: Colors.red,看它实际占用的大小
  • 使用 debugPaintSizeEnabled = true :在 main() 中加入下面代码,会显示出每个组件的边界
javascript 复制代码
import 'package:flutter/rendering.dart';

void main() {
  debugPaintSizeEnabled = true;  // 仅用于调试,发布前记得删除
  runApp(MyApp());
}
  • 热重载 :调整布局参数后按 r 键,秒看效果

8. 常见问题与避坑

问题现象 可能原因 解决办法
Row / Column 里内容显示不全,边缘有黄黑条纹 内容总宽度/高度超出父容器 ExpandedFlexible,或者用 ListView 替代
Container 设置了宽高但没效果 父容器限制了子组件的大小(例如 Scaffoldbody 默认不限制) 检查父组件或用 ConstrainedBox 包裹
想让 Container 中的文字居中但没居中 忘记设置 alignment 添加 alignment: Alignment.center
Stack 里的内容位置不对 没有用 PositionedAlign 给需要定位的子组件加上 Positioned

9. 小结与作业

本篇我们学习了:

  • Row / Column 实现线性布局
  • Container 实现装饰、尺寸、边距
  • Stack + Positioned 实现层叠
  • 组合以上组件完成了一张名片卡片

作业(巩固练习)

  1. 把名片卡片改成暗色主题(背景黑色、文字白色、按钮样式调整)。
  2. 在名片卡片底部加一行社交图标(使用 Row + IconButton)。
  3. 尝试用 Stack 在名片卡片的右上角加一个"认证徽章"(蓝色勾勾图标)。

完成了这些练习,你对 Flutter 布局的基本功就算打好了。

下一篇博客预告:我们将进入交互与数据层面 ------ 如何监听文本输入、管理表单,并介绍状态管理的经典方案 Provider。敬请期待!

相关推荐
风华圆舞2 小时前
DevEco Studio 和 Flutter 工具链如何协同工作
flutter·华为·架构·harmonyos
朱莉^_^JuneLee2 小时前
Flutter 性能优化实战:用 ConsumerWidget + select 做到真正的局部刷新
flutter
G_dou_6 小时前
Flutter三方库适配OpenHarmony【palindrome_checker】回文检测器项目完整实战
flutter·harmonyos
朱莉^_^JuneLee6 小时前
Flutter 模块化架构实战:用 Barrel Export 管控模块边界
flutter·架构
风华圆舞7 小时前
鸿蒙导航意图 的 Flutter 侧封装思路
flutter·华为·harmonyos
风华圆舞7 小时前
Flutter 调用原生失败时,如何优雅处理 `MissingPluginException`
flutter·华为·harmonyos
风华圆舞8 小时前
鸿蒙防窥保护 的 Flutter 侧封装思路
flutter·华为·harmonyos
风华圆舞8 小时前
SpeechRecognitionChannel 的 Flutter 侧封装思路
flutter·华为·harmonyos