Flutter那些事-布局篇

0 阅读10分钟

Flex

Flutter 的布局核心大多围绕弹性布局(Flex)  模型构建,它非常强大且灵活,类似于前端的 Flexbox。理解 Flex 布局,是掌握 Flutter 界面构建的基石。

简单来说,Flutter 的 Flex 布局允许你在一维方向上(水平或垂直)排列子组件,并灵活分配它们之间的空间。

一、Flex 布局的核心思想

在 Flex 布局中,有两个核心轴的概念你需要先了解,这对理解后续所有属性至关重要-2-8

  • 主轴 (Main Axis) :Flex 容器排列子组件的方向。如果容器是水平的(Row),主轴就是水平方向;如果是垂直的(Column),主轴就是垂直方向。
  • 交叉轴 (Cross Axis) :与主轴垂直的另一条轴。

二、基础容器:FlexRow 与 Column

在 Flutter 中,你主要通过三个组件来使用 Flex 布局:

  1. Flex 组件:这是最基础的弹性布局组件。它允许你通过 direction 参数明确指定主轴方向(Axis.horizontal 或 Axis.vertical)。
  2. Row 组件:它是 Flex 的一个专用版本,固定了主轴方向为水平 (Axis.horizontal),用于横向排列子组件。
  3. Column 组件:它也是 Flex 的专用版本,固定了主轴方向为垂直 (Axis.vertical),用于纵向排列子组件。

在实际开发中,因为方向是确定的,我们几乎总是直接使用更简洁的 Row 和 Column

三、关键属性详解(以 Row 和 Column 为例)

这些组件的属性非常丰富,可以精确控制子组件的布局行为。下表整理了最核心的几个属性:

属性类型作用范围说明与常用值
mainAxisAlignmentMainAxisAlignment主轴决定了子组件在主轴上的排列方式 -1-10。 - start:从主轴起点开始排列(默认) - end:向主轴终点对齐 - center:居中对齐 - spaceBetween:首尾靠边,中间等距 - spaceAround:每个子项两侧间距相等,但首尾间距是中间的一半 - spaceEvenly:所有间距(包括首尾)完全相等
mainAxisSizeMainAxisSize主轴决定了 Row 或 Column 自身在主轴方向占用多大空间 -2-7。 - max:占用父容器允许的最大空间(默认) - min:仅占用包裹其子组件所需的最小空间
crossAxisAlignmentCrossAxisAlignment交叉轴决定了子组件在交叉轴上的对齐方式 -1-6。 - start:与交叉轴起点对齐(如 Row 则顶部对齐) - end:与交叉轴终点对齐(如 Row 则底部对齐) - center:居中对齐(默认) - stretch:拉伸填满交叉轴方向的空间 - baseline:使子项的文字基线对齐(需配合 textBaseline 使用)
textDirectionTextDirection水平方向决定主轴水平方向的起始点,影响 start 和 end 的解释 -2-7。 - ltr:从左到右(默认,start 代表左) - rtl:从右到左(start 代表右)
verticalDirectionVerticalDirection垂直方向决定垂直方向的起始点,影响 start 和 end 的解释 -2-6。 - down:从上到下(默认,start 代表顶部) - up:从下到上(start 代表底部)

四、弹性伸缩:ExpandedFlexible 与 Spacer

除了对齐,Flex 布局最强大的地方在于按比例分配空间。这需要借助另外三个组件。

1. Expanded:填充分配的空间

Expanded 组件必须作为 RowColumn 或 Flex 的直接子级使用。它会强制其包裹的子组件,填满主轴上的剩余空间-3-4

  • flex 参数:一个整数,表示弹性系数。如果有多个 Expanded,它们将按照 flex 值的比例来分割主轴剩余空间-6
void main(List<String> args) {
  runApp(MainPage()); 
}

class MainPage extends StatelessWidget {
  const MainPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("FLex布局"),
        ),
        body:Container(
          width: double.infinity,
          height: double.infinity,
          decoration: BoxDecoration(color: Colors.amber),
          child: Flex(
            direction: Axis.vertical,
            children: [
              Container(
                height: 100,
                color: Colors.blue,
              ),
              Expanded(
                flex: 1, 
                child:Container(
                  color: Colors.blueGrey,
                )
              )
              ,
              Container(
                height: 100,
                color: Colors.red,
              )
            ],
          ),
        )
      ),
    );
  }
}

2. Flexible:与 Expanded 的细微差别

Flexible 也是用于控制子组件如何占据剩余空间,但比 Expanded 更灵活-6

  • fit 参数

    • FlexFit.tight:此模式下的 Flexible 和 Expanded 行为完全一致,强制子组件填充分配到的空间。
    • FlexFit.loose(默认):允许子组件最大不超过分配到的空间,但子组件可以保持其原有大小(如果原有大小小于分配空间,则不会强制拉伸填满)。

