Flutter那些事-布局篇

Flex

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

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

一、Flex 布局的核心思想

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

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

二、基础容器:FlexRowColumn

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

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

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

三、关键属性详解(以 RowColumn 为例)

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

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

四、弹性伸缩:ExpandedFlexibleSpacer

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

1. Expanded:填充分配的空间

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

  • flex 参数 :一个整数,表示弹性系数。如果有多个 Expanded,它们将按照 flex 值的比例来分割主轴剩余空间-6
js 复制代码
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:此模式下的 FlexibleExpanded 行为完全一致,强制子组件填充分配到的空间。
    • FlexFit.loose(默认):允许子组件最大不超过分配到的空间,但子组件可以保持其原有大小(如果原有大小小于分配空间,则不会强制拉伸填满)。

3. Spacer:占据空间的空白

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

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

五、布局流程与常见问题

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

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

常见问题

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

Wrap

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

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

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

核心用途

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

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

构造函数与属性详解

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

kotlin 复制代码
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列表
})

关键属性解析

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

属性 类型 功能描述 类比理解 常用值举例
direction Axis 主轴方向,决定了子widget是水平排列还是垂直排列。 想象成河流的流向。 Axis.horizontal(默认) Axis.vertical
spacing double 主轴方向上,相邻两个子widget之间的空隙。 单词与单词之间的空格。 8.010.0
runSpacing double 交叉轴方向上,每一"行"(或"列")之间的空隙 段落与段落之间的行距。 4.015.0
alignment WrapAlignment 主轴方向上,当前"行"(或"列")内子widget的排列方式。 Word文档中的"左对齐"、"居中"、"右对齐"、"两端对齐"。 startcenterendspaceBetween
runAlignment WrapAlignment 交叉轴方向上,所有"行"(或"列")作为一个整体,在父容器中的排列方式。 多行文本在页面中的"顶端对齐"、"垂直居中"、"底端对齐"。 startcenterendspaceAround
crossAxisAlignment WrapCrossAlignment 交叉轴方向上,当前"行"(或"列")内子widget彼此之间的对齐方式。 一行中,有的孩子个子高,有的孩子个子矮,他们是在顶部对齐、底部对齐还是中间对齐。 startcenterend

代码示例

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

js 复制代码
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 的区别RowColumn 只能占据一行/一列,空间不足时会报错;而 Wrap 会自动创建新的行/列来容纳子元素。
  • 核心概念 :理解 主轴 (direction)交叉轴 的方向,以及 spacing (控制元素间距)与 runSpacing (控制行/列间距)的区别,是掌握 Wrap 的关键。

Stack/Positioned

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

核心概念:画布与图层

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

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

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

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

📦 Stack 组件详解

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

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

📌 Positioned 组件详解

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

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

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

🚀 组合使用与代码示例

js 复制代码
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,
                ) 
              )
            ],
          )
        )
      ),
    );
  }
}

执行案例

总结

  • Stack 是一个允许子组件层叠 的容器。它通过 alignment 控制未定位的子组件,通过 fit 调整其大小 -6-9
  • PositionedStack 的专属定位工具,通过 topbottomleftright 实现绝对定位 -7
相关推荐
王码码20352 小时前
Flutter for OpenHarmony:socket_io_client 实时通信的事实标准(Node.js 后端的最佳拍档) 深度解析与鸿蒙适配指南
android·flutter·ui·华为·node.js·harmonyos
zhangkai2 小时前
flutter存储知识点总结
flutter·ios
一个假的前端男3 小时前
# 从零开始创建 Flutter Web 项目(附 VS Code 插件推荐)
前端·flutter·react.js
一个假的前端男4 小时前
[特殊字符] Flutter 安装完整指南 Windows—— 2026最新版
windows·flutter
程序员老刘4 小时前
Flutter版本选择指南:3.41 发布,稳定的开年 | 2026年2月
flutter·客户端
恋猫de小郭4 小时前
Flutter 的真正价值是什么?深度解析再结合鸿蒙,告诉你 Flutter 的真正优势
android·前端·flutter
2501_921930837 小时前
Flutter for OpenHarmony:三方库实战 animations 页面过渡动画详解
flutter
lqj_本人7 小时前
Flutter三方库适配OpenHarmony【apple_product_name】oh-package.json5配置详解
flutter
2501_921930838 小时前
第三方库引入实战指南 Flutter for OpenHarmony:path_provider 文件路径详解
flutter