第4章:布局类组件 —— 4.3 线性布局(Row和Column)

130 阅读9分钟

4.3 线性布局(Row和Column)

📚 章节概览

线性布局是最常用的布局方式之一,本章节将学习:

  • 主轴和纵轴 - 理解线性布局的坐标系
  • Row - 水平线性布局
  • Column - 垂直线性布局
  • MainAxisAlignment - 主轴对齐方式
  • CrossAxisAlignment - 纵轴对齐方式
  • MainAxisSize - 主轴尺寸控制
  • TextDirection - 文本方向
  • VerticalDirection - 垂直方向
  • 特殊情况 - 嵌套布局规则

🎯 核心知识点

什么是线性布局

线性布局指沿水平垂直方向排列子组件的布局方式。

Row     →  水平方向排列(类似Android的LinearLayout horizontal)
Column  →  垂直方向排列(类似Android的LinearLayout vertical)

继承关系

Widget → Flex → Row/Column

RowColumn 都继承自 Flex(弹性布局),我们将在4.4节详细介绍 Flex


1️⃣ 主轴和纵轴

1.1 概念

线性布局有主轴(Main Axis)和纵轴(Cross Axis)之分:

graph LR
    A[Row<br/>水平布局] -->|主轴| B[水平方向 ←→]
    A -->|纵轴| C[垂直方向 ↑↓]
    
    D[Column<br/>垂直布局] -->|主轴| E[垂直方向 ↑↓]
    D -->|纵轴| F[水平方向 ←→]
    
    style A fill:#e1f5ff
    style D fill:#e1ffe1
布局类型主轴方向纵轴方向
Row水平 (←→)垂直 (↑↓)
Column垂直 (↑↓)水平 (←→)

1.2 对齐枚举类

Flutter提供两个枚举类来控制对齐:

  • MainAxisAlignment - 主轴对齐方式
  • CrossAxisAlignment - 纵轴对齐方式

2️⃣ Row(水平布局)

2.1 构造函数

Row({
  Key? key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,  // 主轴对齐
  MainAxisSize mainAxisSize = MainAxisSize.max,                   // 主轴尺寸
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, // 纵轴对齐
  TextDirection? textDirection,                                   // 文本方向
  VerticalDirection verticalDirection = VerticalDirection.down,   // 垂直方向
  TextBaseline? textBaseline,                                     // 基线
  List<Widget> children = const <Widget>[],                       // 子组件列表
})

2.2 主要属性

属性类型默认值说明
mainAxisAlignmentMainAxisAlignmentstart主轴对齐方式
mainAxisSizeMainAxisSizemax主轴占用空间
crossAxisAlignmentCrossAxisAlignmentcenter纵轴对齐方式
textDirectionTextDirection?null文本方向(ltr/rtl)
verticalDirectionVerticalDirectiondown垂直方向(down/up)
textBaselineTextBaseline?null基线对齐类型
childrenList<Widget>[]子组件列表

2.3 基础用法

Row(
  children: [
    Container(
      width: 60,
      height: 60,
      color: Colors.red,
      child: Center(child: Text('Box 1')),
    ),
    Container(
      width: 60,
      height: 60,
      color: Colors.green,
      child: Center(child: Text('Box 2')),
    ),
    Container(
      width: 60,
      height: 60,
      color: Colors.blue,
      child: Center(child: Text('Box 3')),
    ),
  ],
)

效果: 三个盒子从左到右水平排列


3️⃣ MainAxisAlignment(主轴对齐)

控制子组件在主轴方向的对齐方式。

3.1 所有对齐方式

graph TB
    A[MainAxisAlignment] --> B[start<br/>起始对齐]
    A --> C[end<br/>末尾对齐]
    A --> D[center<br/>居中对齐]
    A --> E[spaceBetween<br/>两端对齐]
    A --> F[spaceAround<br/>间距环绕]
    A --> G[spaceEvenly<br/>间距均分]
    
    style A fill:#e1f5ff

3.2 详细说明

① MainAxisAlignment.start(默认)

从主轴起始位置开始排列。

Row(
  mainAxisAlignment: MainAxisAlignment.start,
  children: [Box1, Box2, Box3],
)

效果:

[Box1][Box2][Box3]___________________
② MainAxisAlignment.end

从主轴末尾位置开始排列。