3. Spacer:占据空间的空白

Spacer 是一个实用组件,本质上是 Expanded 的一个包装,用于在子组件之间创建可调节的空白区域

  • 它会占据所有可用的剩余空间,从而将两边的组件“推开”。也可以通过 flex 参数让多个 Spacer 按比例分配空白区域。
Row(
  children: [
    Text('左'),
    Spacer(), // 将左右两个文本推开
    Text('右'),
  ],
)

五、布局流程与常见问题

了解布局的底层逻辑有助于排查问题。Row 和 Column 的布局算法大致分为六步:

  1. 布局非弹性子项:先布局那些没有被 Expanded 或 Flexible 包裹的子项。
  2. 分配剩余空间:计算主轴剩余空间,并按 flex 比例分配给弹性子项。
  3. 布局弹性子项:根据分配到的空间大小,布局这些弹性子项。
  4. 确定容器大小:交叉轴大小取子项的最大值;主轴大小由 mainAxisSize 决定。
  5. 确定子项位置:根据 mainAxisAlignment 和 crossAxisAlignment 放置所有子项。

常见问题

  • “溢出了” (Overflow) :当子项内容总宽度(或高度)超出父容器,且没有使用 Expanded 进行弹性约束时,你会看到黄黑相间的警告条纹。解决方案是使用 Expanded 包裹可伸缩的子项,或者考虑使用 ListView 来支持滚动。
  • Expanded 嵌套失效Expanded 只能直接放在 RowColumn 或 Flex 下面。如果套了多层 Container 之类的组件,它将无法正常工作。

Wrap

当子组件内容时根据数据动态生成时,使用wrap可以确保布局始终适配

在 Flutter 中,Wrap 是一个非常有用的布局小部件。它主要解决了一行(或一列)显示不下所有子widget时,如何处理溢出部分的难题。与 Row 或 Column 不同,Wrap 不会报溢出错误,而是会自动将放不下的子widget“包裹”到新的一行(或一列)。

如果把 Row 想象成一条固定长度的单行文本,那么 Wrap 就像是一个自适应的文本框:当文字超出当前行的宽度时,它会自动换到下一行继续排列。

核心用途

Wrap 非常适合用于创建流式布局,最常见的应用场景包括:

  • 标签列表(如文章标签、商品关键词)。
  • 搜索历史记录展示。
  • 筛选条件选项组。
  • 任何数量不确定、需要自动换行的子元素集合。

构造函数与属性详解

Wrap 的构造函数提供了丰富的属性来精确控制布局行为:

Wrap({
  Key? key,
  this.direction = Axis.horizontal, // 主轴方向
  this.alignment = WrapAlignment.start, // 主轴对齐方式
  this.spacing = 0.0, // 主轴方向上子widget的间距
  this.runAlignment = WrapAlignment.start, // 交叉轴上每一“行/列”的对齐方式
  this.runSpacing = 0.0, // 交叉轴上“行/列”之间的间距
  this.crossAxisAlignment = WrapCrossAlignment.start, // 交叉轴方向上子widget的对齐
  this.textDirection, // 文本方向(影响主轴起止点)
  this.verticalDirection = VerticalDirection.down, // 垂直方向(影响交叉轴起止点)
  this.clipBehavior = Clip.none, // 是否裁剪超出部分
  List<Widget> children = const <Widget>[], // 子widget列表
})

关键属性解析

为了让你更清晰地了解每个属性的作用,可以对照下表:

属性类型功能描述类比理解常用值举例
directionAxis主轴方向,决定了子widget是水平排列还是垂直排列。想象成河流的流向。Axis.horizontal(默认) Axis.vertical
spacingdouble主轴方向上,相邻两个子widget之间的空隙。单词与单词之间的空格。8.0, 10.0
runSpacingdouble交叉轴方向上,每一“行”(或“列”)之间的空隙段落与段落之间的行距。4.0, 15.0
alignmentWrapAlignment主轴方向上,当前“行”(或“列”)内子widget的排列方式。Word文档中的“左对齐”、“居中”、“右对齐”、“两端对齐”。start, center, end, spaceBetween
runAlignmentWrapAlignment交叉轴方向上,所有“行”(或“列”)作为一个整体,在父容器中的排列方式。多行文本在页面中的“顶端对齐”、“垂直居中”、“底端对齐”。start, center, end, spaceAround
crossAxisAlignmentWrapCrossAlignment交叉轴方向上,当前“行”(或“列”)内子widget彼此之间的对齐方式。一行中,有的孩子个子高,有的孩子个子矮,他们是在顶部对齐、底部对齐还是中间对齐。start, center, end

