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('可收缩的文本,不会强制撑满剩余空间'),
    ),
  ],
)
对比ExpandedFlexible
是否强制填满剩余空间✅ 是❌ 否
适用场景均匀分割空间弹性缩放不强制

二、高级布局 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

对比LayoutBuilderMediaQuery
获取的约束父 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 组件复用与组合

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