2.2 布局系统
·
Flutter 的布局系统基于约束传递原则:父 Widget 向子 Widget 传递约束(Constraints),子 Widget 在约束范围内决定自身尺寸。理解这一机制是掌握复杂布局的关键。
一、基础布局 Widget
1.1 Container
Container 是最常用的布局容器,集合了尺寸、边距、装饰等功能:
Container(
width: 200,
height: 100,
margin: const EdgeInsets.all(16), // 外边距
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12,
), // 内边距
alignment: Alignment.center, // 子 Widget 对齐方式
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
gradient: const LinearGradient(
colors: [Colors.blue, Colors.purple],
),
),
child: const Text('Hello Flutter'),
)
1.2 Row 与 Column
// Row:水平排列
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // 主轴对齐
crossAxisAlignment: CrossAxisAlignment.center, // 交叉轴对齐
mainAxisSize: MainAxisSize.min, // 主轴尺寸
children: [
const Icon(Icons.star),
const Text('Rating'),
const Text('4.8'),
],
)
// Column:垂直排列
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch, // 子 Widget 撑满宽度
children: [
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: 8),
const Text('Description text here'),
],
)
MainAxisAlignment 对比:
| 值 | 效果 |
|---|---|
start | 靠起始端对齐 |
end | 靠末尾端对齐 |
center | 居中 |
spaceBetween | 两端对齐,间距均匀 |
spaceAround | 首尾各有半间距 |
spaceEvenly | 所有间距均匀(含首尾) |
1.3 Stack
Stack 允许子 Widget 重叠堆叠:
Stack(
alignment: Alignment.center, // 未定位子 Widget 的默认对齐
fit: StackFit.expand, // 子 Widget 填满 Stack
children: [
// 背景图片
Image.network(imageUrl, fit: BoxFit.cover),
// 渐变遮罩
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black54],
),
),
),
// 定位文字
Positioned(
bottom: 16,
left: 16,
right: 16,
child: Text(
'标题文字',
style: const TextStyle(color: Colors.white, fontSize: 18),
),
),
],
)
1.4 Expanded 与 Flexible
Row(
children: [
// Expanded:占满剩余空间
Expanded(
flex: 2, // 权重为 2
child: Container(color: Colors.red),
),
Expanded(
flex: 1, // 权重为 1
child: Container(color: Colors.blue),
),
// Flexible:弹性但不强制填满
Flexible(
child: Text('可收缩的文本,不会强制撑满剩余空间'),
),
],
)
| 对比 | Expanded | Flexible |
|---|---|---|
| 是否强制填满剩余空间 | ✅ 是 | ❌ 否 |
| 适用场景 | 均匀分割空间 | 弹性缩放不强制 |
二、高级布局 Widget
2.1 GridView
// GridView.count:固定列数
GridView.count(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1.0, // 宽高比
children: List.generate(9, (index) => Card(
child: Center(child: Text('$index')),
)),
)
// GridView.builder:懒加载(推荐大列表)
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: products.length,
itemBuilder: (context, index) => ProductCard(product: products[index]),
)
// GridView.extent:指定最大宽度自适应列数
GridView.extent(
maxCrossAxisExtent: 180, // 每格最大宽度
children: [...],
)
2.2 CustomMultiChildLayout
class DashboardDelegate extends MultiChildLayoutDelegate {
void performLayout(Size size) {
// 布局头部
if (hasChild('header')) {
layoutChild('header', BoxConstraints(maxWidth: size.width));
positionChild('header', Offset.zero);
}
// 布局内容区
if (hasChild('content')) {
final headerSize = layoutChild('content',
BoxConstraints(maxWidth: size.width, maxHeight: size.height - 60));
positionChild('content', const Offset(0, 60));
}
}
bool shouldRelayout(DashboardDelegate oldDelegate) => false;
}
// 使用
CustomMultiChildLayout(
delegate: DashboardDelegate(),
children: [
LayoutId(id: 'header', child: AppBar()),
LayoutId(id: 'content', child: ContentWidget()),
],
)
2.3 Table
Table(
border: TableBorder.all(color: Colors.grey),
columnWidths: const {
0: FixedColumnWidth(100),
1: FlexColumnWidth(),
2: FixedColumnWidth(80),
},
children: [
// 表头
TableRow(
decoration: BoxDecoration(color: Colors.blue.shade50),
children: ['Name', 'Email', 'Age']
.map((h) => TableCell(
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(h, style: const TextStyle(fontWeight: FontWeight.bold)),
),
))
.toList(),
),
// 数据行
...users.map((u) => TableRow(children: [
TableCell(child: Padding(padding: const EdgeInsets.all(8), child: Text(u.name))),
TableCell(child: Padding(padding: const EdgeInsets.all(8), child: Text(u.email))),
TableCell(child: Padding(padding: const EdgeInsets.all(8), child: Text('${u.age}'))),
])),
],
)
三、约束系统(Constraints)
3.1 核心原则
“Constraints go down. Sizes go up. Parent sets position.”
约束向下传递,尺寸向上返回,父 Widget 决定子 Widget 的位置。
父 Widget
↓ 传递 BoxConstraints(minWidth, maxWidth, minHeight, maxHeight)
子 Widget
↑ 返回自身尺寸(width, height)
父 Widget
→ 决定子 Widget 的位置(offset)
3.2 BoxConstraints
// 约束类型
BoxConstraints.tight(Size(200, 100)) // 强制固定尺寸
BoxConstraints.loose(Size(200, 100)) // 最大不超过,最小为 0
BoxConstraints.expand() // 填满可用空间
BoxConstraints(maxWidth: 300) // 只限制最大宽度
// 常见约束场景
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 100,
maxWidth: 300,
minHeight: 50,
),
child: Text('自适应宽高但有约束'),
)
// 无限制(配合 SingleChildScrollView 使用)
UnconstrainedBox(
child: Container(width: 500), // 可以超过屏幕宽度
)
3.3 LayoutBuilder
LayoutBuilder 可以获取父 Widget 传入的约束,实现响应式布局:
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
// 宽屏布局(平板/桌面)
return Row(children: [
Expanded(child: NavigationPanel()),
Expanded(flex: 3, child: ContentArea()),
]);
} else {
// 窄屏布局(手机)
return ContentArea();
}
},
)
四、自适应布局
4.1 MediaQuery
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final screenWidth = mediaQuery.size.width;
final screenHeight = mediaQuery.size.height;
final orientation = mediaQuery.orientation;
final textScaleFactor = mediaQuery.textScaler;
final topPadding = mediaQuery.padding.top; // 状态栏高度
final bottomPadding = mediaQuery.padding.bottom; // Home 指示器高度
return Padding(
padding: EdgeInsets.only(bottom: mediaQuery.viewInsets.bottom), // 键盘高度
child: Container(
width: screenWidth * 0.9,
child: Text('Responsive'),
),
);
}
4.2 OrientationBuilder
OrientationBuilder(
builder: (context, orientation) {
return GridView.count(
crossAxisCount: orientation == Orientation.portrait ? 2 : 4,
children: [...],
);
},
)
4.3 LayoutBuilder vs MediaQuery
| 对比 | LayoutBuilder | MediaQuery |
|---|---|---|
| 获取的约束 | 父 Widget 传入的约束 | 设备屏幕信息 |
| 适用场景 | 组件内部自适应 | 全局屏幕适配 |
| 精确度 | 精确(当前 Widget 可用空间) | 全屏尺寸 |
| 推荐场景 | 可复用组件内部布局 | 全局平台判断 |
五、约束与渲染机制
5.1 RenderObject 树
每个 Widget 对应一个 RenderObject(或复用),RenderObject 负责实际的布局和绘制:
RenderObject
├── layout() → 计算自身尺寸和子 Widget 布局
├── paint() → 在 Canvas 上绘制
└── hitTest() → 处理点击事件
5.2 常见约束陷阱
// ❌ 错误:Column 中直接放 ListView(高度无限导致报错)
Column(children: [
ListView(children: [...]), // 报错:Vertical viewport was given unbounded height
])
// ✅ 正确:用 Expanded 或指定高度
Column(children: [
Expanded(
child: ListView(children: [...]),
),
])
// ❌ 错误:在 Scaffold 外使用 SafeArea
SafeArea(child: Scaffold(...)) // SafeArea 在外层,无法获取正确 padding
// ✅ 正确:SafeArea 包裹内容
Scaffold(
body: SafeArea(child: content),
)
小结
| 知识点 | 核心要点 |
|---|---|
| Container | 集合尺寸/边距/装饰的万能容器 |
| Row / Column | 线性布局,MainAxisAlignment 控制分布 |
| Stack | 层叠布局,Positioned 精确定位 |
| Expanded / Flexible | 弹性布局,flex 控制权重 |
| GridView | 网格布局,builder 懒加载大量数据 |
| 约束原则 | 约束向下,尺寸向上,父定位置 |
| LayoutBuilder | 获取可用约束,实现组件级响应式 |
| MediaQuery | 获取屏幕信息,实现全局适配 |
👉 下一节:2.3 组件复用与组合
更多推荐
所有评论(0)