上一章我们详细的学习了 Flutter 中的Widget,这一章我们将要学习 Flutter 的
布局
, 在上一章我们了解到了:Everything is a widget,在 Flutter 中几乎所有的对象都是一个Widget
,当然也包括布局,Flutter 的布局系统基于Widget
树,通过组合不同的布局Widget
实现复杂的 UI,你在 Flutter 应用程序中能直接看到的图像,图标和文本都是Widget
。此外不能直接看到的也是Widget
,如用来排列、限制和对齐可见Widget
的行、列和网格。
布局规则
1.约束由父组件向子组件传导,大小由子组件向父组件传导,位置由父组件决定。
2.布局Widget是Flutter UI的基础,理解它们的规则和适用场景非常重要
核心布局控件 (多子组件)
1. Row & Column
说明: Row水平排列子Widget,Column垂直排列子Widget
规则: 子Widget可以是非弹性(不扩展)或弹性(Expanded)。主轴-mainAxisAlignment(Row 为 X 轴,Column 为 Y 轴)和交叉轴-crossAxisAlignment(Row 为 Y 轴,Column 为 X 轴)
注意:
1.当子Widget的总长度超过主轴长度时,会溢出(常见错误:黄色黑色条纹警告)。可以使用Expanded或Flexible来避免,或者使用ListView代替
2.如果没有指定主轴对齐方式(mainAxisAlignment)和交叉轴对齐方式(crossAxisAlignment),默认是start和stretch
推荐: 用于线性排列多个子Widget,如:导航栏(Row)、表单项列表(Column)、图文混排(Row + Column 嵌套),如果子Widget较多且可能超出屏幕,考虑使用ListView。
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主轴均匀分布
crossAxisAlignment: CrossAxisAlignment.center, // 交叉轴居中
children: [
Container(width: 50, height: 50, color: Colors.red),
Expanded(child: Container(height: 30, color: Colors.green)), // 弹性填充剩余空间
Container(width: 50, height: 50, color: Colors.blue),
],
)
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主轴均匀分布
crossAxisAlignment: CrossAxisAlignment.center, // 交叉轴居中
children: [
Container(width: 50, height: 50, color: Colors.red),
Expanded(child: Container(height: 30, color: Colors.green)), // 弹性填充剩余空间
Container(width: 50, height: 50, color: Colors.blue),
],
)
2. Stack
说明: 将子Widget重叠在一起。第一个子Widget在底部,后面的依次在上层
规则: 子Widget可以使用Positioned来定位,否则放置在左上角。如果没有定位,则根据alignment属性对齐(默认左上角),子 Widget 顺序决定绘制顺序(后绘制的在上层)
注意: 如果不使用Positioned,且Stack没有指定大小,那么Stack会调整到包裹所有未定位的子Widget(但如果有定位的子Widget,则定位的子Widget不影响Stack大小), Positioned 的子 Widget 可能超出 Stack 边界(需
Clip
处理)推荐: 用于需要重叠的布局,如图片上的标签、浮动按钮、自定义进度条
Stack(
alignment: Alignment.center, // 所有子Widget居中
children: [
Container(width: 200, height: 200, color: Colors.blue), // 底层
Positioned(
bottom: 10,
right: 10,
child: Container(width: 50, height: 50, color: Colors.red), // 定位到右下
),
const Text("Stack Example"), // 文字居中
],
)
3. Wrap
说明: 当一行(或一列)放不下子Widget时,自动换行(或换列)
规则: 可以设置方向(水平或垂直)、间距(spacing-主轴方向间距)和行间距(runSpacing-交叉轴方向行间距)
注意: 与Row不同,Wrap不会溢出,而是换行。但要注意如果子Widget很大且没有足够空间,可能会超出屏幕(在换行方向),按需换行,避免溢出,子 Widget 数量极大时应用 Wrap.builder,计算间距不要忽略 spacing 和 runSpacing 不然容易导致布局重叠
推荐: 用于流式布局,如标签列表、筛选条件栏、自适应按钮组
Wrap(
spacing: 8.0, // 水平间距
runSpacing: 4.0, // 垂直间距(行间距)
children: [
Chip(label: Text('标签1')),
Chip(label: Text('标签2')),
Chip(label: Text('标签3')),
Chip(label: Text('标签4')),
Chip(label: Text('标签5')),
Chip(label: Text('标签6')),
Chip(label: Text('标签7')),
Chip(label: Text('标签8')),
Chip(label: Text('标签9')),
Chip(label: Text('标签10')),
Chip(label: Text('标签11')),
Chip(label: Text('标签12')),
],
)
4. Flow
说明:
1.Flow 需要自定义布局逻辑时使用,通过delegate控制每个子Widget的位置和大小。性能较好,因为子Widget可以独立定位而不影响父Widget
2.动态增减子元素:在delegate中监听数据变化,调用context.invalidateLayout()重布局
规则: Flow:高性能自定义布局(需实现 FlowDelegate)
注意: 通过委托计算(delegate-calculated)实现动态自适应,胜任复杂场景,当布局复杂度低时,优先用Wrap简化开发
推荐: 复杂动画布局,避免Wrap的多次测量,适合动态加载/高频更新场景
class MyFlowDelegate extends FlowDelegate {
@override
void paintChildren(FlowPaintingContext context) {
var x = 0.0, y = 0.0;
for (var i = 0; i < context.childCount; i++) {
// 动态计算每个子组件位置
final childSize = context.getChildSize(i)!;
context.paintChild(i, transform: Matrix4.translationValues(x, y, 0));
x += childSize.width * 0.8; // 重叠效果
y += childSize.height * 0.2;
}
}
@override
bool shouldRepaint(covariant FlowDelegate oldDelegate) => true;
}
Flow(
delegate: MyFlowDelegate(),
children: List.generate(
5,
(index) => Container(
width: 80,
height: 80,
color: Colors.primaries[index],
)),
)
5. ListView
说明: 可滚动的线性布局,支持水平和垂直方向
规则: 如果子Widget数量固定,使用ListView(children: [...]),如果数量多,使用ListView.builder按需构建
注意: 直接使用children方式构建大量子Widget会导致性能问题,因为会一次性构建所有子Widget。对于长列表,务必使用builder,嵌套 ListView 时需明确滚动方向
推荐: 用于需要滚动的列表,例如消息列表、设置菜单、长数据展示
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
)
6. GridView
说明: 网格布局
规则: 有多种构造方式:GridView.count(固定列数)、GridView.extent(固定最大交叉轴长度)、GridView.builder(按需构建)
注意: 同样要注意性能,长列表使用builder,要正确设置 childAspectRatio 不然容易导致单元格变形
推荐: 用于展示网格状内容,如图片墙、产品网格、仪表盘卡片
var array = [Colors.blue, Colors.red, Colors.yellow];
GridView.count(
crossAxisCount: 3, // 每行3列
childAspectRatio: 1.0, // 宽高比
children: List.generate(
9,
(index) => Container(
decoration: BoxDecoration(
color: array[index % 3],
borderRadius: BorderRadius.circular(8.0),
),
child: Center(child: Text('Item $index')),
)),
)
)
7. Table
说明: 是一种基于表格模型的布局方式
规则: 每一行的高度由内容决定,列宽可通过 columnWidths 属性自定义。
支持多种列宽类型:FixedColumnWidth(固定宽度)、FlexColumnWidth(弹性比例)、FractionColumnWidth(百分比)等,通过 rowHeight 属性设置固定行高,或通过 TableRow 的 height 属性单独调整某一行,在 TableRow 中嵌套 Row、Column 或其他布局组件,实现复杂内容排版,通过 MediaQuery 动态调整列宽比例,适配不同屏幕尺寸
注意: 避免在大量数据中直接使用 Table,推荐结合 ListView 或 CustomScrollView 分页加载
推荐: 适用于需要行列对齐的复杂场景,Stack + Table:在表格中叠加其他组件(如悬浮按钮),Wrap + Table:结合弹性布局实现自适应表格
Table(
columnWidths: const {
0: FixedColumnWidth(80), // 第一列固定宽度
1: FlexColumnWidth(2), // 第二列弹性宽度(占比更大)
2: IntrinsicColumnWidth(), // 第三列自适应内容宽度
},
border: TableBorder.all(color: Colors.black, width: 1.0),
defaultVerticalAlignment: TableCellVerticalAlignment.middle, // 垂直居中
children: const [
TableRow(
// 表头行
decoration: BoxDecoration(color: Colors.grey),
children: [
Text('姓名', textAlign: TextAlign.center),
Text('性别', textAlign: TextAlign.center),
Text('年龄', textAlign: TextAlign.center),
],
),
TableRow(
// 数据行
children: [
Text('张三'),
Text('男'),
Text('25'),
],
),
],
)
8. CustomScrollView
说明: 是 Flutter 处理复杂滚动场景的核心工具,通过灵活组合 Sliver 组件,可实现头部折叠、多类型布局、吸顶等高级交互,
规则: 所有子组件共享同一个滚动控制器,确保联动滑动,仅渲染可见区域的 Sliver,其子组件必须是 Sliver 家族成员(如 SliverList、SliverGrid、SliverToBoxAdapter 等),普通组件需通过 SliverToBoxAdapter 包裹才能嵌入
注意: Sliver 的使用规范及性能优化,必要时结合
NestedScrollView
解决嵌套滚动问题,避免嵌套滚动组件推荐: 实现统一且复杂的滚动效果。它像“粘合剂”一样将不同布局粘合为整体滚动区域,解决嵌套滚动冲突问题(例如 ListView 嵌套 GridView 需手动指定高度)
Table(
columnWidths: const {
0: FixedColumnWidth(80), // 第一列固定宽度
1: FlexColumnWidth(2), // 第二列弹性宽度(占比更大)
2: IntrinsicColumnWidth(), // 第三列自适应内容宽度
},
border: TableBorder.all(color: Colors.black, width: 1.0),
defaultVerticalAlignment: TableCellVerticalAlignment.middle, // 垂直居中
children: const [
TableRow(
// 表头行
decoration: BoxDecoration(color: Colors.grey),
children: [
Text('姓名', textAlign: TextAlign.center),
Text('性别', textAlign: TextAlign.center),
Text('年龄', textAlign: TextAlign.center),
],
),
TableRow(
// 数据行
children: [
Text('张三'),
Text('男'),
Text('25'),
],
),
],
)
9. IndexedStack
说明: IndexedStack 是 Stack 的子类(extends Stack),是一种层叠布局组件,专门用于在同一位置切换显示不同子组件
规则: IndexedStack 继承自 Stack 组件,通过 index 属性控制当前显示的子组件。与 Stack 的叠加显示不同,IndexedStack 仅渲染指定索引的子项,其他子项处于隐藏状态,尺寸由最大的子组件决定,与当前显示的 index 无关。支持 alignment 对齐属性,可结合 Positioned 实现精确定位,所有子组件会预先加载到内存中,适合需要保持面状态的场景(如 Tab 切换)。通过 Offstage 隐藏非活跃子项,而非销毁
注意: 子组件过多或复杂时可能引发内存压力,通过 setState 修改 index 实现无动画切换,适合高频操作场景,可通过 RepaintBoundary 优化绘制性能,需要精确定位时,推荐组合使用 Positioned 而非 Align
推荐: 优先用于少量子项的场景,复杂页面建议结合 PageView 使用
class DynamicIndexedStack extends StatefulWidget {
@override
_DynamicIndexedStackState createState() => _DynamicIndexedStackState();
}
class _DynamicIndexedStackState extends State<DynamicIndexedStack> {
int _currentIndex = 0;
final List<Widget> _pages = [
PageWidget(title: "页面1", color: Colors.red),
PageWidget(title: "页面2", color: Colors.green),
PageWidget(title: "页面3", color: Colors.blue),
];
@override
Widget build(BuildContext context) {
return Column(
children: [
// 切换按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildButton(0, "页面1"),
_buildButton(1, "页面2"),
_buildButton(2, "页面3"),
],
),
// IndexedStack区域
Expanded(
child: IndexedStack(
index: _currentIndex,
children: _pages,
),
)
],
);
}
Widget _buildButton(int index, String text) {
return ElevatedButton(
onPressed: () => setState(() => _currentIndex = index),
style: ElevatedButton.styleFrom(
backgroundColor: _currentIndex == index ? Colors.amber : null,
),
child: Text(text),
);
}
}
// 带状态的页面组件(验证状态保留)
class PageWidget extends StatefulWidget {
final String title;
final Color color;
PageWidget({required this.title, required this.color});
@override
_PageWidgetState createState() => _PageWidgetState();
}
class _PageWidgetState extends State<PageWidget> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Container(
color: widget.color.withOpacity(0.3),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.title, style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: () => setState(() => _counter++),
child: Text("计数: $_counter"),
),
],
),
),
);
}
}
10. Flex
说明: 动态增减子元素:使用Key触发子树重建
规则: Flex:更底层的 Row/Column(需配合 Flexible)
注意: 通过约束驱动(constraint-driven)实现空间比例分配,适合结构化界面
推荐: 自定义排版
Flex(
// 等价于Column
direction: Axis.vertical,
children: [
Expanded(
flex: 1, // 占1/3高度
child: Container(color: Colors.red),
),
Expanded(
flex: 2, // 占2/3高度
child: Container(color: Colors.blue),
),
],
)
11. ListBody
说明: 是一个沿指定轴方向顺序排列子组件的布局控件,其核心特性是不限制主轴空间,需配合父容器约束使用
规则: 必须嵌套在ListView、Column等有界容器内,子组件在交叉轴(如水平方向)自动拉伸,无需额外设置,ListBody仅负责线性排列,不包含滚动机制,ListView = ListBody + 滚动功能,适合长列表
注意: 子项数量动态或可能超出屏幕时,必须用ListView替代,避免布局溢出,子组件尺寸变化会触发整个ListBody重新布局,影响性能,对动态子项优先使用ListView.builder ,仅构建可见区域组件
推荐: 固定数量子项的线性排列(如静态菜单栏、卡片组),需父容器明确尺寸约束(如SizedBox、ConstrainedBox)
SingleChildScrollView(
// 提供滚动支持
child: ListBody(
mainAxis: Axis.vertical,
reverse: false,
children: <Widget>[
Container(height: 100, color: Colors.blue[50]),
Container(height: 100, color: Colors.blue[100]),
Container(height: 100, color: Colors.blue[200]),
],
),
)
因为网站字数限制,只能分系列了,需要一次性看完请去这里
Flutter-详解布局(单子组件布局控件)
Flutter-详解布局(弹性和层叠布局辅助控件)
Flutter-详解布局(滚动和Sliver系列布局控件)
Flutter-详解布局(响应式和平台适配及特殊布局控件)
需要代码去这里DEMO