Flutter-详解布局(核心布局控件)

上一章我们详细的学习了 Flutter 中的Widget,这一章我们将要学习 Flutter 的布局, 在上一章我们了解到了:Everything is a widget,在 Flutter 中几乎所有的对象都是一个 Widget ,当然也包括布局,Flutter 的布局系统基于 Widget树,通过组合不同的布局 Widget 实现复杂的 UI,你在 Flutter 应用程序中能直接看到的图像,图标和文本都是 Widget。此外不能直接看到的也是 Widget,如用来排列、限制和对齐可见 Widget 的行、列和网格。

DEMO

布局规则

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),
    ],
)
ROW
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),
    ],
)
Column

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"), // 文字居中
    ],
)
Stack

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')),
    ],
)
Wrap

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],
          )),
)
Flow

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'),
      );
    },
  )
ListView

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')),
          )),
)
)

GridView

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'),
      ],
    ),
  ],
)

Table

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'),
      ],
    ),
  ],
)

CustomScrollView

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"),
            ),
          ],
        ),
      ),
    );
  }
}

IndexedStack

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),
    ),
  ],
)
Flex

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]),
    ],
  ),
)

ListBody

因为网站字数限制,只能分系列了,需要一次性看完请去这里
Flutter-详解布局(单子组件布局控件)
Flutter-详解布局(弹性和层叠布局辅助控件)
Flutter-详解布局(滚动和Sliver系列布局控件)
Flutter-详解布局(响应式和平台适配及特殊布局控件)
需要代码去这里DEMO

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容