Flex
Flutter 的布局核心大多围绕弹性布局(Flex) 模型构建,它非常强大且灵活,类似于前端的 Flexbox。理解 Flex 布局,是掌握 Flutter 界面构建的基石。
简单来说,Flutter 的 Flex 布局允许你在一维方向上(水平或垂直)排列子组件,并灵活分配它们之间的空间。
一、Flex 布局的核心思想
在 Flex 布局中,有两个核心轴的概念你需要先了解,这对理解后续所有属性至关重要-2-8:
- 主轴 (Main Axis) :Flex 容器排列子组件的方向。如果容器是水平的(
Row),主轴就是水平方向;如果是垂直的(Column),主轴就是垂直方向。 - 交叉轴 (Cross Axis) :与主轴垂直的另一条轴。
二、基础容器:Flex、Row 与 Column
在 Flutter 中,你主要通过三个组件来使用 Flex 布局:
Flex组件:这是最基础的弹性布局组件。它允许你通过direction参数明确指定主轴方向(Axis.horizontal或Axis.vertical)。Row组件:它是Flex的一个专用版本,固定了主轴方向为水平 (Axis.horizontal),用于横向排列子组件。Column组件:它也是Flex的专用版本,固定了主轴方向为垂直 (Axis.vertical),用于纵向排列子组件。
在实际开发中,因为方向是确定的,我们几乎总是直接使用更简洁的 Row 和 Column。
三、关键属性详解(以 Row 和 Column 为例)
这些组件的属性非常丰富,可以精确控制子组件的布局行为。下表整理了最核心的几个属性:
| 属性 | 类型 | 作用范围 | 说明与常用值 |
|---|---|---|---|
mainAxisAlignment | MainAxisAlignment | 主轴 | 决定了子组件在主轴上的排列方式 -1-10。 - start:从主轴起点开始排列(默认) - end:向主轴终点对齐 - center:居中对齐 - spaceBetween:首尾靠边,中间等距 - spaceAround:每个子项两侧间距相等,但首尾间距是中间的一半 - spaceEvenly:所有间距(包括首尾)完全相等 |
mainAxisSize | MainAxisSize | 主轴 | 决定了 Row 或 Column 自身在主轴方向占用多大空间 -2-7。 - max:占用父容器允许的最大空间(默认) - min:仅占用包裹其子组件所需的最小空间 |
crossAxisAlignment | CrossAxisAlignment | 交叉轴 | 决定了子组件在交叉轴上的对齐方式 -1-6。 - start:与交叉轴起点对齐(如 Row 则顶部对齐) - end:与交叉轴终点对齐(如 Row 则底部对齐) - center:居中对齐(默认) - stretch:拉伸填满交叉轴方向的空间 - baseline:使子项的文字基线对齐(需配合 textBaseline 使用) |
textDirection | TextDirection | 水平方向 | 决定主轴水平方向的起始点,影响 start 和 end 的解释 -2-7。 - ltr:从左到右(默认,start 代表左) - rtl:从右到左(start 代表右) |
verticalDirection | VerticalDirection | 垂直方向 | 决定垂直方向的起始点,影响 start 和 end 的解释 -2-6。 - down:从上到下(默认,start 代表顶部) - up:从下到上(start 代表底部) |
四、弹性伸缩:Expanded、Flexible 与 Spacer
除了对齐,Flex 布局最强大的地方在于按比例分配空间。这需要借助另外三个组件。
1. Expanded:填充分配的空间
Expanded 组件必须作为 Row、Column 或 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 的布局算法大致分为六步:
- 布局非弹性子项:先布局那些没有被
Expanded或Flexible包裹的子项。 - 分配剩余空间:计算主轴剩余空间,并按
flex比例分配给弹性子项。 - 布局弹性子项:根据分配到的空间大小,布局这些弹性子项。
- 确定容器大小:交叉轴大小取子项的最大值;主轴大小由
mainAxisSize决定。 - 确定子项位置:根据
mainAxisAlignment和crossAxisAlignment放置所有子项。
常见问题:
- “溢出了” (Overflow) :当子项内容总宽度(或高度)超出父容器,且没有使用
Expanded进行弹性约束时,你会看到黄黑相间的警告条纹。解决方案是使用Expanded包裹可伸缩的子项,或者考虑使用ListView来支持滚动。 Expanded嵌套失效:Expanded只能直接放在Row、Column或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列表
})
关键属性解析
为了让你更清晰地了解每个属性的作用,可以对照下表:
| 属性 | 类型 | 功能描述 | 类比理解 | 常用值举例 |
|---|---|---|---|---|
direction | Axis | 主轴方向,决定了子widget是水平排列还是垂直排列。 | 想象成河流的流向。 | Axis.horizontal(默认) Axis.vertical |
spacing | double | 主轴方向上,相邻两个子widget之间的空隙。 | 单词与单词之间的空格。 | 8.0, 10.0 |
runSpacing | double | 交叉轴方向上,每一“行”(或“列”)之间的空隙 | 段落与段落之间的行距。 | 4.0, 15.0 |
alignment | WrapAlignment | 主轴方向上,当前“行”(或“列”)内子widget的排列方式。 | Word文档中的“左对齐”、“居中”、“右对齐”、“两端对齐”。 | start, center, end, spaceBetween |
runAlignment | WrapAlignment | 交叉轴方向上,所有“行”(或“列”)作为一个整体,在父容器中的排列方式。 | 多行文本在页面中的“顶端对齐”、“垂直居中”、“底端对齐”。 | start, center, end, spaceAround |
crossAxisAlignment | WrapCrossAlignment | 交叉轴方向上,当前“行”(或“列”)内子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的子组件分为两种 :- 非定位组件:没有用
Positioned包裹的组件。它们决定了Stack自身的大小。 - 定位组件:被
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 各边的距离来固定位置 。
- 定位属性:
top、bottom、left、right、width、height。 - 关键规则:在同一个轴上,不能同时设置三个属性。例如,水平方向上,你只能设置
left、right、width中的任意两个,第三个会根据Stack的大小自动计算 -9。 - 特殊用法:如果你同时设置了
top和bottom,那么子组件会被强制拉伸以适应这个高度范围 。
为了让概念更清晰,你可以参考下面的布局决策流程:
🚀 组合使用与代码示例
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,
)
)
],
)
)
),
);
}
}
执行案例
总结