第4章:布局类组件 —— 4.4 弹性布局(Flex)

79 阅读8分钟

4.4 弹性布局(Flex)

📚 章节概览

弹性布局允许子组件按照一定比例来分配父容器空间,本章节将学习:

  • Flex - 弹性布局容器
  • Expanded - 按比例扩展组件
  • Flexible - 灵活的弹性组件
  • Spacer - 空白占位组件
  • flex参数 - 弹性系数
  • FlexFit - tight vs loose
  • 实际应用 - 常见布局场景

🎯 核心知识点

什么是弹性布局

弹性布局(Flex Layout)允许子组件按照比例分配父容器的可用空间,而不是固定尺寸。

// 固定布局
Row(
  children: [
    Container(width: 100),  // 固定100
    Container(width: 200),  // 固定200
  ],
)

// 弹性布局
Row(
  children: [
    Expanded(flex: 1),  // 占1/3
    Expanded(flex: 2),  // 占2/3
  ],
)

继承关系

graph TB
    A[Flex] --> B[Row]
    A --> C[Column]
    
    style A fill:#e1f5ff
    style B fill:#e1ffe1
    style C fill:#ffe1e1

Row 和 Column 都继承自 Flex,所以弹性布局的特性在它们中都可以使用。


1️⃣ Flex(弹性容器)

1.1 构造函数

Flex({
  Key? key,
  required Axis direction,              // 主轴方向(必需)
  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>[],
})

1.2 关键属性

属性类型说明
directionAxis主轴方向(horizontal/vertical)必需
其他属性-与Row/Column相同

1.3 Flex vs Row/Column

// 这三种写法是等价的

// 使用 Flex(水平)
Flex(
  direction: Axis.horizontal,
  children: [...],
)

// 使用 Row
Row(
  children: [...],
)

// 使用 Flex(垂直)
Flex(
  direction: Axis.vertical,
  children: [...],
)

// 使用 Column
Column(
  children: [...],
)

何时使用Flex?

  • 需要动态切换方向时
  • 需要抽象化布局逻辑时
  • 通常情况下,直接用Row/Column更直观

2️⃣ Expanded(按比例扩展)

2.1 构造函数

const Expanded({
  Key? key,
  int flex = 1,              // 弹性系数,默认1
  required Widget child,     // 子组件
})

2.2 基本用法

示例1:平均分配空间
Row(
  children: [
    Expanded(child: Container(color: Colors.red)),     // 占50%
    Expanded(child: Container(color: Colors.green)),   // 占50%
  ],
)
示例2:固定+弹性混合
Row(
  children: [
    Container(width: 80, color: Colors.red),     // 固定80
    Expanded(child: Container(color: Colors.green)),  // 占剩余空间
    Container(width: 80, color: Colors.blue),    // 固定80
  ],
)

效果: 绿色占满中间所有剩余空间

2.3 Expanded的本质

// Expanded实际上是Flexible的特殊形式
Expanded(
  flex: 1,
  child: widget,
)

// 等价于
Flexible(
  flex: 1,
  fit: FlexFit.tight,  // 强制填满
  child: widget,
)

3️⃣ flex(弹性系数)

3.1 计算公式

每个组件的尺寸 = 剩余空间 × (当前flex / 总flex)

3.2 示例解析

示例1:1:2比例
Row(
  children: [
    Expanded(flex: 1, child: RedBox),    // 占1/3
    Expanded(flex: 2, child: GreenBox),  // 占2/3
  ],
)

计算过程:

假设Row宽度 = 300pxflex = 1 + 2 = 3

RedBox宽度 = 300 × (1/3) = 100px
GreenBox宽度 = 300 × (2/3) = 200px
示例2:1:2:3比例
Row(
  children: [
    Expanded(flex: 1, child: RedBox),    // 占1/6
    Expanded(flex: 2, child: GreenBox),  // 占2/6
    Expanded(flex: 3, child: BlueBox),   // 占3/6
  ],
)

计算过程:

假设Row宽度 = 600pxflex = 1 + 2 + 3 = 6