Row(
  mainAxisAlignment: MainAxisAlignment.end,
  children: [Box1, Box2, Box3],
)

效果:

___________________[Box1][Box2][Box3]
③ MainAxisAlignment.center

在主轴居中对齐。

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [Box1, Box2, Box3],
)

效果:

_________[Box1][Box2][Box3]__________
④ MainAxisAlignment.spaceBetween

两端对齐,子组件之间均分间距。

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [Box1, Box2, Box3],
)

效果:

[Box1]____________[Box2]____________[Box3]
  • 第一个组件靠左
  • 最后一个组件靠右
  • 中间组件均分间距
⑤ MainAxisAlignment.spaceAround

间距环绕,每个子组件两侧都有间距。

Row(
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: [Box1, Box2, Box3],
)

效果:

___[Box1]______[Box2]______[Box3]___
  • 每个组件两侧间距相等
  • 边缘间距 = 组件间间距 / 2
⑥ MainAxisAlignment.spaceEvenly

间距均分,所有间距都相等。

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [Box1, Box2, Box3],
)

效果:

____[Box1]____[Box2]____[Box3]____
  • 所有间距完全相等
  • 包括边缘间距

3.3 对比表

对齐方式边缘间距组件间间距特点
start00靠起始对齐
end00靠末尾对齐
center相等0居中对齐
spaceBetween0均分两端对齐
spaceAround间距/2间距间距环绕
spaceEvenly间距间距完全均分

4️⃣ CrossAxisAlignment(纵轴对齐)

控制子组件在纵轴方向的对齐方式。

4.1 所有对齐方式

枚举值说明效果
start起始对齐Row中为顶部对齐
end末尾对齐Row中为底部对齐
center居中对齐(默认)垂直居中
stretch拉伸填充子组件填充整个纵轴
baseline基线对齐文本基线对齐

4.2 示例说明

① CrossAxisAlignment.start
Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Container(height: 30, width: 50, color: Colors.red),
    Container(height: 50, width: 50, color: Colors.green),
    Container(height: 40, width: 50, color: Colors.blue),
  ],
)

效果: 所有盒子顶部对齐

② CrossAxisAlignment.end
Row(
  crossAxisAlignment: CrossAxisAlignment.end,
  children: [
    Container(height: 30, width: 50, color: Colors.red),
    Container(height: 50, width: 50, color: Colors.green),
    Container(height: 40, width: 50, color: Colors.blue),
  ],
)

效果: 所有盒子底部对齐

③ CrossAxisAlignment.center(默认)
Row(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Container(height: 30, width: 50, color: Colors.red),
    Container(height: 50, width: 50, color: Colors.green),
    Container(height: 40, width: 50, color: Colors.blue),
  ],
)

效果: 所有盒子垂直居中

④ CrossAxisAlignment.stretch
Row(
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: [
    Expanded(child: Container(color: Colors.red)),
    Expanded(child: Container(color: Colors.green)),
    Expanded(child: Container(color: Colors.blue)),
  ],
)

效果: 所有盒子拉伸到Row的高度

注意: 使用 stretch 时,子组件不应设置固定高度

⑤ CrossAxisAlignment.baseline

基线对齐,需要配合 textBaseline 使用。

Row(
  crossAxisAlignment: CrossAxisAlignment.baseline,
  textBaseline: TextBaseline.alphabetic,
  children: [
    Text('Hello', style: TextStyle(fontSize: 30)),
    Text('World', style: TextStyle(fontSize: 20)),
    Text('!', style: TextStyle(fontSize: 40)),
  ],
)

效果: 所有文本按基线对齐


5️⃣ MainAxisSize(主轴尺寸)

控制 Row/Column 在主轴方向占用的空间大小。

5.1 枚举值

枚举值说明效果
MainAxisSize.max最大化(默认)占满主轴所有空间
MainAxisSize.min最小化只占用子组件实际大小

5.2 示例对比

max(默认)- 占满空间
Container(
  color: Colors.yellow[100],  // 黄色背景
  child: Row(
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Container(width: 50, height: 50, color: Colors.red),
      SizedBox(width: 10),
      Container(width: 50, height: 50, color: Colors.blue),
    ],
  ),
)

效果: Row占满整个宽度,黄色背景填满

