Flutter: 布局系统
Flutter 布局像一条流水线——父组件传递约束,子组件报告尺寸,父组件决定位置。
📋 前置知识:需要先掌握以下内容
- 第3课:Widget 基础
1. 你将学到
- 盒模型:Constraints → Size → Position 约束传递协议
- 线性布局:Row/Column 与 Main/CrossAxisAlignment 对齐
- 弹性布局:Flex/Expanded/Flexible 比例分配
- 层叠布局:Stack/Positioned 实现浮层与徽标
- ShopApp 商品网格布局:GridView.count 与瀑布流
2. 一个 UI 设计师转开发的故事
(1) 痛点:布局溢出黄条噩梦
Bob 拿到 ShopApp 的 UI 设计稿,商品卡片需要左边图片、右边信息,底部价格行。他用 Row 套 Column,结果屏幕上全是黄黑条纹的溢出警告。调整了半天 padding 和 margin,不是文字被截断就是图片变形。1000 个商品页面,10 种屏幕尺寸,布局问题层出不穷。
(2) 约束传递的解法
Flutter 布局的核心规则只有一条:约束向下传递,尺寸向上报告,位置由父决定。理解了这个规则,Bob 就能预判每个 Widget 的行为。
DART
import 'package:flutter/material.dart';
// Parent gives tight constraints: exactly 200x100
SizedBox(
width: 200,
height: 100,
child: Text('I will be 200x100'), // Text forced to 200x100
)
// Parent gives loose constraints: 0 to maxWidth
Row(
children: [
Expanded(child: Text('I take remaining space')),
SizedBox(width: 80, child: Text('I am 80 wide')),
],
)
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(3) 收益:一次写对布局
Bob 掌握约束传递后,布局一次写对率从 30% 提升到 90%,溢出警告彻底消失。
3. 盒模型与约束传递
graph TD
subgraph Constraint Passing
P[Parent Constraints] --> C[Child Layout]
C --> S[Child Size]
S --> P
end
subgraph Common Layouts
R[Row] --> EX1[Expanded]
CO[Column] --> EX2[Flexible]
ST[Stack] --> POS[Positioned]
end
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(1) 约束类型
| 类型 | 说明 | 示例 |
|---|---|---|
| Tight 紧约束 | min = max,尺寸固定 | SizedBox(width: 100) |
| Loose 松约束 | min = 0, max = 父最大 | Center 中的子组件 |
| Unbounded 无界 | max = infinity | ListView 中的 Column |
| Bounded 有界 | min 和 max 都有限 | Page 中的 Column |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:约束传递演示
DART
import 'package:flutter/material.dart';
// Demo: constraint passing in action
class ConstraintDemo extends StatelessWidget {
const ConstraintDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Tight constraint: exactly 200 height
Container(
height: 200,
color: Colors.blue[100],
child: const Center(child: Text('Height: 200 (tight)')),
),
// Loose constraint: takes remaining space
Expanded(
child: Container(
color: Colors.green[100],
child: const Center(child: Text('Expanded (loose, fills remaining)')),
),
),
],
),
);
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
4. 线性布局:Row 与 Column
(1) 对齐属性
| 属性 | 方向 | Row 效果 | Column 效果 |
|---|---|---|---|
mainAxisAlignment |
主轴 | 水平对齐 | 垂直对齐 |
crossAxisAlignment |
交叉轴 | 垂直对齐 | 水平对齐 |
mainAxisSize |
主轴尺寸 | 宽度 | 高度 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:商品信息行(Row)
DART
import 'package:flutter/material.dart';
// Product info row: image + details
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Fixed-size image
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
'https://cdn.shopapp.com/laptop.jpg',
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
// Flexible details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Pro Laptop 16"',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('Electronics > Laptops',
style: TextStyle(fontSize: 12, color: Colors.grey[600])),
const SizedBox(height: 8),
Row(
children: [
const Text('\$1,299.99',
style: TextStyle(fontSize: 18, color: Colors.green)),
const SizedBox(width: 8),
Text('\$1,599.99',
style: TextStyle(
fontSize: 14,
decoration: TextDecoration.lineThrough,
color: Colors.grey[400],
)),
],
),
],
),
),
],
)
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(2) 常见对齐值对比
| MainAxisAlignment | 效果 |
|---|---|
start |
起始对齐(默认) |
center |
居中 |
end |
末尾对齐 |
spaceBetween |
首尾贴边,中间等距 |
spaceAround |
每项两侧等距 |
spaceEvenly |
所有间距相等 |
5. 弹性布局:Expanded 与 Flexible
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:价格与按钮弹性布局
DART
import 'package:flutter/material.dart';
// Bottom bar: price (expanded) + button (fixed)
Container(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
flex: 2, // Takes 2/3 of space
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Total', style: TextStyle(fontSize: 12)),
Text('\$${total.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
],
),
),
Expanded(
flex: 1, // Takes 1/3 of space
child: ElevatedButton(
onPressed: onCheckout,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16)),
child: const Text('Checkout'),
),
),
],
),
)
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
| Widget | 行为 | 剩余空间 | 适用场景 |
|---|---|---|---|
Expanded |
强制填满 | 按比例分配 | 需要占满空间 |
Flexible |
可选填满 | 按比例分配,可小于剩余 | 需要弹性但不强制 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:Expanded vs Flexible
DART
import 'package:flutter/material.dart';
Row(
children: [
// Expanded: must fill allocated space
Expanded(
flex: 1,
child: Container(color: Colors.red, child: const Text('Expanded')),
),
// Flexible: can be smaller than allocated space
Flexible(
flex: 1,
child: Container(
color: Colors.blue,
child: const Text('Flexible'), // May not fill all space
),
),
// Fixed width
SizedBox(width: 80, child: Container(color: Colors.green, child: const Text('Fixed'))),
],
)
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
6. 层叠布局:Stack 与 Positioned
Stack 允许子组件按绘制顺序层叠,Positioned 控制绝对定位。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:商品图片角标
DART
import 'package:flutter/material.dart';
// 简化类定义
class Product {
final String imageUrl;
const Product({required this.imageUrl});
}
// Product image with discount badge and favorite button
Stack(
children: [
// Base image
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
product.imageUrl,
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
),
// Discount badge (top-left)
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: Text('${discountPercent}% OFF',
style: const TextStyle(color: Colors.white, fontSize: 12)),
),
),
// Favorite button (top-right)
Positioned(
top: 8,
right: 8,
child: CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.8),
child: const Icon(Icons.favorite_border, size: 18),
),
),
// Stock indicator (bottom)
Positioned(
bottom: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: const Text('In Stock', style: TextStyle(color: Colors.white, fontSize: 10)),
),
),
],
)
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
| Stack 属性 | 说明 | 默认值 |
|---|---|---|
alignment |
未 Positioned 子组件对齐 | AlignmentDirectional.topStart |
fit |
子组件适配方式 | StackFit.loose |
clipBehavior |
溢出裁剪 | Clip.hardEdge |
7. GridView 网格布局
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:ShopApp 商品网格
DART
import 'package:flutter/material.dart';
// 简化类定义
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
// Two-column product grid
GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.75, // height = width / 0.75
padding: const EdgeInsets.all(8),
children: products.map((product) => ProductCard(
key: ValueKey(product.id),
product: product,
onAddToCart: () => addToCart(product),
)).toList(),
)
// Responsive grid with LayoutBuilder
LayoutBuilder(
builder: (context, constraints) {
int columns = (constraints.maxWidth / 180).floor().clamp(2, 4);
return GridView.count(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.75,
children: productWidgets,
);
},
)
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
| GridView 属性 | 说明 |
|---|---|
crossAxisCount |
每行列数 |
mainAxisSpacing |
主轴间距 |
crossAxisSpacing |
交叉轴间距 |
childAspectRatio |
子项宽高比 |
padding |
内边距 |
8. 完整示例:ShopApp 首页布局
DART
import 'package:flutter/material.dart';
class ShopHomePage extends StatelessWidget {
const ShopHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ShopApp'),
actions: [
Stack(
children: [
IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
Positioned(right: 4, top: 4,
child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
child: const Text('5', style: TextStyle(fontSize: 10, color: Colors.white)))),
],
),
],
),
body: CustomScrollView(
slivers: [
// Banner section
SliverToBoxAdapter(
child: Container(
height: 180,
color: Colors.blue[50],
child: const Center(child: Text('Flash Sale - Up to 50% OFF',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold))),
),
),
// Category chips
SliverToBoxAdapter(
child: SizedBox(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
children: ['All', 'Electronics', 'Clothing', 'Books', 'Home']
.map((c) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Chip(label: Text(c)),
)).toList(),
),
),
),
// Product grid
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: SliverGrid.count(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.7,
children: List.generate(20, (i) => _buildProductCard(i)),
),
),
],
),
);
}
Widget _buildProductCard(int index) {
return Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: Container(
color: Colors.grey[200],
child: const Icon(Icons.image, size: 48, color: Colors.grey),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Product ${index + 1}', maxLines: 1, overflow: TextOverflow.ellipsis),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('\$${(19.99 + index * 10).toStringAsFixed(2)}',
style: const TextStyle(color: Colors.green)),
const Icon(Icons.add_circle, size: 20),
],
),
],
),
),
),
],
),
);
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
❓ 常见问题
Q Row 里面出现黄黑溢出条纹怎么办?
A Row 子组件总宽超过 Row 宽度。用 Expanded 包裹可变宽度的子组件,或用 Flexible 让子组件自适应。
Q Expanded 和 Flexible 有什么区别?
A Expanded 强制填满分配空间(相当于 Flexible(fit: FlexFit.tight));Flexible 允许子组件小于分配空间(fit: FlexFit.loose)。
Q Stack 中未用 Positioned 的子组件怎么定位?
A 按 Stack 的 alignment 属性定位,默认左上角。
Q GridView.count 和 GridView.builder 有什么区别?
A count 适合少量固定项;builder 懒加载适合大数据列表(千万级商品用 builder)。
Q 为什么 Column 里的 ListView 会报错?
A Column 给子组件无界高度约束,ListView 无法确定自身高度。用 Expanded 包 ListView,或用 shrinkWrap: true。
Q childAspectRatio 怎么算?
A ratio = width / height。如果每项宽 180、高 240,则 ratio = 180/240 = 0.75。
📖 小节
- Flutter 布局核心:约束向下传递,尺寸向上报告,位置由父决定
- Row/Column 是线性布局,用 Main/CrossAxisAlignment 控制对齐
- Expanded 强制填满,Flexible 弹性适应,flex 控制比例
- Stack + Positioned 实现层叠效果(角标、浮层)
- GridView.count 构建商品网格,LayoutBuilder 实现响应式列数
📝 作业
- 基础题(难度⭐):创建一个水平滚动分类标签栏(Chip + ListView 水平方向)。
- 进阶题(难度⭐⭐):实现一个商品详情页布局:顶部大图 + 中间信息区 + 底部价格/购买按钮栏,用 Column + Expanded。
- 挑战题(难度⭐⭐⭐):用 LayoutBuilder 实现响应式商品网格:手机 2 列、平板 3 列、桌面 4 列,窗口缩放时实时切换。