RedBox宽度 = 600 × (1/6) = 100px
GreenBox宽度 = 600 × (2/6) = 200px
BlueBox宽度 = 600 × (3/6) = 300px

3.3 混合固定宽度

Row(
  children: [
    Container(width: 100, color: Colors.red),     // 固定100
    Expanded(flex: 1, child: GreenBox),           // 弹性1份
    Expanded(flex: 2, child: BlueBox),            // 弹性2份
  ],
)

计算过程:

假设Row宽度 = 600px
固定宽度 = 100px
剩余空间 = 600 - 100 = 500px
总flex = 1 + 2 = 3

GreenBox宽度 = 500 × (1/3) = 166.7px
BlueBox宽度 = 500 × (2/3) = 333.3px

3.4 flex=0的特殊情况

Expanded(
  flex: 0,  // ⚠️ 不推荐使用
  child: Container(width: 100),
)

效果: flex=0 表示不参与弹性分配,但不推荐这样用,直接用固定宽度即可。


4️⃣ Spacer(空白占位)

4.1 什么是Spacer

Spacer 用于创建可调整的空白空间。

4.2 源码分析

class Spacer extends StatelessWidget {
  const Spacer({Key? key, this.flex = 1});
  
  final int flex;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: const SizedBox.shrink(),  // 空Widget
    );
  }
}

本质: Spacer = Expanded + SizedBox.shrink()

4.3 使用场景

场景1:两端对齐
Row(
  children: [
    Text('左侧'),
    Spacer(),      // 自动填充中间空白
    Text('右侧'),
  ],
)

效果:

左侧__________________________右侧
场景2:垂直间距分配
Column(
  children: [
    Text('顶部'),
    Spacer(flex: 1),   // 占1份空白
    Text('中间'),
    Spacer(flex: 2),   // 占2份空白(是上方的2倍)
    Text('底部'),
  ],
)
场景3:等间距布局
Row(
  children: [
    Spacer(),
    Icon(Icons.home),
    Spacer(),
    Icon(Icons.search),
    Spacer(),
    Icon(Icons.person),
    Spacer(),
  ],
)

效果: 三个图标均匀分布,两侧和中间间距相等

4.4 Spacer vs SizedBox

特性SpacerSizedBox
尺寸弹性,根据flex分配固定尺寸
用途动态间距固定间距
示例Spacer()SizedBox(width: 20)

5️⃣ Flexible(灵活布局)

5.1 构造函数

const Flexible({
  Key? key,
  int flex = 1,
  FlexFit fit = FlexFit.loose,  // loose或tight
  required Widget child,
})

5.2 FlexFit类型

FlexFit说明行为
tight紧约束子组件必须填满分配的空间
loose松约束子组件可以小于分配的空间

5.3 Expanded vs Flexible

// Expanded(强制填满)
Expanded(
  flex: 1,
  child: Container(width: 50),  // ← 宽度被忽略,强制填满
)

// 等价于
Flexible(
  flex: 1,
  fit: FlexFit.tight,  // tight:强制填满
  child: Container(width: 50),
)

// Flexible(可以不填满)
Flexible(
  flex: 1,
  fit: FlexFit.loose,  // loose:可以更小
  child: Container(width: 50),  // ← 实际宽度50
)

5.4 示例对比

tight(强制填满)
Row(
  children: [
    Flexible(
      flex: 1,
      fit: FlexFit.tight,
      child: Container(
        width: 50,  // ← 被忽略
        color: Colors.red,
      ),
    ),
    Flexible(
      flex: 2,
      fit: FlexFit.tight,
      child: Container(
        width: 80,  // ← 被忽略
        color: Colors.green,
      ),
    ),
  ],
)

效果: 红色占1/3,绿色占2/3,忽略width设置

loose(可以更小)
Row(
  children: [
    Flexible(
      flex: 1,
      fit: FlexFit.loose,
      child: Container(
        width: 50,  // ← 生效
        color: Colors.red,
      ),
    ),
    Flexible(
      flex: 2,
      fit: FlexFit.loose,
      child: Container(
        width: 80,  // ← 生效
        color: Colors.green,
      ),
    ),
  ],
)