min - 最小空间
Container(
  color: Colors.yellow[100],  // 黄色背景
  child: Row(
    mainAxisSize: MainAxisSize.min,
    mainAxisAlignment: MainAxisAlignment.center,  // ⚠️ 无效!
    children: [
      Container(width: 50, height: 50, color: Colors.red),
      SizedBox(width: 10),
      Container(width: 50, height: 50, color: Colors.blue),
    ],
  ),
)

效果: Row只占用110宽度,黄色背景只在Row范围内

⚠️ 注意:mainAxisSize = min 时,mainAxisAlignment 无效,因为没有多余空间可以分配。


6️⃣ TextDirection(文本方向)

控制子组件的排列顺序。

6.1 枚举值

枚举值说明效果
TextDirection.ltrLeft to Right(默认)从左到右
TextDirection.rtlRight to Left从右到左

6.2 示例

ltr(默认)
Row(
  textDirection: TextDirection.ltr,
  mainAxisAlignment: MainAxisAlignment.end,
  children: [
    Text('1'), Text('2'), Text('3'),
  ],
)

效果:

_____________[1][2][3]
            ↑ 右对齐
rtl
Row(
  textDirection: TextDirection.rtl,  // 从右到左
  mainAxisAlignment: MainAxisAlignment.end,  // ⚠️ 此时表示左对齐!
  children: [
    Text('1'), Text('2'), Text('3'),
  ],
)

效果:

[3][2][1]_____________
↑ 此时end表示左对齐

⚠️ 重要:textDirection = rtl 时:

  • 子组件顺序反转:[1][2][3][3][2][1]
  • 对齐方向也反转:start 变成右对齐,end 变成左对齐

7️⃣ Column(垂直布局)

7.1 构造函数

Column 的参数与 Row 完全相同,只是布局方向不同。

Column({
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  TextDirection? textDirection,
  VerticalDirection verticalDirection = VerticalDirection.down,
  TextBaseline? textBaseline,
  List<Widget> children = const <Widget>[],
})

7.2 主轴和纵轴(相对于Row反转)

方向RowColumn
主轴水平 (←→)垂直 (↑↓)
纵轴垂直 (↑↓)水平 (←→)

7.3 基础用法

Column(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Text('hi'),
    Text('world'),
  ],
)

7.4 Column的实际宽度

重要规则: Column的宽度取决于最宽的子组件

Column(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Text('短'),                        // 宽度: 20
    Text('这是一个很长的文本'),         // 宽度: 150 ← 最宽
  ],
)
// Column的实际宽度 = 150

7.5 让Column占满屏幕宽度

方法1:使用 ConstrainedBox
ConstrainedBox(
  constraints: BoxConstraints(minWidth: double.infinity),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
      Text('hi'),
      Text('world'),
    ],
  ),
)
方法2:使用 SizedBox
SizedBox(
  width: double.infinity,
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
      Text('hi'),
      Text('world'),
    ],
  ),
)
方法3:使用 Center
Center(
  child: Column(
    children: [
      Text('hi'),
      Text('world'),
    ],
  ),
)

7.6 VerticalDirection(垂直方向)

控制Column中子组件的排列顺序。

枚举值说明效果
VerticalDirection.down从上到下(默认)正常顺序
VerticalDirection.up从下到上反向顺序
示例
// down(默认)
Column(
  verticalDirection: VerticalDirection.down,
  children: [
    Text('1'),  // 顶部
    Text('2'),
    Text('3'),  // 底部
  ],
)

// up(反向)
Column(
  verticalDirection: VerticalDirection.up,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('1'),  // 底部!
    Text('2'),
    Text('3'),  // 顶部!
  ],
)

⚠️ 注意:verticalDirection = up 时:

  • 子组件从下往上排列
  • crossAxisAlignment.start 表示底部对齐

8️⃣ 特殊情况 - 嵌套布局

8.1 核心规则

⚠️ 只有最外层的 Row/Column 会占用尽可能大的空间
   内层的 Row/Column 只占用实际大小

8.2 示例:嵌套Column

问题代码
Container(
  color: Colors.green,  // 外层背景
  child: Column(
    mainAxisSize: MainAxisSize.max,  // ✅ 有效:占满整个屏幕高度
    children: [
      Container(
        color: Colors.red,  // 内层背景
        child: Column(
          mainAxisSize: MainAxisSize.max,  // ❌ 无效:只占实际高度
          children: [
            Text('hello world'),
            Text('I am Jack'),
          ],
        ),
      ),
    ],
  ),
)