代码示例

下面的例子展示了如何用 Wrap 创建一组标签,并控制它们的间距和对齐方式:

import 'package:flutter/material.dart';

void main(List<String> args) {
  runApp(MainPage()); 
}

class MainPage extends StatelessWidget {
  const MainPage({Key? key}) : super(key: key);
  List<Widget> getList(){
    return List.generate(8, (index){
      return Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      );
    });
    // return [];
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("Wrap布局"),
        ),
        body:Container(
          width: double.infinity,
          height: double.infinity,
          decoration: BoxDecoration(color: Colors.amber),
          child: Wrap(
            direction: Axis.horizontal,
            // 主轴间距
            spacing: 10,
            // 交叉轴间距
            runSpacing: 10,
            alignment: WrapAlignment.spaceAround,
            runAlignment: WrapAlignment.start,
            children: getList(),
          ),
          
        )
      ),
    );
  }
}

总结

  • Wrap 的核心价值:优雅地解决布局溢出问题,实现流式布局效果-6
  • 与 Row / Column 的区别Row 和 Column 只能占据一行/一列,空间不足时会报错;而 Wrap 会自动创建新的行/列来容纳子元素。
  • 核心概念:理解 主轴 (direction)  和 交叉轴 的方向,以及 spacing(控制元素间距)与 runSpacing(控制行/列间距)的区别,是掌握 Wrap 的关键。

Stack/Positioned

Stack 和 Positioned 是一对黄金搭档,专门用来在 Flutter 中创建层叠布局。简单来说,Stack 就像一个容器,允许你把多个组件堆叠在一起;而 Positioned 则像是一个定位工具,可以精确控制被堆叠组件的位置

核心概念:画布与图层

为了更好地理解,你可以把 Stack 想象成一块画布,而它的每一个子组件(children)就是画布上的独立图层 

  • 图层顺序:代码中先添加的组件在最底层,后添加的依次覆盖在上面。

  • 两类图层Stack 的子组件分为两种 :

    1. 非定位组件:没有用 Positioned 包裹的组件。它们决定了 Stack 自身的大小。
    2. 定位组件:被 Positioned 包裹的组件。它们根据设置的偏移量,相对于 Stack 的边界进行精确定位。

📦 Stack 组件详解

Stack 负责搭建舞台,它本身有一些关键属性来定义基础规则 。

属性类型功能描述常用值举例
alignmentAlignmentGeometry没有用 Positioned 定位(或只定位了一个方向)的子组件进行对齐。Alignment.center (默认) Alignment.bottomRight
fitStackFit决定非定位组件如何调整自身大小以适应 StackStackFit.loose (默认,组件自己多大就多大) StackFit.expand (强制组件扩大到 Stack 的大小)
clipBehaviorClip当子组件超出 Stack 边界时,是否裁剪。Clip.hardEdge (裁剪) Clip.none (不裁剪,默认)

📌 Positioned 组件详解

Positioned 就是定位工具,它必须作为 Stack 的子孙组件使用。通过设置距离 Stack 各边的距离来固定位置 。

  • 定位属性topbottomleftrightwidthheight 。
  • 关键规则:在同一个轴上,不能同时设置三个属性。例如,水平方向上,你只能设置 leftrightwidth 中的任意两个,第三个会根据 Stack 的大小自动计算 -9
  • 特殊用法:如果你同时设置了 top 和 bottom,那么子组件会被强制拉伸以适应这个高度范围 。

为了让概念更清晰,你可以参考下面的布局决策流程:

deepseek_mermaid_20260226_0c0d12.png deepseek_mermaid_20260226_0c0d12.png

🚀 组合使用与代码示例

import 'package:flutter/material.dart';

void main(List<String> args) {
  runApp(MainPage());
}

class MainPage extends StatelessWidget {
  const MainPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("Stack/Positioned组件"),
        ),
        body: Container(
          width: double.infinity,
          height: double.infinity,
          color: Colors.amber,
          child:Stack(
            children: [
              Container(
                width: 200,
                height: 200,
                color: Colors.blueGrey,
              ),
              Positioned(
                top:10,
                left:10,
                child:Container(
                  width: 50,
                  height: 50,
                  color: Colors.red,
                ) 
              ),
              Positioned(
                bottom:10,
                right:10,
                child:Container(
                  width: 50,
                  height: 50,
                  color: Colors.green,
                ) 
              )
            ],
          )
        )
      ),
    );
  }
}

执行案例

image.png

总结

  • Stack 是一个允许子组件层叠的容器。它通过 alignment 控制未定位的子组件,通过 fit 调整其大小 -6-9
  • Positioned 是 Stack 的专属定位工具,通过 topbottomleftright 实现绝对定位 -7