效果: 红色宽度50,绿色宽度80,右侧有剩余空间

5.5 何时使用Flexible

场景使用组件
需要填满空间Expanded(或Flexible + tight)
允许小于分配空间Flexible + loose
大多数情况Expanded(更常用)

🤔 常见问题(FAQ)

Q1: Expanded和Flexible的区别?

A:

Expanded = Flexible(fit: FlexFit.tight)
组件fit行为
Expandedtight强制填满分配空间
Flexible(默认)loose可以小于分配空间

建议: 90%的情况用 Expanded 即可

Q2: 为什么Expanded必须在Flex/Row/Column中使用?

A: Expanded 依赖父组件提供剩余空间信息

// ❌ 错误:Expanded不在Flex中
Container(
  child: Expanded(child: Text('错误')),
)
// Error: Incorrect use of ParentDataWidget

// ✅ 正确
Row(
  children: [
    Expanded(child: Text('正确')),
  ],
)

Q3: 如何实现三等分布局?

A: 三种方法:

方法1:Expanded(推荐)
Row(
  children: [
    Expanded(child: Container(color: Colors.red)),
    Expanded(child: Container(color: Colors.green)),
    Expanded(child: Container(color: Colors.blue)),
  ],
)
方法2:Flexible
Row(
  children: [
    Flexible(flex: 1, fit: FlexFit.tight, child: RedBox),
    Flexible(flex: 1, fit: FlexFit.tight, child: GreenBox),
    Flexible(flex: 1, fit: FlexFit.tight, child: BlueBox),
  ],
)
方法3:FractionallySizedBox
Row(
  children: [
    FractionallySizedBox(widthFactor: 1/3, child: RedBox),
    FractionallySizedBox(widthFactor: 1/3, child: GreenBox),
    FractionallySizedBox(widthFactor: 1/3, child: BlueBox),
  ],
)

Q4: 多个Expanded如何设置不同比例?

A: 使用 flex 参数:

Row(
  children: [
    Expanded(flex: 2, child: Text('占2份')),  // 2/5 = 40%
    Expanded(flex: 3, child: Text('占3份')),  // 3/5 = 60%
  ],
)

记忆技巧: flex值越大,占比越大

Q5: Spacer和SizedBox的区别?

A:

特性SpacerSizedBox
尺寸弹性(根据剩余空间)固定
适用场景两端对齐、动态间距固定间距
性能轻量轻量
// Spacer:动态间距
Row(
  children: [
    Text('左'),
    Spacer(),  // 自动填充
    Text('右'),
  ],
)

// SizedBox:固定间距
Row(
  children: [
    Text('A'),
    SizedBox(width: 20),  // 固定20
    Text('B'),
  ],
)

🎯 跟着做练习

练习1:实现一个底部导航栏

目标: 创建4个图标均分的底部导航栏

步骤:

  1. 使用Row布局
  2. 4个Expanded平均分配
  3. 每个包含图标和文字
💡 查看答案
class BottomNavBar extends StatefulWidget {
  const BottomNavBar({super.key});

  @override
  State<BottomNavBar> createState() => _BottomNavBarState();
}