效果:

  • 外层Column(绿色)占满整个屏幕高度
  • 内层Column(红色)只占两行文本的高度

8.3 解决方案:使用 Expanded

Container(
  color: Colors.green,
  child: Column(
    mainAxisSize: MainAxisSize.max,
    children: [
      Expanded(  // ✅ 使用Expanded
        child: Container(
          color: Colors.red,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,  // 现在有效了
            children: [
              Text('hello world'),
              Text('I am Jack'),
            ],
          ),
        ),
      ),
    ],
  ),
)

效果: 内层Column填充外层Column的剩余空间

8.4 可视化对比

graph TB
    A[外层Column<br/>mainAxisSize=max<br/>占满屏幕] --> B[内层Column<br/>mainAxisSize=max<br/>无效!只占实际大小]
    
    C[外层Column<br/>mainAxisSize=max<br/>占满屏幕] --> D[Expanded] --> E[内层Column<br/>填充剩余空间]
    
    style A fill:#90EE90
    style B fill:#FFB6C1
    style C fill:#90EE90
    style D fill:#FFD700
    style E fill:#87CEEB

🤔 常见问题(FAQ)

Q1: Row/Column的默认尺寸是多少?

A: 取决于 mainAxisSize

  • mainAxisSize = max(默认):主轴占满,纵轴取最大子组件
  • mainAxisSize = min主轴和纵轴都取实际大小
// Row: 宽度占满,高度取最高的子组件
Row(
  mainAxisSize: MainAxisSize.max,  // 默认
  children: [
    Container(width: 50, height: 30),
    Container(width: 50, height: 80),  // ← 最高
  ],
)
// Row的尺寸: 宽度=屏幕宽度, 高度=80

// Row: 宽度和高度都取实际大小
Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    Container(width: 50, height: 30),
    Container(width: 50, height: 80),
  ],
)
// Row的尺寸: 宽度=100, 高度=80

Q2: 如何在Row中平均分配空间?

A: 使用 ExpandedFlexible

// 方法1:Expanded(平均分配)
Row(
  children: [
    Expanded(child: Container(color: Colors.red)),      // 1/3
    Expanded(child: Container(color: Colors.green)),    // 1/3
    Expanded(child: Container(color: Colors.blue)),     // 1/3
  ],
)

// 方法2:Expanded with flex(按比例分配)
Row(
  children: [
    Expanded(flex: 1, child: Container(color: Colors.red)),    // 1/6
    Expanded(flex: 2, child: Container(color: Colors.green)),  // 2/6
    Expanded(flex: 3, child: Container(color: Colors.blue)),   // 3/6
  ],
)

Q3: Row/Column中的子组件溢出怎么办?

A: 有三种解决方案:

方案1:使用 Expanded/Flexible
Row(
  children: [
    Expanded(  // 自动调整宽度
      child: Text('很长很长的文本...', overflow: TextOverflow.ellipsis),
    ),
  ],
)
方案2:使用 SingleChildScrollView
SingleChildScrollView(
  scrollDirection: Axis.horizontal,  // 水平滚动
  child: Row(
    children: [
      Container(width: 500),  // 超出屏幕宽度
      Container(width: 500),
    ],
  ),
)
方案3:使用 Wrap(自动换行)
Wrap(
  children: [
    Container(width: 100),
    Container(width: 100),
    Container(width: 100),
    // 如果一行放不下,会自动换行
  ],
)

Q4: 如何实现两端对齐且最后一行左对齐?

A: 这是一个常见需求,使用 Wrap 更合适:

Wrap(
  spacing: 10,  // 水平间距
  runSpacing: 10,  // 垂直间距
  children: List.generate(10, (index) {
    return Container(
      width: 100,
      height: 100,
      color: Colors.blue,
      child: Center(child: Text('$index')),
    );
  }),
)

为什么不用Row?

  • Row不会自动换行
  • 超出宽度会溢出

Q5: Column中如何让某个子组件占满剩余空间?

A: 使用 Expanded

Column(
  children: [
    Container(height: 100, color: Colors.red),     // 固定高度
    Expanded(                                       // 填充剩余空间
      child: Container(color: Colors.blue),
    ),
    Container(height: 100, color: Colors.green),   // 固定高度
  ],
)

🎯 跟着做练习

练习1:实现一个卡片标题栏

