上一章我们详细的学习了 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]),
],
),
)
单子组件布局控件
1. Container
说明: 最常用的布局 Widget,可以包含一个子Widget,并可以设置 padding、margin、边框、背景等
规则: 如果没有子 Widget,Container 会尽可能大;如果有子 Widget,则根据子 Widget 和自身约束调整大小
注意: 在没有约束的情况下,Container会尽可能大(比如占满整个屏幕);在有约束的情况下(如父Widget是Column),Container如果没有子Widget,则会收缩到没有大小
推荐: 作为其他Widget的容器,用于装饰或设置间距
Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8.0),
),
child: const Text('Hello World'),
)
2. Padding
说明: 给子Widget设置内边距
规则: 只有一个子Widget
注意: 与Container的padding不同,Padding是一个独立的Widget
推荐: 当需要内边距但不需要Container的其他装饰时使用
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Text(
"Flutter Padding 背景颜色示例",
style: TextStyle(color: Colors.white),
),
),
)
3. Align
说明: 控制子Widget在父Widget中的对齐方式
规则: 通过alignment属性设置对齐方式(如Alignment.center)
注意: 如果父Widget没有约束,Align会尽可能大;如果有约束,则根据约束调整大小,并将子Widget对齐
推荐: 用于精确控制子Widget的位置
Container(
height: 200,
width: 200,
color: Colors.grey,
child: Align(
alignment: Alignment.bottomRight,
child: Container(width: 50, height: 50, color: Colors.red),
),
)
4. Center
说明: 用于将子组件在父组件中水平和垂直居中显示(是Align(alignment: Alignment.center)的简写),Center 组件继承自 Align,因此它本质上是一个简化版的 Align,专门用于居中布局
规则: 如果 Center 没有子组件且父容器提供无界约束(unbounded),则 Center 会尽可能缩小自身尺寸,如果父容器提供有界约束(bounded),则 Center 会扩展到父容器的大小。有子组件时,Center 的尺寸会根据子组件的大小和父容器的约束动态调整。
如果设置了 widthFactor 或 heightFactor,子组件的尺寸会按比例缩放(例如 widthFactor: 2 会使子组件宽度翻倍)注意: widthFactor 和 heightFactor用于调整子组件的宽度和高度比例,避免嵌套过多层级,如果父容器限制了尺寸(如
SizedBox
),需确保Center
的子组件尺寸在约束范围内推荐: 快速居中单个组件,与复杂布局结合,响应式布局
Center(
child: Container(
width: 200,
height: 100,
color: Colors.blue,
child: const Text(
"居中内容",
style: TextStyle(color: Colors.white),
),
),
)
5. AspectRatio
说明: 强制子Widget保持指定的宽高比
规则: 需要设置aspectRatio属性(宽/高)
注意: 父Widget必须提供约束,否则无法计算
推荐: 用于需要固定宽高比的场景,如播放器
AspectRatio(
aspectRatio: 16 / 9,
child: Container(color: Colors.blue),
)
6. ConstrainedBox
说明: 对子Widget施加额外的约束(BoxConstraints)
规则: 可以设置最小、最大宽高
注意: 如果子Widget本身有约束,可能会冲突,此时以更紧的约束为准
推荐: 用于需要限制子Widget大小的情况
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 100,
maxWidth: 200,
minHeight: 50,
maxHeight: 100,
),
child: Container(color: Colors.red),
)
7. UnconstrainedBox
说明: 解除父容器的约束,允许子控件按照自身大小绘制,然后根据实际大小再约束
规则: 子组件,其尺寸不会受到UnconstrainedBox的约束,若子组件尺寸超过父容器区域,Debug模式下会显示黄色溢出警告(Release模式会裁剪),若父级有多个约束组件(如嵌套的ConstrainedBox),UnconstrainedBox只能突破直接父级的约束,无法突破更高层约束
注意: constrainedAxis:null(默认):完全解除约束 / Axis.horizontal :仅水平方向解除约束,垂直方向仍受父级限制 / Axis.vertical :仅垂直方向解除约束,水平方向受限制,过度使用可能导致布局计算复杂化,建议仅在必要时使用
推荐: 适用于需要突破父容器约束的场景,例如在ListView或AppBar中保持子组件原始尺寸
ListView(
children: [
// 原始Container被拉伸至屏幕宽度
Container(width: 200, height: 100, color: Colors.red),
// 使用UnconstrainedBox保持原始尺寸
UnconstrainedBox(
child: Container(width: 200, height: 100, color: Colors.blue),
),
],
)
8. SizedBox
说明: 可以指定固定大小的盒子,也可以用于设置间隔
规则: 如果指定了width和height,则强制子Widget使用该大小(如果子Widget有约束,则可能会强制调整)
- SizedBox.shrink() 零尺寸占位符
- SizedBox.expand() 扩展填充占位符
- SizedBox.fromSize() 指定尺寸占位符
注意: 当没有子Widget时,SizedBox会占据指定大小的空间;有子Widget时,则强制子Widget大小为指定大小
推荐: 用于固定尺寸的盒子或间隔(如SizedBox(width: 10))
SizedBox(
width: 100,
height: 100,
child: Container(color: Colors.green),
)
9. FractionallySizedBox
说明: 子Widget的大小相对于父Widget的百分比
规则: 需要设置widthFactor和heightFactor(0.0到1.0)
注意: 父Widget必须提供约束,否则无法计算百分比
推荐: 用于需要相对父容器百分比大小的场景
Container(
width: 200,
height: 200,
color: Colors.blue,
child: FractionallySizedBox(
widthFactor: 0.5,
heightFactor: 0.5,
child: Container(color: Colors.red),
),
)
10. Transform
说明: 对子Widget进行矩阵变换(平移、旋转、缩放等)
规则: 变换不影响布局,变换是在布局之后进行的,所以可能会超出父Widget区域
注意: 变换后可能会超出父Widget范围,导致被裁剪(可以使用Clip.none避免)
推荐: 用于需要变换的场景,如旋转一个图标
Transform.rotate(
angle: 3.14 / 4, // 45度
child: Container(width: 100, height: 100, color: Colors.blue),
)
11. Baseline
说明: 根据子Widget的基线对齐(常用于文本), Baseline 的对齐方式有两种类型:
alphabetic: 对齐字母底部基线(适用于英文等拉丁文字)
ideographic: 对齐表意文字基线(适用于中文、日文等)
规则: 需要设置baseline和baselineType(如TextBaseline.alphabetic)
注意: 需要子Widget有基线(如Text),否则无效
推荐: 尤其适用于文字排版场景,处理混合文字(如中英文)时,通过baselineType区分基线类型,避免排版错乱
const Row(
children: [
Baseline(
baseline: 50.0,
baselineType: TextBaseline.alphabetic,
child: Text('Hello', style: TextStyle(fontSize: 20)),
),
Baseline(
baseline: 50.0,
baselineType: TextBaseline.alphabetic,
child: Text('World', style: TextStyle(fontSize: 30)),
),
],
)
12. FittedBox
说明: 对子Widget进行缩放和位置调整,以使其适应可用空间
规则: 通过fit属性设置适应方式(如BoxFit.contain)
注意: 如果子Widget没有约束,可能会出现问题
推荐: 用于需要缩放的场景,如保持图片比例并适应容器
Container(
width: 200,
height: 100,
color: Colors.amber,
child: FittedBox(
fit: BoxFit.contain,
child: Image.network(
'https://upload-images.jianshu.io/upload_images/1976231-cb638ee25dbc7368.png'),
),
)
13. OverflowBox
说明: 允许子Widget超出父Widget的约束,从而在父容器之外显示
规则: 设置自己的约束,子Widget可以突破父Widget的约束
注意: 可能导致布局溢出(无警告),使用时需注意
推荐: 在需要突破约束时使用,但需谨慎
Container(
color: Colors.green,
width: 200.0,
height: 200.0,
padding: const EdgeInsets.all(50.0),
child: OverflowBox(
alignment: Alignment.topLeft,
maxWidth: 400.0,
maxHeight: 400.0,
child: Container(
color: Colors.blueGrey,
width: 300.0,
height: 300.0,
),
),
)
14. LimitedBox
说明: 一个用于限制子组件最大尺寸的布局组件,当父Widget没有约束时,限制子Widget的最大宽高
规则: 在无约束时生效(如在ListView中,ListView沿着主轴方向有约束,但交叉轴方向无约束,此时在交叉轴方向使用LimitedBox可以限制最大宽高)
注意: 在父Widget有约束时无效
推荐: 在ListView中限制交叉轴方向的大小
Row(
children: [
Container(
color: Colors.grey,
width: 100.0,
),
LimitedBox(
maxWidth: 150.0,
maxHeight: 150.0,
child: Container(
color: Colors.lightGreen,
width: 250.0,
height: 250.0,
),
),
],
)
15. IntrinsicWidth/IntrinsicHeight
说明: 调整子Widget到其内部内容的高度或宽度(有性能问题,慎用)
规则: 迫使子Widget计算其内部内容的最大高度或宽度,并调整其他子Widget到相同高度或宽度
注意: 性能差,因为需要遍历子Widget两次(一次测量,一次布局)
推荐: 尽量避免使用,寻找其他布局方式替代。如果必须使用,注意性能影响
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IntrinsicWidth(
stepWidth: 100,
child: Container(
color: Colors.blue,
child: Center(
child: Container(
color: Colors.red,
width: 50,
height: 50,
),
),
),
),
],
)
16. CustomSingleChildLayout/CustomMultiChildLayout
说明: 使用自定义的布局代理进行布局,可以实现复杂的布局效果,CustomSingleChildLayout 是一个用于控制单个子组件布局的组件,CustomMultiChildLayout 是一个用于控制多个子组件布局的组件
规则: 需要自定义LayoutDelegate,SingleChildLayoutDelegate 来实现布局逻辑。该组件适用于需要对单个子组件进行复杂或非常规布局的场景,例如需要精确控制子组件的位置或尺寸时,MultiChildLayoutDelegate 来实现布局逻辑,适用于需要对多个子组件进行复杂布局的场景
注意: 相对复杂,需要自己实现布局逻辑
推荐: 当其他布局Widget无法满足需求时使用,自定义的网格布局、层叠布局
CustomSingleChildLayout(
delegate: _MyDelegate(),
child: Container(color: Colors.blue),
)
class _MyDelegate extends SingleChildLayoutDelegate {
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.loosen(); // 解除子组件约束
}
@override
Offset getPositionForChild(Size size, Size childSize) {
return Offset(size.width/2, 0); // 顶部居中
}
@override
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) {
// TODO: implement shouldRelayout
throw UnimplementedError();
}
}
Container(
width: 200.0,
height: 100.0,
color: Colors.yellow,
child: CustomMultiChildLayout(
delegate: TestLayoutDelegate(),
children: <Widget>[
LayoutId(
id: TestLayoutDelegate.title,
child: Container(
color: Colors.red,
child: const Text('Title'),
),
),
LayoutId(
id: TestLayoutDelegate.description,
child: Container(
color: Colors.green,
child: const Text('Description'),
),
),
],
),
)
class TestLayoutDelegate extends MultiChildLayoutDelegate {
static const String title = 'title';
static const String description = 'description';
@override
void performLayout(Size size) {
final BoxConstraints constraints = BoxConstraints(maxWidth: size.width);
final Size titleSize = layoutChild(title, constraints);
positionChild(title, const Offset(0.0, 0.0));
final double descriptionY = titleSize.height;
layoutChild(description, constraints);
positionChild(description, Offset(0.0, descriptionY));
}
@override
bool shouldRelayout(TestLayoutDelegate oldDelegate) => false;
}
17. Placeholder
说明: 主要用于在开发过程中快速构建页面骨架,加速页面流程的运行。它可以在布局中占位,帮助开发者快速预览页面结构。Placeholder 的大小默认适合其容器,若位于无界空间(unbounded space),则根据 fallbackWidth 和 fallbackHeight 调整大小。Placeholder 支持自定义颜色、线条宽度、fallbackHeight 和 fallbackWidth
规则: 可以在 Row 或 Column 中使用 Placeholder,但需要通过 fallbackHeight 和 fallbackWidth 限定大小,在无界空间中,可以使用 fallbackWidth 和 fallbackHeight 来限定占位符的大小
注意: 虽然 Placeholder 是一个简单的占位符,但在复杂的布局中过多使用可能会影响性能,因此应合理使用
推荐: 在页面开发初期,可以使用 Placeholder 来快速构建页面骨架,帮助开发者快速预览页面结构
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
height: 100,
child: Row(
children: [
Placeholder(
fallbackWidth: 100,
color: Colors.green,
strokeWidth: 10,
),
],
),
),
SizedBox(
width: 100,
child: Column(
children: [
Placeholder(
fallbackHeight: 100,
color: Colors.green,
strokeWidth: 10,
),
],
),
),
],
),
);
弹性布局辅助控件
1. Expanded
说明: 作为Row、Column或Flex的子Widget,可以扩展以填充主轴上的可用空间
规则: 必须位于Row、Column或Flex内。flex参数用于分配剩余空间的比例(默认为1)
注意: 在Row或Column中,如果子Widget的总主轴长度超过可用空间,使用Expanded可以避免溢出,因为它会压缩子Widget(强制子Widget适应分配的空间)
推荐: 在需要子Widget按比例分配空间时使用,比例分割布局(如 70%-30% 面板)
Row(
children: [
Expanded(flex: 2, child: Container(color: Colors.red)), // 占2/3空间
Expanded(flex: 1, child: Container(color: Colors.green)), // 占1/3空间
],
)
2. Flexible
说明: 与Expanded类似,但更灵活。Expanded是Flexible(fit: FlexFit.tight)的简写。Flexible的默认fit是FlexFit.loose,即子Widget不需要填满分配的空间
规则: 同样必须位于Row、Column或Flex内
注意: Flexible与Expanded的区别:Expanded会强制子Widget填满空间,而Flexible则允许子Widget使用更小的空间
推荐: 当需要子Widget有弹性但不需要强制填满时使用,自适应表格列
Row(
children: [
Flexible(
child: Container(height: 50, color: Colors.red),
),
Flexible(
child: Container(
height: 100,
color: Colors
.green), // 这个容器高度100,但Row高度由最高子Widget决定(100),红色容器高度50,但会被拉伸到100(因为Row交叉轴是垂直方向,默认是stretch)
),
],
)
注意:Row的交叉轴对齐默认是stretch,所以子Widget在垂直方向会被拉伸。如果不想被拉伸,可以设置crossAxisAlignment: CrossAxisAlignment.start。
3. Spacer
说明: 在弹性布局中占据剩余空间(相当于
Expanded
包裹一个空的SizedBox
),是一个用于在 Row、Column 或 Flex 布局中填充剩余空间的空白组件规则: 它会根据 flex 参数按比例占据主轴(水平或垂直方向)的可用空间,类似弹性布局中的“弹簧”,flex(默认值为1):指定空间分配比例。例如,两个 Spacer(flex: 2) 和 Spacer(flex: 1) 会按 2:1 分配剩余空间
注意: 必须作为 Row、Column 或 Flex 的直接子组件,空间不足时可能引发溢出错误(需确保父容器有足够空间)
推荐: 等间距按钮组,左右对齐元素,多比例空间分配
Column(
children: [
const Text(
"顶部标题",
style: TextStyle(
color: Colors.blue,
),
),
const Spacer(flex: 1),
const Row(
children: [
Text(
"左侧文本",
style: TextStyle(
color: Colors.black,
),
),
Spacer(flex: 2),
Text("中间文本",
style: TextStyle(
color: Colors.red,
)),
Spacer(flex: 1),
Icon(Icons.star),
],
),
const Spacer(flex: 3), // 下方大面积留白
ElevatedButton(
onPressed: () {},
child: const Text(
"确认",
style: TextStyle(
color: Colors.blue,
),
),
),
],
)
层叠布局辅助控件
1. Positioned
说明: 仅作为 Stack 的直接子组件使用,用于在层叠布局中精确定位子组件,类似 CSS 的绝对定位
规则: left、right、top、bottom(控制子组件相对于父 Stack 边缘的距离),width、height(可选,用于固定子组件尺寸),支持 Positioned(topLeft/bottomRight) 等快捷方式
注意: 若设置 left + right,则不能同时设置 width;同理适用于 top + bottom 与 height 的冲突,未设置尺寸时,需至少指定两个相对定位属性(如 left 和 right)以确定布局范围
推荐: 结合 MediaQuery 动态计算定位值,适配不同屏幕尺寸,使用 Align 简化对齐逻辑
Stack(
fit: StackFit.expand, // 填充父容器
children: [
Positioned(
left: 10,
right: 10,
height: 80,
child: Container(color: Colors.orange), // 水平居中且宽度自适应
),
Positioned(
top: 10,
bottom: 10,
width: 80,
child: Container(color: Colors.purple), // 垂直居中且高度自适应
),
],
)
2. PositionedDirectional
说明: 它用于在 Stack 中根据 textDirection 的方向来定位子部件。与 Positioned 不同,PositionedDirectional 接受的是一个偏移量和一个 textDirection 参数,从而决定子部件相对 Stack 的位置。这使得在布局中使用 PositionedDirectional 更加灵活和方便
规则: start:在 TextDirection.ltr (从左到右)时对应 left,在 TextDirection.rtl (从右到左)时对应 right。end:在 TextDirection.ltr 时对应 right,在 TextDirection.rtl 时对应 left。top 和 bottom 保持固定方向,不受文本方向影响
注意: 必须作为 Stack 的直接子组件,否则会抛出错误,支持 width 和 height 属性,但需注意与 start/end 和 top/bottom 的组合规则
推荐: 可通过 AnimatedPositionedDirectional 实现位置变化的动画效果,适配不同语言方向的布局(如阿拉伯语、希伯来语)
Directionality(
// 设置文本方向(ltr 或 rtl)
textDirection: TextDirection.rtl,
child: Stack(
children: [
PositionedDirectional(
start: 20.0, // 在 rtl 环境下对应 right=20
top: 50.0,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: const Center(child: Text('动态定位')),
),
),
// 其他子组件...
],
),
)
滚动布局相关控件
1. SingleChildScrollView
说明: 为单个子控件添加滚动功能,支持垂直或水平滚动,类似于 Android 中的 ScrollView 或 iOS 中的 UIScrollView
规则:
- scrollDirection: 滚动方向,默认为垂直方向(Axis.vertical)。
- reverse: 是否反转滚动方向,默认为 false。
- padding: 子组件的内边距。
- primary: 是否为主滚动视图,用于判断是否自动生成控制器。
- physics: 控制滚动行为,如滑动速度和边缘效应。
- controller: 用于控制滚动位置的对象。
- child: 要包含在 SingleChildScrollView 中的子组件。
- dragStartBehavior: 拖动开始行为,默认为 DragStartBehavior.start。
- clipBehavior: 裁剪行为,默认为 Clip.hardEdge。
- restorationId: 恢复标识。
- keyboardDismissBehavior: 键盘关闭行为,默认为
ScrollViewKeyboardDismissBehavior.manual。
注意: 不支持 Sliver 延迟加载,内容过多时性能较差,建议改用 ListView,适用于内容不会超过屏幕尺寸太多的情况,因为当内容超出屏幕尺寸较多时,性能会受到影响
推荐: 单个子组件的滚动(如表单、文章详情页),内容量较小,无需动态加载的场景
Scrollbar(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Center(
child: Column(
children: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
.split('')
.map((c) => Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
blurRadius: 4.0,
offset: const Offset(2.0, 2.0),
),
],
),
child: Text(
c,
style: const TextStyle(
color: Colors.white, fontSize: 24.0),
),
))
.toList(),
),
),
),
)
2. PageView
- 说明: 是一个分页滚动视图,用于实现可滑动的页面切换(水平/垂直)
- PageView():静态页面列表
- PageView.builder() :动态懒加载(大数据量优化)
- PageView.custom() :高度自定义子组件
- 规则:
- scrollDirection: 视图滚动方向,默认为水平方向(Axis.horizontal)。
- reverse: 是否反转方向,默认为 false。
- controller: 用于监听视图滚动情况的控制器。
- onPageChanged: 索引改变时的回调函数。
- physics: 滚动逻辑,可以设置为不滚动、滚动或滚动到边缘是否反弹等。
- pageSnapping: 是否启用页面对齐,默认为 true。
- allowImplicitScrolling: 是否允许隐式滚动,默认为 false。
注意: 使用 PageView.builder 避免一次性构建所有页面,搭配 AutomaticKeepAliveClientMixin 缓存页面状态,嵌套 ListView 时设置 physics: NeverScrollableScrollPhysics() 禁用父级滑动,避免垂直滑动冲突
推荐: 适用于引导页、轮播图、全屏滑动视图等场景
PageView(
children: <Widget>[
Container(
alignment: Alignment.center,
decoration: const BoxDecoration(color: Colors.green),
child: const Text("页面 0",
style: TextStyle(fontSize: 20, color: Colors.black)),
),
Container(
alignment: Alignment.center,
decoration: const BoxDecoration(color: Colors.red),
child: const Text("页面 1",
style: TextStyle(fontSize: 20, color: Colors.white)),
),
Container(
alignment: Alignment.center,
decoration: const BoxDecoration(color: Colors.blue),
child: const Text("页面 2",
style: TextStyle(fontSize: 20, color: Colors.white)),
),
],
)
3. Scrollbar
说明: 一个 Material 风格的滚动指示器,Flutter 的滚动视图(如 ListView、GridView 等)没有滚动条,但可以通过将滚动视图包裹在 Scrollbar 组件中来添加滚动条
规则:
- child: 必需属性,用于指定需要滚动的内容。
- controller: 可选属性,用于控制滚动行为。
- isAlwaysShown: 布尔值,表示是否始终显示滚动条。默认为 false,需要设置 controller 才能显示。
- thickness: 滚动条的宽度。
- radius: 滚动条的圆角半径。
- showTrackOnHover: 布尔值,表示当鼠标悬停在滚动条上时是否显示轨道。
- hoverThickness: 鼠标悬停时滚动条的宽度。
- notificationPredicate: 用于过滤滚动通知的谓词。
- interactive: 布尔值,表示滚动条是否可以交互。
- scrollbarOrientation: 滚动条的方向。
注意: Scrollbar 需要包裹可滚动组件(如 ListView),但不要求直接作为父级,只要在组件树中存在即可
推荐: 自定义滚动条样式 、显示滚动条
Scrollbar(
child: ListView.builder(
itemCount: 40,
itemBuilder: (context, index) {
return Card(
child: Container(
height: 45,
alignment: Alignment.center,
child: Text('$index'),
),
);
},
),
)
4. RefreshIndicator
说明: 实现 Material 风格下拉刷新功能的组件,通常包裹在可滚动的子组件(如 ListView、CustomScrollView)外层。当用户下拉超过一定距离时,会触发回调函数执行刷新操作,并显示加载动画
规则:
- child: 必需参数,通常为 ListView 或 CustomScrollView。
- displacement: 可选参数,从子组件的顶部或底部边缘到刷新指示符所在位置的距离。
- onRefresh: 必需参数,当用户将刷新指示器拖到足够远以表明他们希望应用刷新时调用的函数。
- color: 可选参数,进度指示器前景色。
- backgroundColor: 可选参数,进度指示器背景色。
- notificationPredicate: 可选参数,用于过滤滚动通知。
- semanticsLabel: 可选参数,语义标签。
- semanticsValue: 可选参数,语义值。
注意: 上拉加载:需结合
ScrollController
监听滚动位置,实现LoadMore
功能,onRefresh
必须返回Future
,且需在数据加载完成后完成该Future
,否则刷新动画不会停止推荐: 列表数据刷新
List<String> _items = List.generate(20, (index) => "Item $index");
Future<void> _refresh() async {
await Future.delayed(Duration(seconds: 2));
setState(() {
_items = List.generate(20, (index) => "Refreshed Item $index");
});
}
RefreshIndicator(
onRefresh: _refresh,
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_items[index]),
);
},
),
)
5. DraggableScrollableSheet
说明: 允许用户通过手势操作来拖动一个可滚动的区域。它常用于创建底部弹出式面板或可拖动的控件,类似于高德地图首页从底部滑动向上的效果。该组件继承自 StatefulWidget,通过调整滚动区域的大小来响应拖动手势,直到达到限制,然后开始滚动
规则:
1. initialChildSize: 设置初始高度占屏幕的比例,范围为 0 到 1,默认为 0.5。
2. minChildSize: 指定最小高度占屏幕的比例,默认为 0.25。
3. maxChildSize: 指定最大高度占屏幕的比例,默认为 1.0。
4. expand: 控制是否允许在内容小于屏幕高度时扩展以填充屏幕,默认为 true。
5. snap: 布尔值,用于控制滚动停止时是否自动捕捉到接近的最小或最大值,默认为 false。
6. builder: 构建函数,用于构建 DraggableScrollableSheet 的内容,接受两个参数:
BuildContext 和 ScrollController,返回一个 Widget,通常是 SingleChildScrollView 或 ListView。注意: 确保 maxChildSize >= initialChildSize >= minChildSize,否则会抛出异常
推荐: 底部弹出式面板评论区、详细信息展示
Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: 0,
right: 0,
child: Container(
color: Colors.grey[300],
child: Center(
child: Text('背景内容'),
),
),
),
DraggableScrollableSheet(
initialChildSize: 0.5,
minChildSize: 0.3,
maxChildSize: 1.0,
builder: (_, controller) {
return Container(
color: Theme.of(context).cardColor,
child: Scrollbar(
child: ListView.builder(
controller: controller,
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
),
),
);
},
),
],
)
6. NestedScrollView
说明: 用于协调多个独立滚动区域的容器组件。它通过 NestedScrollViewCoordinator 协调两个滚动控制器:外层滚动(Header)和内层滚动(Body)。外层滚动由 headerSliverBuilder 定义的 Sliver 组件组成(如 SliverAppBar),受 PrimaryScrollController 控制;内层滚动由 body 定义的普通滚动组件(如 ListView),拥有独立的 ScrollController
规则:
1.外层滚动优先: 当用户向下滑动时,外层滚动先消耗滚动事件,直到外层完全折叠后,内层滚动接管。
2.内层反向优先: 当内层滚动到达顶部且用户继续上滑时,外层滚动会展开注意: 确保内部可滚动组件(body)的 physics 属性设置正确,以避免滚动冲突
推荐: 折叠头部图片效果
NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: const Text('NestedScrollView Example'),
pinned: true,
floating: true,
forceElevated: innerBoxIsScrolled,
expandedHeight: 160.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
"https://miaobi-lite.bj.bcebos.com/miaobi/5mao/b%275aS05YOPYXBwXzE3MzM3NjcxODMuODQyNDgz%27/0.png",
fit: BoxFit.cover,
),
),
),
];
},
body: ListView.builder(
itemCount: 50,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('List Item $index'),
);
},
),
)
7. ScrollConfiguration
说明: 用于统一控制子组件树的滚动行为(如滚动条样式、拖拽反馈等),尤其适用于定制不同平台的滚动体验
规则: ScrollConfiguration 应该直接放置在列表组件的父组件下,以避免与其他列表组件发生冲突,若需要全局设置整个应用程序的所有列表默认样式,则可将 ScrollConfiguration 组件插入接近组件树根部的位置
注意: 由于 ScrollConfiguration 的子组件可能为 null,因此在使用时需要进行 null 检查,以避免潜在的错误
推荐: 自定义滚动条的样式,如颜色、厚度
ScrollConfiguration(
behavior: MyCustomBehavior(),
child: ListView.separated(
itemCount: 20,
separatorBuilder: (_, __) => Divider(),
itemBuilder: (_, index) {
return Container(
height: 56,
alignment: Alignment.center,
child: Text("这是第 ${index + 1} 项"),
);
},
),
)
class MyCustomBehavior extends ScrollBehavior {
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
// 自定义滚动条样式
return GlowingOverscrollIndicator(
axisDirection: axisDirection,
color: Colors.grey, // 设置滚动条颜色为灰色
child: child,
);
}
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
// 使用 ClampingScrollPhysics 防止过度滚动
return ClampingScrollPhysics();
}
}
Sliver 系列控件 (用于 CustomScrollView)
1. SliverList
说明: 创建线性可滚动列表的组件,它属于 Sliver 家族的一部分,SliverList 需要与 CustomScrollView 结合使用,以实现复杂的滚动效果
规则: 使用 SliverChildDelegate 来构建子组件。常用的实现是 SliverChildBuilderDelegate 和 SliverChildListDelegate。SliverChildListDelegate: (静态列表)适用于已知数量的子组件,一次性全部渲染。
SliverChildBuilderDelegate: (动态构建列表项)适用于未知数量的子组件,按需加载和销毁列表项,提升性能。注意: 必须通过代理生成子项,其核心特点是支持统一滚动、避免滑动冲突
推荐: 滚动时两个列表无缝衔接,无滑动冲突,实现吸顶 AppBar 和列表联动
CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 250,
flexibleSpace: FlexibleSpaceBar(
title: Text('SliverList 示例'),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item #$index'),
);
},
childCount: 20, // 列表项数目
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.blue[100 * (index % 9)],
child: Text("list item $index"),
);
},
childCount: 50,
),
),
],
)
2. SliverGrid
说明: 创建网格布局的 Sliver 组件,它非常适合与其他 Sliver 组件结合使用,以构建复杂的可滚动布局,
规则: SliverGrid 需要指定 gridDelegate 和 delegate 属性。gridDelegate 控制网格布局,而 delegate 用于构建每个网格单元的内容,SliverGrid 提供了三种主要的构造方法:
1.SliverGrid: 适用于动态显示内容(从数据库或 API 获取)。
2.SliverGrid.extent: 允许指定子项的最大跨轴范围。
3.SliverGrid.count: 用于指定跨轴上固定数量的子项
- delegate: 控制子项生成,常用:
- SliverChildListDelegate: 静态子项列表
- SliverChildBuilderDelegate: 动态懒加载子项
- gridDelegate: 控制网格布局,两种实现:
- SliverGridDelegateWithFixedCrossAxisCount: 固定列数
- SliverGridDelegateWithMaxCrossAxisExtent: 限制子项最大宽度
注意: 按需加载子项,性能优于一次性构建所有子项的 GridView,与普通的网格布局组件(如 GridView)不同,SliverGrid 不会提前渲染所有网格项,而是在滚动时动态渲染当前可见的部分,从而节省内存和渲染时间
推荐: 下拉刷新、嵌套滚动
CustomScrollView(
slivers: [
SliverAppBar(
// 可折叠标题栏
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
'https://miaobi-lite.bj.bcebos.com/miaobi/5mao/b%275aS05YOPYXBwXzE3MzM3NjcxODMuODQyNDgz%27/0.png',
fit: BoxFit.cover)),
),
SliverGrid(
// 网格区域
delegate: SliverChildBuilderDelegate((ctx, i) => Container(
color: [
Colors.red,
Colors.blue,
Colors.black,
Colors.yellow,
Colors.green
][i % 5],
)),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150, // 自适应宽度
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
),
],
)
3. SliverAppBar
说明: 创建可滚动应用栏的组件,它可以在滚动过程中动态变化,如展开、收缩或固定在顶部。SliverAppBar 通常与 CustomScrollView 搭配使用,可以实现复杂的滚动效果,类似于 Android 中的 CollapsingToolbarLayout
规则:
- expandedHeight: 指定 SliverAppBar 展开时的高度。
- pinned: 设置为 true 时,AppBar 将在滚动到顶部时固定在屏幕顶部。
- floating: 设置为 true 时,当向下滚动时,AppBar 会重新出现。
- snap: 配合 floating 属性使用,当设置为 true 时,SliverAppBar 会在滑动过程中自动“吸附”到顶部或消失。
- flexibleSpace: 用于定义可伸缩的空间,通常使用 FlexibleSpaceBar。
- backgroundColor: 设置 AppBar 的背景色。
- title: 设置 AppBar 的标题。
- leading: 设置 AppBar 前面显示的一个控件。
- actions: 设置 AppBar 的操作按钮。
- bottom: 设置 AppBar 的底部区域,通常用于添加 TabBar。
注意: pinned 和 floating 不可同时为 true,否则引发布局冲突,必须嵌套在 Sliver 容器,避免在 flexibleSpace 中使用复杂动画,可能导致滚动卡顿,如果需要实现滚动折叠效果,flexibleSpace 必须包含 FlexibleSpaceBar
推荐: 折叠式图片标题、快速展开的搜索栏、带悬浮 Tab 的分页布局
DefaultTabController(
length: 2,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('SliverAppBar Example'),
background: Image.network(
'https://img.win3000.com/m00/76/6a/3fb7a5729f51fedf4261cb02addbd133.jpg',
fit: BoxFit.cover,
),
),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.home), text: 'Home'),
Tab(icon: Icon(Icons.settings), text: 'Settings'),
],
),
),
];
},
body: TabBarView(
children: [
CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
height: 85,
alignment: Alignment.center,
color: Colors
.primaries[index % Colors.primaries.length],
child: Text(
'$index',
style: const TextStyle(
color: Colors.white, fontSize: 20.0),
),
);
},
childCount: 25,
),
),
],
),
CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
height: 85,
alignment: Alignment.center,
color: Colors
.primaries[index % Colors.primaries.length],
child: Text(
'$index',
style: const TextStyle(
color: Colors.white, fontSize: 20.0),
),
);
},
childCount: 25,
),
),
],
),
],
),
),
),
)
4. SliverPadding
说明: 专门用于在 滚动布局(如 CustomScrollView)中为子 Sliver 组件添加内边距的组件。它属于 Sliver 家族,适用于需要精细控制滚动区域间距的场景,例如在 SliverList、SliverGrid 等组件外部包裹边距
规则: 仅能作为 CustomScrollView 的 slivers 数组的直接子组件使用,不可单独用于普通布局。SliverPadding 的 sliver 属性必须是一个 Sliver 类型的组件,否则会导致运行时错误
核心参数:
padding(必填): 通过 EdgeInsets 设置边距(上、下、左、右或对称边距)。
sliver(必填): 接受任意 Sliver 组件(如 SliverList、SliverGrid)
注意: 普通 Padding 用于静态布局,而 SliverPadding 专为滚动视图优化,能正确计算滚动区域和视口边界,避免在 SliverPadding 内嵌套多层 Sliver 组件,推荐直接包裹目标组件,如需避开刘海屏或底部导航栏,优先使用 SliverSafeArea 而非 SliverPadding
推荐: 列表/网格的全局边距、复杂滚动布局的间隔控制
CustomScrollView(
slivers: [
const SliverAppBar(title: Text('SliverPadding 示例')),
SliverPadding(
padding: const EdgeInsets.all(20), // 全局边距
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text("列表项 $index"),
),
childCount: 20,
),
),
),
SliverPadding(
padding: const EdgeInsets.only(top: 30, bottom: 50), // 仅上下边距
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
color: Colors.blue,
child: Center(child: Text("网格 $index")),
),
childCount: 6,
),
),
),
],
)
5. SliverToBoxAdapter
说明: 将普通 盒模型布局组件(如 Container、Text 等)嵌入到 Sliver 滚动布局(如 CustomScrollView)中的适配器。它允许非 Sliver 组件(即非滚动专用组件)作为 Sliver 列表的一部分参与滚动,在 CustomScrollView 的 slivers 列表中混合使用常规 Widget 与 Sliver 组件(如 SliverList、SliverGrid)
规则: 只能用于 CustomScrollView.slivers 或嵌套的 Sliver 容器(如 SliverPadding),非 Sliver 组件(如 Container)必须指定高度或宽度,否则会因无限空间约束导致布局错误,若子组件包含 ListView 等自身可滚动的 Widget,需禁用其滚动行为(如 physics: NeverScrollableScrollPhysics())
注意: 尽量避免在 SliverToBoxAdapter 中嵌套过多的复杂布局,这可能会导致布局计算变慢,虽然 SliverToBoxAdapter 提供了便利性,但在大量使用时需要注意性能问题,尽量减少不必要的包裹,如果 child 的高度没有明确设置,可能会导致布局问题或渲染错误
推荐: 列表头部/尾部添加独立 Widget(如 Banner、标题、按钮)
CustomScrollView(
slivers: [
// 1. 顶部 Banner
SliverToBoxAdapter(
child: Container(
height: 200,
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue, Colors.green]),
),
child: const Center(
child: Text('欢迎页', style: TextStyle(fontSize: 24))),
),
),
// 2. 分隔标题
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('产品列表',
style: Theme.of(context).textTheme.headlineSmall),
),
),
// 3. 网格布局
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
),
delegate: SliverChildBuilderDelegate(
(context, index) =>
Card(child: Center(child: Text('产品 $index'))),
childCount: 6,
),
),
// 4. 底部按钮
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: ElevatedButton(
onPressed: () {},
child: const Text('加载更多'),
),
),
),
],
)
6. SliverFillRemaining
说明: 用于 填充滚动视图剩余空间 的 Sliver 布局组件。它会根据父容器的剩余高度或宽度自动调整自身大小,确保内容区域占满屏幕可视区域。适用于需要动态适配剩余空间的场景
规则: 作为 CustomScrollView 的 slivers 列表中的最后一个组件,用于填充剩余空间,允许组件自适应地填充剩余空间,无论剩余空间如何变化
关键参数
- child:必填,用于填充剩余空间的子组件。
- hasScrollBody:
- 默认 true:子组件可滚动(如 ListView),适用于内容超出剩余空间的情况。
- false:子组件不可滚动(如 Column),内容高度不超过剩余空间
- fillOverscroll:
- true:在过度滚动时继续填充(如 iOS 回弹效果)。
- false:仅填充未滚动区域(默认值)
注意: 当 hasScrollBody: true 时,子组件必须是可滚动的(如 ListView),否则会抛出布局错误,SliverFillRemaining 在滚动视图中动态填充剩余空间,而 SingleChildScrollView 会尝试包裹整个内容,可能导致布局溢出,在长列表底部使用 SliverFillRemaining 时,避免在 child 中嵌套复杂布局,防止重复构建
推荐: 表单底部固定按钮、分页加载占位、全屏内容适配
CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Flexible Space'),
),
),
SliverList(
delegate: SliverChildListDelegate(
[
Container(
height: 500,
color: Colors.amber,
alignment: Alignment.center,
child: const Text('List Item'),
),
],
),
),
SliverFillRemaining(
hasScrollBody: false,
child: Container(
color: Colors.blue,
alignment: Alignment.center,
child: const Text('Fill Remaining Space',
style: TextStyle(color: Colors.white)),
),
),
],
)
7. SliverPersistentHeader
说明: 创建可折叠/悬浮的头部的Sliver组件,在滚动视图中创建一个持久化的头部组件,该头部组件可以根据滚动位置自动调整大小、透明度或其他属性。可以固定在页面顶部或底部,也可以随着滚动而动态变化,从而实现更加灵活的布局效果
规则:
- delegate:必需属性,用于定义头部组件的构建逻辑。
- pinned:布尔值,默认为 false,当设置为 true 时,头部组件会在滚动到顶部时固定在视图中。
- floating:布尔值,默认为 false,当设置为 true 时,头部组件会在滚动到顶部时短暂显示,然后隐藏。
注意: pinned 和 floating 属性不能同时为 true,避免在build()方法中执行耗时操作,推荐使用const组件或StatelessWidget。若内容复杂,用AutoDispose混合Riverpod管理状态,不可直接嵌套ListView等滚动组件,需通过SliverToBoxAdapter,确保minExtent和maxExtent精确计算,避免小数精度误差。必须显式指定头部组件的高度范围(minExtent 和 maxExtent)。如果依赖 overlapsContent 参数构建子组件,则必须保证之前至少还有一个 SliverPersistentHeader 或 SliverAppBar
推荐: 电商分类导航、渐变收缩的Banner图
CustomScrollView(
slivers: <Widget>[
// 创建一个固定在页面顶部的 SliverPersistentHeader
SliverPersistentHeader(
pinned: true, // 固定在页面顶部
delegate: _MyHeaderDelegate(),
),
// 添加一个普通的 SliverList
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
leading: CircleAvatar(child: Text('$index')),
);
},
childCount: 50,
),
),
],
)
class _MyHeaderDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
// 获取状态栏高度
final double statusBarHeight = MediaQuery.of(context).padding.top;
// 构建头部组件
return Container(
padding: EdgeInsets.only(top: statusBarHeight),
color: Colors.blue,
alignment: Alignment.center,
child: const Text('Header'),
);
}
@override
double get maxExtent => 120; // 最大高度
@override
double get minExtent => 40; // 最小高度
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
false;
}
还有其他的一些Sliver布局控件
SliverFillViewport:填充视口
SliverAnimatedList:动画列表
SliverAnimatedOpacity:动画透明度
SliverIgnorePointer:忽略指针事件
SliverOpacity:透明度效果
SliverSafeArea:安全区域
SliverOverlapInjector:处理重叠
SliverOverlapAbsorber:吸收重叠
响应式布局控件
1. LayoutBuilder
说明: 用于根据父容器的约束对其子组件进行布局。它在布局过程中获取父组件传递的约束信息,从而动态构建布局。这使得开发者可以根据父容器的尺寸来调整子组件的布局,非常适合实现响应式布局
规则: 根据设备尺寸动态调整布局,在开发过程中获取和调试约束信息,便于排查问题,如果是 Sliver 布局,可以使用 SliverLayoutBuilder
- constraints.maxWidth/constraints.maxHeight :父容器要求的最大尺寸
- constraints.minWidth/minHeight :父容器要求的最小尺寸
注意: 如果子组件超出显示大小,可能会导致频繁的 rebuild。解决方案是将 constraints 保存为成员变量,避免每次 build 都通过 LayoutBuilder 获取 constraints,当父容器的约束变化(如屏幕旋转、父容器尺寸调整)时,builder 会重新调用,考虑布局的复杂度和动画效果,避免过重的计算和不必要的重绘
推荐: 要根据屏幕尺寸调整布局、获取约束信息,帮助调试布局问题
Center(
child: Container(
width: 300, // 父容器宽度
height: 200, // 父容器高度
color: Colors.blue,
child: LayoutBuilder(
builder: (context, constraints) {
// 根据父容器宽度动态切换布局
if (constraints.maxWidth > 200) {
return _buildWideLayout(constraints);
} else {
return _buildNarrowLayout(constraints);
}
},
),
),
)
// 宽布局:水平排列
Widget _buildWideLayout(BoxConstraints constraints) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
width: constraints.maxWidth * 0.3, // 宽度占父级30%
height: 50,
color: Colors.red,
),
Container(
width: constraints.maxWidth * 0.5, // 宽度占父级50%
height: 80,
color: Colors.green,
),
],
);
}
// 窄布局:垂直排列
Widget _buildNarrowLayout(BoxConstraints constraints) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: constraints.maxWidth * 0.8, // 宽度占父级80%
height: 40,
color: Colors.orange,
),
const SizedBox(height: 20),
Container(
width: constraints.maxWidth * 0.6, // 宽度占父级60%
height: 60,
color: Colors.purple,
),
],
);
}
2. OrientationBuilder
说明: 根据设备的方向(纵向或横向)动态构建布局。它提供了一个回调函数,当设备方向发生变化时,会调用该回调函数来重建布局。这使得开发者可以根据不同的屏幕方向提供不同的用户界面布局
规则: 依赖于父组件的尺寸来判断当前的方向,而不是直接依赖于设备的方向。因此,在某些情况下,可能需要结合 MediaQuery 来获取更准确的方向信息
注意: 由于 OrientationBuilder 依赖于父组件的尺寸,因此在某些情况下可能无法准确检测到设备的方向。建议在需要高精度方向检测时,结合 MediaQuery.of(context).orientation 使用
推荐: 横竖屏显示不同排列的网格、列表,在多设备适配方案中作为基础工具
OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? _buildVerticalLayout()
: _buildHorizontalLayout();
},
)
Widget _buildVerticalLayout() {
return ListView(
children: const <Widget>[
ListTile(title: Text('项目 1')),
ListTile(title: Text('项目 2')),
ListTile(title: Text('项目 3')),
],
);
}
Widget _buildHorizontalLayout() {
return GridView.count(
crossAxisCount: 3,
children: <Widget>[
Container(
color: Colors.red,
child: const Center(child: Text('项目 1')),
),
Container(
color: Colors.green,
child: const Center(child: Text('项目 2')),
),
Container(
color: Colors.blue,
child: const Center(child: Text('项目 3')),
),
],
);
}
3. MediaQuery
说明: 用于获取设备的屏幕尺寸、方向、像素密度等信息。它可以帮助开发者创建响应式布局,以适应不同设备和屏幕尺寸
规则:
1. 获取屏幕尺寸: 使用 MediaQuery.of(context).size 可以获取屏幕的宽度和高度。
2. 获取方向: 使用 MediaQuery.of(context).orientation 可以获取设备的方向(竖屏或横屏)。
3. 获取文本缩放因子: 使用MediaQuery.of(context).textScaleFactor 可以获取用户的文本缩放设置。
4. 获取系统亮度: 使用 MediaQuery.of(context).platformBrightness 可以获取系统的亮度模式(亮色或暗色)。
5. 获取系统遮挡区域: 使用 MediaQuery.of(context).padding 可以获取系统遮挡区域的信息,如状态栏和导航栏的高度。注意: 频繁使用 MediaQuery.of(context).size 可能会导致不必要的小部件重建,影响性能。建议仅在需要的地方使用 MediaQuery,键盘弹出时,MediaQuery 的 viewInsets 属性会变化,开发者需要处理这种情况以避免布局问题
推荐: 根据用户的文本缩放设置调整字体大小
var screenSize = MediaQuery.of(context).size;
var textScaleFactor = MediaQuery.of(context).textScaleFactor;
var orientation = MediaQuery.of(context).orientation;
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Screen Size: ${screenSize.width} x ${screenSize.height}',
style: TextStyle(fontSize: 24),
),
SizedBox(height: 10),
Text(
'Text Scale Factor: $textScaleFactor',
style: TextStyle(fontSize: 24),
),
SizedBox(height: 10),
Text(
'Orientation: $orientation',
style: TextStyle(fontSize: 24),
),
SizedBox(height: 20),
if (orientation == Orientation.portrait)
FilledButton(
onPressed: () {},
child: Text('Button for Portrait Mode'),
)
else
FilledButton(
onPressed: () {},
child: Text('Button for Landscape Mode'),
),
SizedBox(height: 20),
FilledButton(
onPressed: () {},
child: Text(
'Custom Font Size Button',
style: TextStyle(fontSize: 16 * textScaleFactor),
),
),
],
)
4. AspectRatio
说明: 根据设备的方向(纵向或横向)动态构建不同的 UI 布局。当设备的方向发生变化时,OrientationBuilder 会重新构建其子组件树,以适应新的方向。这使得开发者能够轻松地实现横竖屏适配,它通过回调函数返回当前屏幕方向,无需依赖全局 MediaQuery
规则: 根据设备的方向返回不同的布局组件,builder: 一个回调函数,接收两个参数:BuildContext 和 Orientation。Orientation 是一个枚举类型,包含两个值:Orientation.portrait 和 Orientation.landscape
OrientationBuilder( builder: (BuildContext context, Orientation orientation) { // 根据 orientation 的值返回不同的布局 return YourWidget(); }, )
注意: 避免在 builder 函数中执行耗时操作(如网络请求),因其在屏幕旋转时可能频繁触发,直接通过回调提供方向,更适合布局构建阶段,仅监听方向变化,若需响应尺寸变化(如分屏模式),应结合 LayoutBuilder
推荐: 视频播放器、表单输入
AspectRatio(
aspectRatio: 16 / 9,
child: Container(color: Colors.blue),
)
平台适配布局控件
1. SafeArea
说明: 用于规避系统 UI(如状态栏、刘海屏、底部导航栏)的遮挡。它会根据设备屏幕的“安全区域”自动添加内边距(padding),确保内容不被系统控件覆盖,使 UI 动态且适应各种设备。在设计组件布局时,我们考虑了不同设备及其屏幕占用的约束,如状态栏、刘海屏、导航栏等。然而,新设备不断推出,设计各异,有时你的应用可能会覆盖这些占用的约束。因此,为了使 UI 适应性强且无错误,我们使用 SafeArea 组件。SafeArea 实际上是一个填充组件,根据设备运行时的情况,为你的应用添加必要的填充。如果应用的组件覆盖了系统特征,如刘海屏、状态栏、相机孔等,SafeArea 会根据需要在周围添加填充。SafeArea 内部使用 MediaQuery 检查屏幕尺寸,并在必要时包含额外填充。构造函数允许你决定是否在特定方向上避免侵入,通过布尔值 true 或 false 来实现
规则: 在需要避免系统侵入的区域包裹 SafeArea 组件,通过 left、top、right、bottom 属性控制是否在相应方向上避免系统侵入;通过 minimum 属性设置最小填充;通过 maintainBottomViewPadding 属性决定是否保持底部填充不变,将需要保护的子组件放在 SafeArea 的 child 属性中
注意: 当软键盘弹出时,底部填充可能会消失。可以通过设置 maintainBottomViewPadding 为 true 来保持底部填充不变,在页面骨架中直接应用,避免全局遮挡,在沉浸式全屏(如视频播放页)中需关闭 SafeArea,否则顶部留白,若父容器已设置 padding(如 ListView),SafeArea 可能造成双重边距。此时需手动调整,强制取消安全边距(如 padding: EdgeInsets.zero )可能导致内容被遮挡
推荐: 顶部状态栏区域、底部导航栏区域、刘海屏/挖孔屏设备、弹窗/浮层内容
SafeArea(
// 2. 关闭底部安全区域(自定义底部栏时使用)
bottom: false,
child: Column(
children: [
// 3. 顶部标题栏(自动避开状态栏)
const AppHeader(),
// 4. 内容区域(使用Expanded填充剩余空间)
Expanded(
child: ListView(
children: List.generate(
20, (i) => ListTile(title: Text("Item $i"))),
),
),
// 5. 自定义底部导航栏(需手动避开系统栏)
const CustomBottomBar(),
],
),
)
// 自定义顶部组件
class AppHeader extends StatelessWidget {
const AppHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
padding: const EdgeInsets.all(16),
child: const Text("SafeArea Demo", style: TextStyle(color: Colors.white)),
);
}
}
// 自定义底部组件(需单独处理安全边距)
class CustomBottomBar extends StatelessWidget {
const CustomBottomBar({super.key});
@override
Widget build(BuildContext context) {
// 6. 为底部栏单独添加SafeArea
return SafeArea(
top: false, // 关闭顶部边距
minimum: const EdgeInsets.only(bottom: 10), // 追加额外外边距
child: Container(
height: 50,
color: Colors.green,
child: const Center(child: Text("Bottom Bar")),
),
);
}
}
2. PlatformView
说明: 允许在 Flutter 应用中嵌入原生的 UI 组件,如 Android 的 View 或 iOS 的 UIView
规则:
- 首先需要在原生代码中创建一个自定义的 PlatformView。
- 创建一个 PlatformViewFactory 来创建 PlatformView 实例,并将其与 Flutter 应用关联
- 通过 PlatformView Widget 将原生视图嵌入到 Flutter 应用中
- 通过 MethodChannel 实现 Dart 与原生代码的双向通信
注意: 由于 PlatformView 是原生视图,频繁的交互可能会影响性能,因此应尽量减少不必要的原生视图嵌入,确保正确管理原生视图的生命周期,避免内存泄漏,不同平台的原生视图实现方式不同,需要分别处理 Android 和 iOS 的实现,Dart 的 viewType(native_text_view)需与原生注册的 ID 匹配
推荐: 嵌入地图、视频播放、相机、文件选择器、WebView、条形码扫描
// dart
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Flutter 文本显示原生视图传回的时间
Text('Flutter 显示: $_currentTime',
style: const TextStyle(fontSize: 20)),
const SizedBox(height: 30),
// 原生视图容器
Container(
width: 300,
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.blue, width: 2),
borderRadius: BorderRadius.circular(10),
),
child: Platform.isAndroid
? const AndroidView(
viewType: _viewType,
creationParams: {'textColor': '#FF0000'}, // 红色文本
creationParamsCodec: StandardMessageCodec(),
)
: const UiKitView(
viewType: _viewType,
creationParams: {'textSize': 24.0}, // iOS 文本大小
creationParamsCodec: StandardMessageCodec(),
),
),
],
)
// swift
// TimeView.swift
class TimeView: NSObject, FlutterPlatformView {
private var label: UILabel
private var methodChannel: FlutterMethodChannel?
init(frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, messenger: FlutterBinaryMessenger) {
label = UILabel(frame: frame)
label.textAlignment = .center
label.text = "iOS原生时间视图"
super.init()
// 解析Flutter传递的参数
if let params = args as? [String: Any],
let textSize = params["textSize"] as? Double {
label.font = UIFont.systemFont(ofSize: CGFloat(textSize))
} else {
label.font = UIFont.systemFont(ofSize: 20)
}
label.textColor = .blue
// 创建方法通道
methodChannel = FlutterMethodChannel(
name: "com.example/time_channel",
binaryMessenger: messenger
)
methodChannel?.setMethodCallHandler(handleMethodCall)
}
func view() -> UIView {
return label
}
func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getCurrentTime":
// 返回当前时间给Flutter
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
result(formatter.string(from: Date()))
default:
result(FlutterMethodNotImplemented)
}
}
}
// TimeViewFactory.swift
class TimeViewFactory: NSObject, FlutterPlatformViewFactory {
private var messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
return TimeView(
frame: frame,
viewIdentifier: viewId,
arguments: args,
messenger: messenger
)
}
}
// AppDelegate.swift (添加部分)
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
// 注册PlatformView
let factory = TimeViewFactory(messenger: controller.binaryMessenger)
registrar(forPlugin: "TimeViewPlugin")?.register(
factory,
withId: "com.example/NativeTimeView"
)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
// ios 配置
<!-- ios/Runner/Info.plist -->
<key>io.flutter.embedded_views_preview</key>
<true/>
// android
// TimeView.kt
package com.example.platformviewdemo;
import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.platform.PlatformView;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
class TimeView implements PlatformView, MethodChannel.MethodCallHandler {
private final TextView textView;
private final MethodChannel methodChannel;
TimeView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
textView = new TextView(context);
// 解析Flutter传递的参数
if (params != null && params.containsKey("textColor")) {
String color = (String) params.get("textColor");
textView.setTextColor(Color.parseColor(color));
} else {
textView.setTextColor(Color.BLUE);
}
textView.setTextSize(20);
textView.setText("Android原生时间视图");
// 创建方法通道
methodChannel = new MethodChannel(messenger, "com.example/time_channel");
methodChannel.setMethodCallHandler(this);
}
@Override
public View getView() {
return textView;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
if (call.method.equals("getCurrentTime")) {
// 返回当前时间给Flutter
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
result.success(sdf.format(new Date()));
} else {
result.notImplemented();
}
}
@Override
public void dispose() {
methodChannel.setMethodCallHandler(null);
}
}
// TimeViewFactory.kt
package com.example.platformviewdemo;
import android.content.Context;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.util.Map;
public class TimeViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
public TimeViewFactory(BinaryMessenger messenger) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
}
@Override
public PlatformView create(Context context, int viewId, Object args) {
Map<String, Object> params = (Map<String, Object>) args;
return new TimeView(context, messenger, viewId, params);
}
}
// android/app/src/main/java/com/example/platformviewdemo/MainActivity.kt
package com.example.platformviewdemo;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.example/time_channel";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
// 注册PlatformView
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory(
"com.example/NativeTimeView",
new TimeViewFactory(flutterEngine.getDartExecutor())
);
}
}
AndroidView: 嵌入Android原生视图
UiKitView: 嵌入iOS原生视图
平台适配:
Platform.isIOS ? CupertinoButton(onPressed: () {}, child: Text('iOS')) : ElevatedButton(onPressed: () {}, child: Text('Android'));
特殊布局控件
1. Offstage
说明: 用于控制子组件是否参与布局和渲染的一个小部件,其核心功能是通过offstage属性切换子组件的可见状态
规则: 当 offstage 属性设置为 true 时,子组件会被隐藏,并且不会参与布局或渲染,也不会占用任何空间,当 offstage 为 false 时,子组件正常显示
注意: 如果子组件有动画,应该手动停止动画,因为 Offstage 不会停止动画,Offstage 不会从渲染树中移除子组件,只是不绘制和不响应点击事件,因此在需要频繁切换显示状态的场景中,Offstage 是一个高效的选择,隐藏状态的子组件不占用空间,可能导致父布局重新计算尺寸(如Column中的子组件隐藏后,其他子组件会向上移动)。需注意布局的稳定性,避免频繁切换导致界面抖动
推荐: 实现平滑的显示/隐藏动画(如淡入淡出、滑动入场)
bool _isHidden = false; // 控制文本是否隐藏
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 使用 Offstage 控制文本显示
Offstage(
offstage: _isHidden,
child: const Text(
'Hello, Offstage!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 20),
// 切换按钮
ElevatedButton(
onPressed: () {
setState(() => _isHidden = !_isHidden);
},
child: Text(_isHidden ? '显示文本' : '隐藏文本'),
),
],
)
2. Visibility
说明: 用于控制子组件的可见性。当设置为“可见”时,子组件将显示;设置为“隐藏”时,子组件将被隐藏。该组件还提供了多个可选属性来控制子组件的状态和行为,例如是否维持状态、动画、大小和交互性
规则:
- child: 必填属性,指定要控制可见性的子组件。
- visible: 可选属性,布尔值,控制子组件的显示或隐藏,默认为true。
- replacement: 可选属性,当子组件不可见时显示的替代小部件。
- maintainState: 可选属性,布尔值,控制子组件状态是否保持不变,默认为false。
- maintainAnimation: 可选属性,布尔值,控制子组件动画是否保持不变,默认为false。
- maintainSize: 可选属性,布尔值,控制子组件空间是否保持不变,默认为false。
- maintainInteractivity: 可选属性,布尔值,控制子组件在不可见时是否仍可交互,默认为false。
- maintainSemantics: 可选属性,布尔值,控制子组件语义是否保持不变,默认为false。
注意: 即使子组件被隐藏,其状态也会被维持,这在需要保留用户输入或状态的情况下非常有用,对高频切换显隐的组件(如列表项),避免启用 maintainState,防止内存泄漏,
推荐: 表单动态字段、Tab 切换内容、配合 AnimatedSwitcher 实现渐变/缩放效果
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Visibility(
visible: _isShown,
child: Container(
width: 100,
height: 100,
color: Colors.amber,
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _toggleVisibility,
child: Text(_isShown ? '隐藏盒子' : '显示盒子'),
),
],
),
)
3. IgnorePointer
说明: 用于忽略其子组件的指针事件(如点击、拖动等)。这意味着包裹在 IgnorePointer 中的子组件将不会响应用户的交互操作,但仍然会显示在界面上。IgnorePointer 与 AbsorbPointer 类似,但 IgnorePointer 不会终止指针事件,而是让这些事件传递到其下方的组件
规则: ignoring: 布尔值,决定是否忽略指针事件。默认为 true,ignoringSemantics: 布尔值,决定是否忽略语义信息。默认为 null,即不忽略,可以嵌套在任何布局组件(如 Stack、Column)中。
若多个 IgnorePointer 嵌套,外层 ignoring 为 true 时,内层设置无效,事件穿透:当 ignoring 为 true 时,子组件的点击事件会穿透到父级组件。适用于需要隐藏交互但保留显示效果的场景(如背景图、装饰元素)注意: 避免在复杂嵌套结构中滥用,可能导致不必要的渲染开销。优先结合 Stack 和 Positioned 实现局部交互控制,需动态控制 ignoring 时,建议通过 StatefulWidget 管理状态,配合 setState 更新,与 GestureDetector 结合使用时,需注意事件优先级。例如,外层 IgnorePointer 为 true 时,内层 GestureDetector 无法触发事件
推荐: 背景图片、水印等无需交互的组件、表单提交时禁用按钮点击,防止重复提交、在 Stack 中叠加多个组件时,控制特定层的交互优先级
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 使用 IgnorePointer 包裹按钮
IgnorePointer(
ignoring: _isIgnoring,
child: ElevatedButton(
onPressed: () {
print('按钮被点击');
},
child: const Text('可点击按钮'),
),
),
const SizedBox(height: 20),
// 切换开关
Switch(
value: _isIgnoring,
onChanged: (value) {
setState(() {
_isIgnoring = value;
});
},
activeTrackColor: Colors.blueGrey,
activeColor: Colors.blue,
),
const Text('开关开启时按钮不可点击'),
],
)
4. AbsorbPointer
说明: 用于阻止子组件接收指针事件的布局组件,其核心功能是通过终止命中测试(HitTest)来禁用子树的交互能力。它不会影响布局和绘制,仅控制事件传递
规则: absorbing:布尔值,默认为 true。当设置为 true 时,子组件将无法接收用户输入事件,必须直接包裹需要禁用的子组件,且不影响父级或外部组件的交互
注意: 虽然 AbsorbPointer 的使用不会显著影响性能,但在复杂的布局中应谨慎使用,避免不必要的嵌套,需配合状态管理(如 setState)动态控制 absorbing 属性,实现交互开关,即使 AbsorbPointer 设置为吸收模式,它仍然会将点击事件传递给其父组件。这意味着父组件的 GestureDetector 仍然能够捕获点击事件
推荐: 临时禁用交互、批量禁用组件
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 外层可点击区域
GestureDetector(
// ignore: avoid_print
onTap: () => print('外层点击'),
child: Container(
color: Colors.grey,
padding: const EdgeInsets.all(16),
child: AbsorbPointer(
absorbing: true, // 禁用子组件交互
child: Row(
children: [
ElevatedButton(
// ignore: avoid_print
onPressed: () => print('按钮点击'),
child: const Text('禁用按钮'),
),
const SizedBox(width: 16),
const Text('不可点击区域'),
],
),
),
),
),
const SizedBox(height: 20),
// 对比示例(交互正常)
GestureDetector(
// ignore: avoid_print
onTap: () => print('外层点击'),
child: Container(
color: Colors.grey,
padding: const EdgeInsets.all(16),
child: AbsorbPointer(
absorbing: false, // 启用子组件交互
child: ElevatedButton(
// ignore: avoid_print
onPressed: () => print('正常按钮'),
child: const Text('可点击按钮'),
),
),
),
),
],
),
)
Overlay 覆盖层
Opacity 透明度效果
AnimatedOpacity 动画透明度
ClipRRect 圆角裁剪
ClipOval 椭圆裁剪
ClipPath 路径裁剪
ClipRect 矩形裁剪
BackdropFilter 背景滤镜
DecoratedBox 装饰盒子
RotatedBox 旋转盒子
平台特定布局控件
CupertinoPageScaffold iOS风格页面脚手架
CupertinoTabScaffold iOS风格标签栏脚手架
CupertinoNavigationBar iOS风格导航栏
CupertinoTabBar iOS风格标签栏
CupertinoActionSheet iOS风格操作表
CupertinoAlertDialog iOS风格警告对话框
CupertinoContextMenu iOS风格上下文菜单
MaterialApp Material Design 应用容器
Scaffold Material Design 页面脚手架
AppBar Material Design 应用栏
BottomAppBar Material Design 底部应用栏
TabBar Material Design 标签栏
Drawer Material Design 抽屉
SnackBar. Material Design 底部消息条
BottomSheet Material Design 底部表单
Material Material Design 表面
Card Material Design 卡片
Chip Material Design 标签
Divider Material Design 分割线:水平分割线组件,常见于列表项、板块划分等场景
ListTile Material Design 列表项:主要用于创建列表项(List Item),通常用于 ListView、Card 或 Drawer 等布局中
总结
虽然 Flutter 提供了超过 100+ 个布局相关控件,以上列举覆盖了几乎所有官方核心布局控件,实际开发中最常用的约只有 15 个。掌握 Row/Column/Stack/Expanded/ListView这五个核心组件即可解决绝大多数布局需求。
基础布局三巨头: Row/Column(线性布局)、Stack(层叠布局)、ListView/GridView(滚动布局)满足 80% 场景
弹性布局必用: Expanded 和 Flexible 是自适应布局的核心
性能关键: 长列表必用 ListView.builder,避免滥用 IntrinsicWidth/Height,复杂动画使用 CustomMultiChildLayout
响应式最佳实践: LayoutBuilder + MediaQuery + 约束条件判断