class _BottomNavBarState extends State<BottomNavBar> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 60,
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: Row(
        children: [
          _buildNavItem(Icons.home, '首页', 0),
          _buildNavItem(Icons.explore, '发现', 1),
          _buildNavItem(Icons.notifications, '消息', 2),
          _buildNavItem(Icons.person, '我的', 3),
        ],
      ),
    );
  }

  Widget _buildNavItem(IconData icon, String label, int index) {
    final isActive = _currentIndex == index;
    return Expanded(
      child: InkWell(
        onTap: () {
          setState(() {
            _currentIndex = index;
          });
        },
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              icon,
              color: isActive ? Colors.blue : Colors.grey,
              size: 24,
            ),
            const SizedBox(height: 4),
            Text(
              label,
              style: TextStyle(
                fontSize: 12,
                color: isActive ? Colors.blue : Colors.grey,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

练习2:实现一个进度条组件

目标: 创建可自定义进度的进度条

步骤:

  1. 使用Row布局
  2. 已完成部分用Expanded
  3. 未完成部分用Expanded
💡 查看答案
class CustomProgressBar extends StatelessWidget {
  final double progress;  // 0.0 - 1.0
  final Color activeColor;
  final Color backgroundColor;

  const CustomProgressBar({
    super.key,
    required this.progress,
    this.activeColor = Colors.blue,
    this.backgroundColor = Colors.grey,
  });

  @override
  Widget build(BuildContext context) {
    final progressPercent = (progress * 100).toInt();
    final activePercent = progressPercent;
    final inactivePercent = 100 - progressPercent;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Text(
              '$progressPercent%',
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 16,
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: ClipRRect(
                borderRadius: BorderRadius.circular(4),
                child: Container(
                  height: 8,
                  child: Row(
                    children: [
                      // 已完成部分
                      if (activePercent > 0)
                        Expanded(
                          flex: activePercent,
                          child: Container(color: activeColor),
                        ),
                      // 未完成部分
                      if (inactivePercent > 0)
                        Expanded(
                          flex: inactivePercent,
                          child: Container(
                            color: backgroundColor.withOpacity(0.3),
                          ),
                        ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ],
    );
  }
}

// 使用示例
Column(
  children: [
    CustomProgressBar(progress: 0.3, activeColor: Colors.red),
    SizedBox(height: 16),
    CustomProgressBar(progress: 0.65, activeColor: Colors.green),
    SizedBox(height: 16),
    CustomProgressBar(progress: 0.9, activeColor: Colors.blue),
  ],
)

练习3:实现一个聊天气泡布局

目标: 左右对齐的聊天气泡

步骤:

  1. 使用Row布局
  2. 自己的消息右对齐
  3. 对方的消息左对齐
  4. 使用Spacer控制对齐
💡 查看答案
class ChatBubble extends StatelessWidget {
  final String message;
  final bool isMine;

  const ChatBubble({
    super.key,
    required this.message,
    required this.isMine,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
      child: Row(
        children: [
          // 自己的消息:左侧用Spacer占位
          if (isMine) const Spacer(flex: 2),
          
          // 消息气泡
          Flexible(
            flex: 5,
            child: Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 16,
                vertical: 10,
              ),
              decoration: BoxDecoration(
                color: isMine ? Colors.blue : Colors.grey[300],
                borderRadius: BorderRadius.circular(16),
              ),
              child: Text(
                message,
                style: TextStyle(
                  color: isMine ? Colors.white : Colors.black87,
                  fontSize: 14,
                ),
              ),
            ),
          ),
          
          // 对方的消息:右侧用Spacer占位
          if (!isMine) const Spacer(flex: 2),
        ],
      ),
    );
  }
}

// 使用示例
Column(
  children: [
    ChatBubble(message: '你好!', isMine: false),
    ChatBubble(message: '你好,很高兴认识你', isMine: true),
    ChatBubble(message: '最近怎么样?', isMine: false),
    ChatBubble(message: '挺好的,谢谢关心!', isMine: true),
  ],
)

📋 小结

核心概念

组件说明常用场景
Flex弹性容器需要动态方向时
Expanded按比例扩展填充剩余空间(最常用)
Flexible灵活布局需要loose约束时
Spacer空白占位两端对齐、间距分配

继承关系

Flex ← Row
Flex ← Column
Expanded = Flexible(fit: FlexFit.tight)
Spacer = Expanded + SizedBox.shrink()

弹性系数计算

组件尺寸 = 剩余空间 × (flex / 总flex)

FlexFit对比

FlexFit行为使用场景
tight强制填满Expanded(默认)
loose可以更小Flexible(默认)

记忆技巧

  1. Expanded最常用:填充剩余空间首选
  2. flex表示份数:flex=2表示占2份
  3. Spacer做间距:动态间距用Spacer
  4. Row/Column继承Flex:弹性布局特性都能用

🔗 相关资源