目标: 创建一个卡片标题栏,左侧图标,中间标题,右侧按钮

步骤:

  1. 使用Row布局
  2. 左侧放置Icon
  3. 中间Expanded放置标题
  4. 右侧放置IconButton
💡 查看答案
class CardHeader extends StatelessWidget {
  const CardHeader({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.blue[50],
        border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
      ),
      child: Row(
        children: [
          // 左侧图标
          Icon(Icons.article, color: Colors.blue),
          const SizedBox(width: 12),
          
          // 中间标题(占满剩余空间)
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  '文章标题',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  '副标题或描述信息',
                  style: TextStyle(
                    fontSize: 12,
                    color: Colors.grey[600],
                  ),
                ),
              ],
            ),
          ),
          
          // 右侧按钮
          IconButton(
            icon: const Icon(Icons.more_vert),
            onPressed: () {
              print('更多操作');
            },
          ),
        ],
      ),
    );
  }
}

练习2:实现一个评分组件

目标: 创建一个星级评分显示,包含星星和评分文本

步骤:

  1. 使用Row布局
  2. 用循环生成5个星星Icon
  3. 右侧显示评分数字
💡 查看答案
class RatingWidget extends StatelessWidget {
  final double rating;  // 0.0 - 5.0
  final int reviewCount;

  const RatingWidget({
    super.key,
    required this.rating,
    required this.reviewCount,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        // 星星
        ...List.generate(5, (index) {
          if (index < rating.floor()) {
            // 整颗星
            return const Icon(Icons.star, color: Colors.amber, size: 20);
          } else if (index < rating) {
            // 半颗星
            return const Icon(Icons.star_half, color: Colors.amber, size: 20);
          } else {
            // 空星
            return Icon(Icons.star_border, color: Colors.grey[400], size: 20);
          }
        }),
        
        const SizedBox(width: 8),
        
        // 评分文本
        Text(
          '$rating',
          style: const TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        
        const SizedBox(width: 4),
        
        // 评价数量
        Text(
          '($reviewCount)',
          style: TextStyle(
            fontSize: 12,
            color: Colors.grey[600],
          ),
        ),
      ],
    );
  }
}

// 使用示例
RatingWidget(rating: 4.5, reviewCount: 128)

练习3:实现一个信息展示列表

目标: 创建一个用户信息展示列表,每行包含标签和值

步骤:

  1. 使用Column布局
  2. 每行用Row布局
  3. 标签固定宽度,值占剩余空间
💡 查看答案
class InfoList extends StatelessWidget {
  const InfoList({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildInfoRow('姓名', '张三'),
        _buildInfoRow('手机', '138-xxxx-xxxx'),
        _buildInfoRow('邮箱', 'zhangsan@example.com'),
        _buildInfoRow('地址', '北京市朝阳区某某街道某某小区xxx号'),
      ],
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 标签(固定宽度)
          SizedBox(
            width: 60,
            child: Text(
              '$label:',
              style: TextStyle(
                color: Colors.grey[600],
                fontSize: 14,
              ),
            ),
          ),
          
          const SizedBox(width: 12),
          
          // 值(占满剩余空间)
          Expanded(
            child: Text(
              value,
              style: const TextStyle(
                fontSize: 14,
                fontWeight: FontWeight.w500,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

📋 小结

核心概念

概念说明
主轴布局方向的轴(Row=水平,Column=垂直)
纵轴垂直于主轴的轴
MainAxisAlignment主轴对齐方式
CrossAxisAlignment纵轴对齐方式
MainAxisSize主轴占用空间(max/min)

Row vs Column

特性RowColumn
主轴方向水平 (←→)垂直 (↑↓)
纵轴方向垂直 (↑↓)水平 (←→)
默认尺寸宽度占满,高度取最大子组件高度占满,宽度取最大子组件

嵌套规则

✅ 外层 Row/Column:占满空间(mainAxisSize=max)
❌ 内层 Row/Column:只占实际大小
✅ 使用 Expanded:让内层也填充空间

记忆技巧

  1. 主轴 = 布局方向:Row主轴水平,Column主轴垂直
  2. max vs min:max占满,min最小
  3. start/end/center:最常用的对齐方式
  4. spaceBetween最实用:两端对齐,中间均分
  5. 嵌套用Expanded:让内层填充空间

🔗 相关资源