Appearance
Flutter 组件
Flutter 一切皆 Widget——从最基础的
Container、Text、Image,到Row/Column/Stack布局,再到AppBar/Drawer/TabBar等成品组件,都是通过嵌套 Widget 树声明式地拼出界面。本篇按「基础展示 → 布局排列 → 导航与骨架 → 交互反馈」四条主线,系统梳理常用组件的属性、用法与避坑要点,配代码可直接复用。
导航目录
基础展示篇(内容承载)
Container容器组件Text组件属性详解Image组件属性详解- Flutter 实现圆形图片的 5 种方法
Icon图标组件(Material Icons)- 集成阿里巴巴图标库自定义字体图标指南
布局排列篇(组件如何摆放)
卡片与头像篇(成品展示组件)
导航与骨架篇(页面结构)
BottomNavigationBarDrawer组件全面指南- UserAccountsDrawerHeader 组件
AppBarTabBarTabBarViewAppBar+TabBar+TabBarView三者协同使用PreferredSize组件
交互反馈篇(按钮与对话框)
- 按钮组件(
ElevatedButton/TextButton/OutlinedButton/IconButton等) FloatingActionButton- GetX Dialog(对话框)
Container 容器组件
一切皆 Widget:从 Container 理解 Flutter 组合思想
Flutter 界面由 Widget 层层嵌套而成,Container 是最常用的「万能盒子」——它把对齐、装饰、间距、尺寸、变换等能力集于一身,本质是多个单一职责 Widget 的组合封装。
text
Container(万能盒子)
├── alignment 对齐子元素
├── decoration 背景/边框/圆角/阴影/渐变
├── margin 外间距
├── padding 内边距
├── transform 矩阵变换
├── width/height 尺寸
└── child 唯一子 Widget只需一个子 Widget(child)——要放多个请配合 Column/Row/Stack。
| 属性名 | 类型 | 说明 | 示例 |
|---|---|---|---|
| alignment | AlignmentGeometry | 子元素对齐方式 | topCenter/topLeft/topRightcenter/centerLeft/centerRightbottomCenter/bottomLeft/bottomRight |
| decoration | BoxDecoration | 容器装饰样式(背景/边框/阴影等) | dart<br>BoxDecoration(<br> color: Colors.blue,<br> border: Border.all(<br> color: Colors.red,<br> width: 2.0<br> ),<br> borderRadius: BorderRadius.circular(8),<br> boxShadow: [BoxShadow(<br> color: Colors.black54,<br> offset: Offset(2.0, 2.0),<br> blurRadius: 10.0<br> )],<br> gradient: LinearGradient( // 或 RadialGradient<br> colors: [Colors.red, Colors.orange]<br> )<br>)<br> |
| margin | EdgeInsetsGeometry | 容器外间距(与外部组件的距离) | EdgeInsets.all(20.0)EdgeInsets.only(top: 10) |
| padding | EdgeInsetsGeometry | 容器内边距(边缘与子元素的间距) | EdgeInsets.all(10.0)EdgeInsets.symmetric(horizontal: 15) |
| transform | Matrix4 | 矩阵变换(旋转/缩放/平移等) | Matrix4.rotationZ(0.2)Matrix4.translationValues(10, 0, 0) |
| height | double | 容器高度 | height: 100.0 |
| width | double | 容器宽度 | width: 200.0 |
| child | Widget | 子元素内容 | child: Text("Hello")child: Icon(Icons.star) |
关键说明:
alignment 使用
Alignment类的静态常量控制子元素位置,如:dartalignment: Alignment.topCenter // 顶部居中decoration 支持嵌套多种样式效果:
color:背景色(与gradient冲突)border:边框样式borderRadius:圆角半径boxShadow:阴影效果gradient:线性/径向渐变背景
margin & padding 使用
EdgeInsets类定义间距,常用构造方法:dartEdgeInsets.all(20) // 全方向 EdgeInsets.only(left:10) // 指定方向 EdgeInsets.symmetric(vertical: 10) // 对称方向transform 通过 4x4 矩阵实现 3D 变换,常用操作:
dartMatrix4.rotationZ(angle) // Z轴旋转 Matrix4.translationValues(x, y, z) // 平移
Text 组件属性详解
| 属性名 | 类型 | 说明 | 可选值/示例 |
|---|---|---|---|
| textAlign | TextAlign | 文本水平对齐方式 | TextAlign.center(居中)TextAlign.left(左对齐)TextAlign.right(右对齐)TextAlign.justify(两端对齐) |
| textDirection | TextDirection | 文本阅读方向 | TextDirection.ltr(从左至右)TextDirection.rtl(从右至左) |
| overflow | TextOverflow | 文本溢出处理方式 | TextOverflow.clip(裁剪)TextOverflow.fade(渐隐)TextOverflow.ellipsis(省略号) |
| textScaleFactor | double | 字体缩放比例(基于系统字体大小) | 1.0(默认大小)1.2(放大 20%)0.8(缩小 20%) |
| maxLines | int | 最大显示行数 | 1(单行显示)3(最多 3 行)null(无限制) |
| style | TextStyle | 文本样式设置(支持嵌套多种样式) | dart<br>TextStyle(<br> color: Colors.red,<br> fontSize: 16.0,<br> fontWeight: FontWeight.bold,<br> fontStyle: FontStyle.italic,<br> letterSpacing: 1.5,<br> decoration: TextDecoration.underline<br>) |
关键属性详解:
textAlign
控制文本在水平方向的对齐方式:dartText('Hello World', textAlign: TextAlign.center)textDirection
影响对齐方向和溢出行为:dartText('مرحبا', textDirection: TextDirection.rtl) // 阿拉伯语从右向左overflow 与 maxLines 配合使用
常见组合:dartText('长文本内容...', maxLines: 1, overflow: TextOverflow.ellipsis // 单行省略 )textScaleFactor
响应系统字体设置:dartText('可缩放的文本', textScaleFactor: 1.2)style
常用样式属性建议优先用TextStyle管理并复用(如通过copyWith())。
TextStyle 常用属性速查
| 样式属性 | 类型/值 | 说明 |
|---|---|---|
color | Color | 文字颜色 |
fontSize | double | 字体大小 |
fontWeight | FontWeight.w100-w900 | 字体粗细 |
fontStyle | FontStyle.normal / italic | 正常 / 斜体 |
letterSpacing | double | 字符间距 |
decoration | TextDecoration.underline / lineThrough | 下划线 / 删除线 |
fontFamily | String | 自定义字体 |
Text 最佳实践
- 多语言文本务必设置正确的
textDirection - 长文本推荐组合使用
maxLines和overflow - 使用
textScaleFactor保持 UI 可访问性 - 通过
style的copyWith()复用文本样式:
dart
TextStyle baseStyle = TextStyle(fontSize: 16);
Text('标题', style: baseStyle.copyWith(fontWeight: FontWeight.bold))Image 组件属性详解
Image.asset:本地资源图片(需要在pubspec.yaml声明)Image.network:网络图片(可配合缓存库使用)
| 属性名 | 类型 | 说明 | 可选值/示例 |
|---|---|---|---|
| alignment | Alignment | 图片在容器内的对齐方式 | Alignment.topCenterAlignment.bottomLeftAlignment(-0.5, 0.5) |
| color colorBlendMode | Color BlendMode | 颜色叠加与混合模式(需配合使用) | dart<br>color: Colors.green,<br>colorBlendMode: BlendMode.difference |
| fit | BoxFit | 图片填充方式 | BoxFit.fillBoxFit.containBoxFit.coverBoxFit.fitWidthBoxFit.fitHeightBoxFit.scaleDown |
| repeat | ImageRepeat | 图片平铺方式 | ImageRepeat.repeatImageRepeat.repeatXImageRepeat.repeatYImageRepeat.noRepeat |
| width | double | 图片宽度(常配合圆形裁剪使用) | width: 120.0 |
| height | double | 图片高度(常配合圆形裁剪使用) | height: 120.0 |
关键属性详解:
1. color & colorBlendMode
dart
Image.asset('assets/icon.png',
color: Colors.green,
colorBlendMode: BlendMode.difference
)- 常用混合模式:
BlendMode.difference:反色效果BlendMode.multiply:叠加效果BlendMode.saturation:饱和度混合
2. fit(图片填充方式)
| 值 | 效果说明 | 视觉表现 |
|---|---|---|
BoxFit.fill | 拉伸充满容器(可能变形) | ⬛ 全填充(变形) |
BoxFit.contain | 保持比例全显示(可能有留白) | ⬛ 完整显示(带留白) |
BoxFit.cover | 保持比例充满(可能裁剪) | ⬛ 充满(裁剪边缘) |
BoxFit.fitWidth | 宽度充满(高度自适应) | ↔️ 宽度撑满 |
BoxFit.fitHeight | 高度充满(宽度自适应) | ↕️ 高度撑满 |
BoxFit.scaleDown | 保持比例但不放大(contain 变体) | ⬛ 完整显示(不放大) |
3. repeat(平铺模式)
dart
Image.asset('pattern.png',
repeat: ImageRepeat.repeatX,
width: double.infinity
)- 应用场景:
- 背景纹理平铺
- 创建连续图案
- 无缝拼接大图
4. width/height(尺寸控制)
dart
ClipOval(
child: Image.network('url',
width: 100,
height: 100,
fit: BoxFit.cover,
)
)最佳实践:
混合模式选择:
- 使用
color属性实现单色图标 - 通过
colorBlendMode创建特殊滤镜效果
- 使用
响应式图片处理:
dartLayoutBuilder( builder: (context, constraints) { return Image.asset('bg.jpg', width: constraints.maxWidth, fit: BoxFit.cover ); } )圆形头像实现:
dartClipOval( child: Image.asset('avatar.jpg', width: 80, height: 80, fit: BoxFit.cover, // 关键:确保图片居中裁剪 ), )平铺背景优化:
dartContainer( decoration: BoxDecoration( image: DecorationImage( image: AssetImage('tile.png'), repeat: ImageRepeat.repeat, scale: 0.5 // 控制图案密度 ) ) )
Image 性能提示
- 网络图片使用
cacheHeight/cacheWidth减少内存占用 - 需要圆角效果优先考虑
ClipRRect而非borderRadius - 平铺小图替代大图可显著降低内存消耗
Flutter 实现圆形图片的 5 种方法
5 种方案怎么选
| 方案 | 关键 API | 适用场景 |
|---|---|---|
| ClipOval | ClipOval | 最常用,任意图片裁圆 |
| CircleAvatar | backgroundImage | 头像专用,自带占位文本 |
| Container + BoxDecoration | shape: BoxShape.circle | 需要背景/边框/装饰一体化 |
| ClipRRect + 圆角 | BorderRadius.circular | 圆角可调(半径=宽高一半即正圆) |
| PhysicalModel | elevation | 需要物理阴影/层次感 |
核心前提:宽高必须相等才能得到正圆。
方法 1:使用 ClipOval(最常用)
dart
ClipOval(
child: Image.network(
'https://example.com/avatar.jpg',
width: 100,
height: 100,
fit: BoxFit.cover, // 关键:确保图片填充裁剪区域
),
)方法 2:使用 CircleAvatar(头像专用)
dart
CircleAvatar(
radius: 60, // 控制圆形大小
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
child: Text('AB'), // 可添加备用文本
)方法 3:使用 Container + BoxDecoration
dart
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
// borderRadius: BorderRadius.circular(75),
image: DecorationImage(
image: NetworkImage('https://example.com/avatar.jpg'),
fit: BoxFit.cover,
),
),
)方法 4:使用 ClipRRect + 圆角(可调整圆角程度)
dart
ClipRRect(
borderRadius: BorderRadius.circular(100), // 设置为宽高的一半
child: Image.asset(
'assets/profile.jpg',
width: 100,
height: 100,
fit: BoxFit.cover,
),
)方法 5:使用 PhysicalModel(带物理效果)
dart
PhysicalModel(
color: Colors.transparent,
shape: BoxShape.circle,
elevation: 5, // 添加阴影深度
child: Image.network(
'https://example.com/avatar.jpg',
width: 100,
height: 100,
fit: BoxFit.cover,
),
)最佳实践建议:
尺寸控制:
- 宽高必须相等才能得到正圆
- 使用
LayoutBuilder实现响应式圆形:
dartLayoutBuilder( builder: (context, constraints) { double size = constraints.maxWidth; return ClipOval( child: Image.asset('avatar.jpg', width: size, height: size, fit: BoxFit.cover, ), ); } )占位与错误处理:
dartClipOval( child: CachedNetworkImage( imageUrl: 'https://example.com/avatar.jpg', width: 100, height: 100, fit: BoxFit.cover, placeholder: (context, url) => CircularProgressIndicator(), errorWidget: (context, url, error) => Icon(Icons.error), ), )添加边框:
dartContainer( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.blue, width: 3), ), child: ClipOval( child: Image.asset(...), ), )带点击效果的圆形图片:
dartInkWell( onTap: () {}, // 点击事件 borderRadius: BorderRadius.circular(50), child: ClipOval( child: Image.asset(...), ), )
圆形图片性能优化
- 网络图片使用
cached_network_image包缓存 - 本地图片使用
Image.asset并指定精确尺寸 - 对于小图标,优先使用
Icon而不是图片资源
Icon 图标组件(Material Icons)
- Material Design 图标库可在官网查看:Material Icons
dart
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: const [
Icon(Icons.search, color: Colors.red, size: 40),
SizedBox(height: 10),
Icon(Icons.home),
SizedBox(height: 10),
Icon(Icons.category),
SizedBox(height: 10),
Icon(Icons.shop),
SizedBox(height: 10),
],
)
);
}
}集成阿里巴巴图标库自定义字体图标指南
完整实现步骤
dart
// 步骤1:定义图标映射类
class Iconfont {
static const String _family = 'iconfont'; // 与pubspec.yaml中定义一致
Iconfont._();
// 使用Unicode码点定义图标
static const IconData home = IconData(0xe600, fontFamily: _family);
static const IconData search = IconData(0xe601, fontFamily: _family);
static const IconData cart = IconData(0xe602, fontFamily: _family);
static const IconData user = IconData(0xe603, fontFamily: _family);
static const IconData settings = IconData(0xe604, fontFamily: _family);
}
class IconfontDemo extends StatelessWidget {
const IconfontDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('阿里巴巴图标库示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 步骤2:使用自定义图标
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildIcon(Iconfont.home, '首页', Colors.blue),
_buildIcon(Iconfont.search, '搜索', Colors.red),
_buildIcon(Iconfont.cart, '购物车', Colors.orange),
],
),
const SizedBox(height: 40),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildIcon(Iconfont.user, '用户', Colors.green),
_buildIcon(Iconfont.settings, '设置', Colors.purple),
],
),
const SizedBox(height: 40),
// 使用Text显示图标
Text(
String.fromCharCode(Iconfont.home.codePoint),
style: TextStyle(
fontFamily: Iconfont._family,
fontSize: 40,
color: Colors.blue,
),
),
],
),
),
);
}
Widget _buildIcon(IconData icon, String label, Color color) {
return Column(
children: [
Icon(icon, size: 36, color: color),
const SizedBox(height: 8),
Text(label, style: TextStyle(color: color)),
],
);
}
}配置步骤详解
步骤 1:获取图标文件
- 访问阿里巴巴图标库
- 创建项目并添加所需图标
- 下载图标包(选择
Font class方式) - 解压后得到:
iconfont.ttf(字体文件)iconfont.css(包含 Unicode 编码)
步骤 2:配置 Flutter 项目
- 在项目根目录创建
assets/fonts文件夹 - 将
iconfont.ttf复制到此文件夹 - 修改
pubspec.yaml添加字体配置:
yaml
flutter:
uses-material-design: true
assets:
- assets/images/ # 如果有其他资源
fonts:
- family: iconfont # 自定义字体名称
fonts:
- asset: assets/fonts/iconfont.ttf高级用法:自动生成映射类
安装代码生成工具:
bashflutter pub global activate iconfont_builder使用命令生成 Dart 类:
bashflutter pub global run iconfont_builder --from path/to/iconfont.css --to lib/iconfont.dart
常见问题解决
字体图标高频踩坑
图标显示为方块 □ 是最常见问题,几乎都出在字体路径/pubspec.yaml 缩进上——改完配置务必 flutter clean 后重新运行。颜色不生效多因图标为多色图标(需改用 SVG)。
图标显示为方块 □
- 检查字体文件路径是否正确
- 确保
pubspec.yaml缩进正确 - 执行
flutter clean后重新运行
图标颜色不生效
- 确认图标不是多色图标(多色图标需要使用 SVG)
- 检查是否在父组件中覆盖了图标颜色
图标大小异常
- 使用
size参数明确指定尺寸 - 避免在父容器中使用固定宽高限制
- 使用
图标偏移问题
dartIcon( Iconfont.cart, size: 24, textDirection: TextDirection.ltr, // 解决方向问题 )
最佳实践建议
图标管理
- 创建单独的
iconfont.dart文件管理所有图标 - 使用脚本自动更新图标映射类
- 为图标添加详细注释说明用途
- 创建单独的
性能优化
- 使用
const修饰图标组件 - 避免在动画中频繁改变图标
- 复杂图标优先考虑 SVG 格式
- 使用
多主题适配
dartIcon( Iconfont.home, color: Theme.of(context).iconTheme.color, )图标命名规范
dartclass AppIcons { static const IconData home = IconData(0xe600, fontFamily: 'iconfont'); static const IconData search = IconData(0xe601, fontFamily: 'iconfont'); // 按功能模块分组 }
多色图标提示
对于多色图标或需要动态变化的图标,建议使用 flutter_svg 包加载 SVG 格式图标。
ListView 列表组件
列表选型一句话
静态少量数据用 ListView(children:);数据量大或不确定必用 ListView.builder(按需构建、性能最优);需要分隔线用 ListView.separated;吸顶/复杂滚动用 CustomScrollView + Sliver。
列表类型及核心属性
| 列表类型 | 实现方式 | 特点描述 |
|---|---|---|
| 垂直列表 | ListView | 默认垂直滚动,适合单列数据展示 |
| 垂直图文列表 | ListView + ListTile | 图文混合布局,适合商品列表、新闻列表等复杂内容 |
| 水平列表 | ListView(scrollDirection: Axis.horizontal) | 横向滚动,适合轮播图、分类标签等 |
| 动态列表 | ListView.builder | 按需构建列表项,性能最优,适合大数据集 |
核心属性详解
| 属性名 | 类型 | 说明 |
|---|---|---|
| scrollDirection | Axis | 滚动方向:Axis.vertical(垂直,默认),Axis.horizontal(水平) |
| padding | EdgeInsetsGeometry | 列表内边距,如:EdgeInsets.all(10) |
| reverse | bool | 是否反向显示列表(从下往上/从右往左) |
| children | List<Widget> | 直接子组件列表(静态列表使用) |
| itemCount | int | 动态列表项数量(仅builder使用) |
| itemBuilder | IndexedWidgetBuilder | 动态列表项构建函数((context, index) => Widget) |
1. 基础垂直列表
dart
ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(title: Text('选项1')),
ListTile(title: Text('选项2')),
ListTile(title: Text('选项3')),
ListTile(title: Text('选项4')),
],
)2. 垂直图文列表
dart
ListView(
children: [
ListTile(
leading: Image.network('https://example.com/img1.jpg', width: 50),
title: const Text('商品标题1'),
subtitle: const Text('商品描述信息'),
trailing: const Icon(Icons.arrow_forward),
),
ListTile(
leading: Image.network('https://example.com/img2.jpg', width: 50),
title: const Text('商品标题2'),
subtitle: const Text('商品描述信息'),
trailing: const Icon(Icons.arrow_forward),
),
// 更多列表项...
],
)3. 水平列表
dart
SizedBox(
height: 120, // 必须指定高度
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
_buildCategory('美食', Icons.restaurant),
_buildCategory('购物', Icons.shopping_cart),
_buildCategory('旅行', Icons.flight),
_buildCategory('娱乐', Icons.movie),
_buildCategory('学习', Icons.school),
],
),
)
Widget _buildCategory(String name, IconData icon) {
return Container(
width: 100,
margin: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 40),
const SizedBox(height: 8),
Text(name),
],
),
);
}4. 动态列表(性能优化)
dart
final List<String> items = List.generate(100, (i) => '列表项 ${i + 1}');
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(items[index]),
subtitle: Text('索引: $index'),
trailing: Icon(
index % 3 == 0 ? Icons.star : Icons.star_border,
color: Colors.amber,
),
);
},
)高级列表技巧
1. 分隔线控制
dart
ListView.separated(
itemCount: 50,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) => ListTile(title: Text('项目 $index')),
)2. 滚动控制器
dart
final ScrollController _controller = ScrollController();
@override
void initState() {
super.initState();
_controller.addListener(() {
print('滚动位置: ${_controller.offset}');
});
}
ListView.builder(
controller: _controller,
// ...
)3. 无限滚动加载
dart
ListView.builder(
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
return _buildLoadingIndicator();
}
return ListTile(title: Text(items[index]));
},
)
Widget _buildLoadingIndicator() {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}4. 吸顶头部
dart
CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
title: Text('吸顶标题'),
background: Image.network('https://example.com/header.jpg', fit: BoxFit.cover),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('内容 $index')),
childCount: 50,
),
),
],
)性能优化建议
静态列表:
dartListView( children: const [ // 使用const ItemWidget(), // 组件也使用const ItemWidget(), ], )动态列表黄金法则:
- 始终使用
ListView.builder - 设置精确的
itemCount - 避免在
itemBuilder中创建新函数
- 始终使用
懒加载图片:
dartListView.builder( itemBuilder: (context, index) => CachedNetworkImage( imageUrl: images[index], placeholder: (_, __) => LoadingWidget(), ), )列表项保持单一职责:
dart// ❌ 避免 itemBuilder: (c, i) => Column( children: [ if (i == 0) Header(), Item(data[i]) ] ) // ✅ 正确 Column( children: [ Header(), Expanded(child: ListView.builder(...)) ] )使用
RepaintBoundary:dartListView.builder( itemBuilder: (c, i) => RepaintBoundary( child: ComplexItemWidget(data[i]) ) )
常见问题解决方案
嵌套滚动冲突:
dartListView( physics: const ClampingScrollPhysics(), // 解决嵌套滚动 // ... )列表高度错误:
dart// 在Column中包裹ListView Expanded( child: ListView(...) )滚动位置保持:
dartclass _MyListState extends State<MyList> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(context) { super.build(context); return ListView(...); } }动态更新优化:
dart// 使用Key强制重建特定项 ListView.builder( itemBuilder: (c, i) => ItemWidget( key: ValueKey(items[i].id), // 唯一Key data: items[i], ) )
GridView 网格组件
ListView vs GridView 怎么选
ListView 是单列(或单行)线性滚动,GridView 是多列网格滚动。两者的动态构建(.builder)思路一致:数据量大时都应按需构建、设置精确 itemCount。GridView 的关键在于 gridDelegate 控制每行数量与子项宽高比。
核心属性总览
| 属性名 | 类型 | 说明 |
|---|---|---|
| scrollDirection | Axis | 滚动方向:Axis.vertical(垂直,默认),Axis.horizontal(水平) |
| padding | EdgeInsetsGeometry | 内边距 |
| reverse | bool | 是否反向排序(默认 false) |
| crossAxisSpacing | double | 水平子 Widget 之间的间距 |
| mainAxisSpacing | double | 垂直子 Widget 之间的间距 |
| crossAxisCount | int | 在GridView.count中使用,指定一行中的网格数量 |
| maxCrossAxisExtent | double | 在GridView.extent中使用,指定横轴子元素的最大长度 |
| childAspectRatio | double | 子 Widget 宽高比例(宽度/高度) |
| children | List<Widget> | 子组件列表(用于静态网格) |
| gridDelegate | SliverGridDelegate | 布局控制器(用于动态网格),有两种类型:SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent |
三种创建方式详解
1. GridView.count - 固定数量网格
dart
GridView.count(
crossAxisCount: 3, // 每行3个元素
crossAxisSpacing: 8, // 水平间距
mainAxisSpacing: 12, // 垂直间距
childAspectRatio: 0.8, // 宽高比(宽/高)
padding: const EdgeInsets.all(16),
children: List.generate(12, (index) {
return Container(
color: Colors.blue[100 * (index % 9)],
child: Center(child: Text('Item $index')),
);
}),
)2. GridView.extent - 最大宽度网格
dart
GridView.extent(
maxCrossAxisExtent: 150, // 每个网格最大宽度150
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1, // 正方形网格
children: List.generate(20, (index) {
return Card(
elevation: 3,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.star, size: 40, color: Colors.amber),
Text('商品 $index')
],
),
);
}),
)3. GridView.builder - 动态网格(推荐)
dart
final List<Product> products = [
Product(name: '手机', icon: Icons.phone_iphone),
Product(name: '电脑', icon: Icons.computer),
// ...更多数据
];
GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, // 每行4个
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.9,
),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductItem(product: products[index]);
},
)高级网格布局技巧
1. 响应式网格布局
dart
LayoutBuilder(
builder: (context, constraints) {
int crossAxisCount;
if (constraints.maxWidth > 600) {
crossAxisCount = 4; // 大屏幕4列
} else if (constraints.maxWidth > 400) {
crossAxisCount = 3; // 中等屏幕3列
} else {
crossAxisCount = 2; // 小屏幕2列
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
// ...
),
// ...
);
},
)2. 网格与列表混合布局
dart
CustomScrollView(
slivers: [
SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 0.8,
),
delegate: SliverChildBuilderDelegate(
(context, index) => ProductCard(products[index]),
childCount: products.length,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('推荐商品 $index')),
childCount: 10,
),
),
],
)3. 网格头部吸顶效果
dart
CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
title: Text('商品分类'),
background: Image.network('header.jpg', fit: BoxFit.cover),
),
),
SliverGrid(
// ...网格配置
),
],
)4. 网格项动画效果
dart
GridView.builder(
gridDelegate: /*...*/,
itemBuilder: (context, index) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: /*...*/,
);
},
)网格布局最佳实践
1. 性能优化方案
dart
GridView.builder( // 始终使用builder
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: items.length,
itemBuilder: (context, index) {
return ProductCard( // 提取为独立组件
key: ValueKey(items[index].id), // 唯一键值
product: items[index],
);
},
)2. 网格卡片组件示例
dart
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
child: Image.network(product.imageUrl, fit: BoxFit.cover),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.name, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text('¥${product.price}', style: const TextStyle(color: Colors.red)),
],
),
),
],
),
);
}
}3. 网格布局常见问题解决
问题:网格高度不正确
dart
// 解决方案:使用Expanded包裹
Scaffold(
body: Column(
children: [
Expanded( // 关键
child: GridView.count(
crossAxisCount: 3,
// ...
),
),
],
),
)问题:滚动冲突
dart
GridView.builder(
physics: const ClampingScrollPhysics(), // 解决嵌套滚动
// ...
)问题:网格项大小不一致
dart
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150,
mainAxisExtent: 180, // 固定高度
// ...
)网格布局使用场景示例
电商商品网格
dart
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductGridItem(product: products[index]);
},
)照片墙布局
dart
GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
),
itemCount: images.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => _openFullScreen(index),
child: Hero(
tag: 'image-$index',
child: Image.network(images[index], fit: BoxFit.cover),
),
);
},
)设置选项网格
dart
GridView.count(
crossAxisCount: 4,
padding: const EdgeInsets.all(24),
children: [
_buildSettingItem(Icons.wifi, '网络'),
_buildSettingItem(Icons.bluetooth, '蓝牙'),
_buildSettingItem(Icons.battery_full, '电池'),
_buildSettingItem(Icons.storage, '存储'),
// ...更多设置项
],
)
Widget _buildSettingItem(IconData icon, String label) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 32),
const SizedBox(height: 8),
Text(label),
],
);
}性能提示:
- 对于大型网格使用
GridView.builder+SliverGridDelegate- 使用
const构造函数优化静态网格项- 为网格项添加
RepaintBoundary减少重绘- 使用
cacheExtent预加载网格区域提高滚动流畅度
Padding 组件
Padding vs Container
只需要内边距时优先用 Padding——它更轻量、语义更清晰;当你同时需要背景、边框、圆角、尺寸时才用 Container(其 padding 属性本质就是内嵌了一个 Padding)。
| 属性名 | 类型 | 说明 | 示例 |
|---|---|---|---|
| padding | EdgeInsetsGeometry | 设置内边距(核心属性) | EdgeInsets.all(16)EdgeInsets.only(top: 20)EdgeInsets.symmetric(horizontal: 10) |
| child | Widget | 需要添加内边距的子组件 | child: Text('Hello')child: Icon(Icons.star) |
1. 统一内边距
dart
Padding(
padding: const EdgeInsets.all(20), // 所有方向20像素内边距
child: Container(
color: Colors.blue,
child: Text('统一内边距'),
),
)2. 非对称内边距
dart
Padding(
padding: const EdgeInsets.only(
top: 30,
left: 15,
right: 15
), // 仅顶部/左侧/右侧有内边距
child: Card(
child: ListTile(
title: Text('非对称内边距示例'),
subtitle: Text('底部无内边距'),
),
),
)3. 对称内边距
dart
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10, // 垂直方向10px
horizontal: 20 // 水平方向20px
),
child: ElevatedButton(
onPressed: () {},
child: Text('对称内边距按钮'),
),
)4. 组合内边距
dart
Padding(
padding: const EdgeInsets.fromLTRB(15, 10, 15, 5), // 左15, 上10, 右15, 下5
child: Text('自定义组合内边距'),
)线性布局(Row 和 Column)
布局怎么选:一张图看懂主流布局组件
Flutter 布局的核心是「沿什么方向摆放子组件」。线性布局(Row/Column)是最基础的一维排列,配合 Expanded/Flexible 控制占比。
text
┌──────────────────────┐
│ 要摆放多个子组件? │
└──────────┬───────────┘
│ 排列方向
┌───────────┬───────┴───────┬──────────────┐
水平一行 垂直一列 自动换行 层叠重合
│ │ │ │
Row Column Wrap Stack + Positioned/Align
│ │
主轴=水平 主轴=垂直
交叉轴=垂直 交叉轴=水平
└─────┬─────┘
▼
MainAxisAlignment 控主轴
▼
CrossAxisAlignment 控交叉轴
▼
Expanded/Flexible 分配剩余空间记住:MainAxisAlignment 管主轴方向的分布,CrossAxisAlignment 管交叉轴方向的对齐。
| 属性名 | Row(水平布局) | Column(垂直布局) | 说明 |
|---|---|---|---|
| 主轴方向 | 水平(从左到右) | 垂直(从上到下) | 布局的主轴方向 |
| 主轴对齐 | MainAxisAlignment | MainAxisAlignment | 控制子组件在主轴上的排列方式 |
| 交叉轴对齐 | CrossAxisAlignment | CrossAxisAlignment | 控制子组件在交叉轴上的排列方式 |
| 主轴尺寸 | MainAxisSize | MainAxisSize | 主轴方向占用的空间(max 或 min) |
| 文本方向 | TextDirection | - | 控制水平方向子组件的排列顺序 |
| 垂直方向 | - | VerticalDirection | 控制垂直方向子组件的排列顺序 |
| 子组件列表 | children | children | 包含的子组件 |
1. 主轴对齐(MainAxisAlignment)
dart
enum MainAxisAlignment {
start, // 从起始位置开始排列
end, // 从结束位置开始排列
center, // 居中排列
spaceBetween, // 两端对齐,子组件之间间距相等
spaceAround, // 每个子组件两侧间距相等
spaceEvenly, // 所有间距相等(包括两端)
}2. 交叉轴对齐(CrossAxisAlignment)
dart
enum CrossAxisAlignment {
start, // 沿交叉轴起始位置对齐
end, // 沿交叉轴结束位置对齐
center, // 居中对齐(默认)
stretch, // 拉伸子组件填满交叉轴
baseline, // 按文本基线对齐
}3. 主轴尺寸(MainAxisSize)
dart
enum MainAxisSize {
min, // 占用最小空间
max, // 占用最大可用空间(默认)
}基础用法示例
1. 基本水平布局(Row)
dart
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.star, color: Colors.amber),
Text('评分:4.8', style: TextStyle(fontSize: 16)),
ElevatedButton(
onPressed: () {},
child: Text('购买'),
),
],
)2. 基本垂直布局(Column)
dart
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('标题', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('副标题', style: TextStyle(fontSize: 16, color: Colors.grey)),
SizedBox(height: 16),
Text('详细内容...' * 5),
],
)高级布局技巧
1. 灵活空间分配(Expanded)
dart
Row(
children: [
Expanded(
flex: 2, // 占2份
child: Container(height: 50, color: Colors.red),
),
Expanded(
flex: 3, // 占3份
child: Container(height: 50, color: Colors.blue),
),
],
)2. 相对空间分配(Flexible)
dart
Row(
children: [
Flexible(
fit: FlexFit.loose, // 宽松分配
child: Container(width: 150, color: Colors.green),
),
Flexible(
fit: FlexFit.tight, // 紧密分配
child: Container(color: Colors.yellow),
),
],
)3. 基线对齐(跨轴)
dart
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'24',
style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
),
Text(
'°C',
style: TextStyle(fontSize: 20),
),
],
)4. 嵌套布局
dart
Column(
children: [
// 顶部导航栏
Row(
children: [
IconButton(icon: Icon(Icons.menu),
Expanded(child: TextField()),
IconButton(icon: Icon(Icons.search)),
],
),
// 内容区域
Expanded(
child: ListView(/*...*/),
),
// 底部导航
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(icon: Icon(Icons.home)),
IconButton(icon: Icon(Icons.shopping_cart)),
IconButton(icon: Icon(Icons.person)),
],
),
],
)最佳实践与解决方案
1. 处理溢出问题
dart
Row(
children: [
Expanded( // 使用Expanded防止溢出
child: Text('这是一个非常长的文本内容' * 5),
),
IconButton(icon: Icon(Icons.more_vert)),
],
)2. 响应式布局
dart
LayoutBuilder(
builder: (context, constraints) {
bool isWide = constraints.maxWidth > 600;
return isWide
? Row(children: [NavigationRail(), ContentArea()]) // 宽屏布局
: Column(children: [AppBar(), ContentArea()]); // 窄屏布局(示例占位组件)
},
)说明:这里的
ContentArea()/NavigationRail()/AppBar()作为“占位组件”用于演示布局切换。实际项目中请替换为你自己的页面组件,避免和 Flutter 的AppBarWidget 概念混淆。
3. 动态列表构建
dart
Column(
children: List.generate(10, (index) =>
ListTile(
leading: CircleAvatar(child: Text('${index+1}')),
title: Text('项目 $index'),
),
),
)4. 复杂对齐场景
dart
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左侧头像
CircleAvatar(radius: 30),
// 右侧内容(使用Column)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('用户名', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(width: 8),
Text('@userhandle', style: TextStyle(color: Colors.grey)),
],
),
SizedBox(height: 4),
Text('内容文本...'),
],
),
),
],
)性能优化指南
使用 const 构造函数:
dartconst Row( children: [ Icon(Icons.star), Text('固定内容'), ], )提取子组件:
dart// 提取为独立组件 class RatingBar extends StatelessWidget { const RatingBar({super.key}); @override Widget build(BuildContext context) { return Row(/*...*/); } }避免深层嵌套:
dart// ❌ 不推荐(嵌套过深) Row(children: [Column(children: [Row(children: [...]])]) // ✅ 推荐(提取子组件) CustomComplexWidget()使用 IntrinsicHeight(谨慎使用):
dartIntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 等高子组件 ], ), )
常见布局模式
1. 居中图标+文字按钮
dart
ElevatedButton(
onPressed: () {},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.download),
SizedBox(width: 8),
Text('下载文件'),
],
),
)2. 标签+数值展示
dart
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('温度', style: TextStyle(color: Colors.grey)),
Text('24°C', style: TextStyle(fontSize: 20)),
],
)3. 带图标的输入框
dart
Row(
children: [
Icon(Icons.search, color: Colors.grey),
SizedBox(width: 8),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: '搜索...',
border: InputBorder.none,
),
),
),
],
)4. 卡片底部操作栏
dart
Card(
child: Column(
children: [
Image.network('image.jpg'),
Padding(
padding: const EdgeInsets.all(16),
child: Text('描述内容'),
),
const Divider(height: 1),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(onPressed: () {}, child: Text('取消')),
TextButton(onPressed: () {}, child: Text('确定')),
],
),
],
),
)线性布局实践建议
- 使用
Spacer()在布局中创建灵活空间 - 结合
Wrap组件实现自动换行 - 在需要滚动时使用
SingleChildScrollView包裹 Row/Column - 使用
LayoutBuilder获取父容器约束实现响应式布局
层叠布局(Stack, Align, Positioned)
三剑客分工
Stack 让子组件像图层一样重叠堆放;Positioned 用 top/right/bottom/left 精确定位子组件(仅在 Stack 内有效);Align 用对齐方式(如 Alignment.bottomRight)相对定位。典型场景:图片上叠加文字、角标、悬浮按钮。
| 组件 | 主要功能 | 核心特点 |
|---|---|---|
| Stack | 层叠布局容器,允许子组件重叠显示 | 支持相对定位和绝对定位 |
| Align | 控制子组件在父容器中的对齐位置 | 支持精确的分数定位(FractionalOffset) |
| Positioned | 在 Stack 中精确控制子组件的位置 | 可设置上下左右四个方向的偏移值 |
Stack 属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| alignment | AlignmentDirectional | 默认子组件对齐方式(默认左上角) |
| textDirection | TextDirection | 文本方向(影响对齐方向) |
| fit | StackFit | 未定位子组件的尺寸调整方式(loose/expand/passthrough) |
| clipBehavior | Clip | 内容溢出处理方式(默认为 Clip.hardEdge) |
| children | List<Widget> | 子组件列表 |
Align 属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| alignment | Alignment | 对齐方式(如 Alignment.center) |
| widthFactor | double | 宽度因子(相对于子组件宽度) |
| heightFactor | double | 高度因子(相对于子组件高度) |
| child | Widget | 子组件 |
Positioned 属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| left | double | 距离 Stack 左边的距离 |
| top | double | 距离 Stack 顶部的距离 |
| right | double | 距离 Stack 右边的距离 |
| bottom | double | 距离 Stack 底部的距离 |
| width | double | 指定宽度(非必须) |
| height | double | 指定高度(非必须) |
| child | Widget | 子组件 |
基础用法示例
1. 基础层叠布局
dart
Stack(
children: [
// 底层背景
Container(color: Colors.blue, height: 200, width: double.infinity),
// 居中对齐的文本
const Align(
alignment: Alignment.center,
child: Text('居中文本', style: TextStyle(color: Colors.white, fontSize: 24)),
// 右上角图标
const Positioned(
top: 10,
right: 10,
child: Icon(Icons.star, color: Colors.amber, size: 30),
),
// 左下角按钮
Positioned(
bottom: 20,
left: 20,
child: ElevatedButton(
onPressed: () {},
child: const Text('点击'),
),
),
],
)2. 相对定位(FractionalOffset)
dart
Align(
alignment: const FractionalOffset(0.7, 0.3), // 70%宽度,30%高度处
child: Container(
width: 50,
height: 50,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
)3. 部分定位
dart
Positioned(
left: 20,
right: 20, // 左右定位,宽度自适应
bottom: 40,
child: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(10),
),
child: const Center(child: Text('底部横幅')),
),
)高级应用技巧
1. 响应式层叠布局
dart
LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
// 背景图片
Image.network('background.jpg',
width: constraints.maxWidth,
height: constraints.maxHeight,
fit: BoxFit.cover),
// 根据屏幕尺寸调整位置
Positioned(
top: constraints.maxHeight * 0.1,
left: constraints.maxWidth * 0.15,
child: const Text('响应式标题', style: TextStyle(fontSize: 24)),
),
],
);
},
)2. 动画浮动按钮
dart
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
bottom: _expanded ? 100 : 20,
right: _expanded ? 100 : 20,
child: FloatingActionButton(
onPressed: () => setState(() => _expanded = !_expanded),
child: const Icon(Icons.add),
),
)3. 自定义对话框
dart
Stack(
children: [
// 半透明遮罩
Positioned.fill(
child: Container(color: Colors.black54),
),
// 居中对话框
Center(
child: Container(
width: 300,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('确认删除?', style: TextStyle(fontSize: 18)),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(onPressed: () {}, child: const Text('取消')),
TextButton(onPressed: () {}, child: const Text('确定')),
],
)
],
),
),
),
],
)4. 图片水印效果
dart
Stack(
children: [
// 主图片
Image.network('photo.jpg'),
// 水印文本(重复排列)
Positioned.fill(
child: Column(
children: List.generate(5, (row) => Expanded(
child: Row(
children: List.generate(4, (col) => Expanded(
child: Center(
child: Transform.rotate(
angle: -0.2,
child: const Text('机密',
style: TextStyle(
color: Colors.white30,
fontSize: 24,
fontWeight: FontWeight.bold
),
),
),
),
)),
),
)),
),
),
],
)最佳实践与解决方案
1. 处理溢出问题
dart
Stack(
clipBehavior: Clip.none, // 允许内容溢出
children: [
Container(color: Colors.blue, width: 100, height: 100),
Positioned(
top: 80, // 部分溢出容器
left: 80,
child: Container(width: 50, height: 50, color: Colors.red),
),
],
)2. 等比例缩放元素
dart
Align(
alignment: Alignment.center,
widthFactor: 0.8, // 容器宽度为子组件的80%
heightFactor: 0.6, // 容器高度为子组件的60%
child: Container(
color: Colors.green,
child: const Center(child: Text('等比例缩放')),
),3. 复杂定位系统
dart
Stack(
children: [
// 使用Align定位
const Align(
alignment: Alignment(0.8, -0.8), // 右上区域
child: Icon(Icons.star, size: 40),
),
// 使用Positioned定位
Positioned(
top: MediaQuery.of(context).size.height * 0.4,
left: MediaQuery.of(context).size.width * 0.2,
child: const Icon(Icons.circle, size: 40),
),
// 使用FractionalTranslation
FractionalTranslation(
translation: const Offset(0.5, 0.5), // 中心点
child: const Icon(Icons.add, size: 60),
),
],
)4. 性能优化
dart
// 使用const避免重绘
const Stack(
children: [
Positioned(top: 10, child: Icon(Icons.star)),
Align(alignment: Alignment.center, child: Text('固定内容')),
],
)
// 提取独立组件
class Badge extends StatelessWidget {
const Badge({super.key});
@override
Widget build(BuildContext context) {
return const Positioned(
top: 5,
right: 5,
child: CircleAvatar(radius: 10, backgroundColor: Colors.red),
);
}
}常见使用场景
1. 带徽章的头像
dart
Stack(
clipBehavior: Clip.none, // 允许徽章溢出
children: [
const CircleAvatar(radius: 30, backgroundImage: NetworkImage('avatar.jpg')),
Positioned(
top: -5,
right: -5,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Text('3', style: TextStyle(color: Colors.white)),
),
),
],
)2. 卡片重叠效果
dart
Stack(
children: [
Positioned( // 底层卡片
top: 20,
left: 20,
right: 20,
child: Card(
elevation: 4,
child: Container(height: 150),
),
),
Positioned( // 中层卡片
top: 40,
left: 40,
right: 40,
child: Card(
elevation: 8,
child: Container(height: 150),
),
),
Positioned( // 顶层卡片
top: 60,
left: 60,
right: 60,
child: Card(
elevation: 12,
child: Container(height: 150),
),
),
],
)3. 视频播放器控制层
dart
Stack(
children: [
// 视频内容
VideoPlayer(controller: _controller),
// 控制层(半透明背景)
Positioned.fill(
child: Container(
color: Colors.black45,
child: Column(
children: [
// 顶部标题栏
AppBar(title: const Text('视频标题'), backgroundColor: Colors.transparent),
// 中间播放按钮
Expanded(
child: Center(
child: IconButton(
icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
iconSize: 50,
color: Colors.white,
onPressed: _togglePlay,
),
),
),
// 底部进度条
VideoProgressIndicator(_controller, allowScrubbing: true),
],
),
),
),
],
)4. 地图标记点
dart
Stack(
children: [
// 地图组件
GoogleMap(/*...*/),
// 标记点
Positioned(
left: 100,
top: 200,
child: Column(
children: [
const Icon(Icons.location_pin, color: Colors.red, size: 40),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
child: const Text('位置A'),
),
],
),
),
],
)层叠布局实践建议
- 优先使用
Positioned进行精确控制,使用Align进行相对定位 - 使用
LayoutBuilder实现响应式层叠布局 - 对于复杂布局,组合使用
Stack+Transform+Opacity - 使用
Clip.none允许内容溢出容器边界 - 避免在
Stack中嵌套过多子组件(超过 5 个考虑重构)
Card 组件全面指南
Card 是 Flutter 中用于创建卡片式布局的核心组件,具有圆角、阴影和立体效果,非常适合展示内容区块。以下是 Card 组件的详细解析:
Card 使用场景
Card 本质是带阴影和圆角的 Material 容器,适合列表项、信息卡片、商品展示等需要「区块感」的场景。elevation 控制阴影深度体现层级;配合 InkWell/ListTile 可实现点击涟漪效果。Card 只有一个 child,多内容需嵌套 Column/Row。
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| margin | EdgeInsetsGeometry | null | 卡片外部的边距 |
| child | Widget | 必填 | 卡片内容区域 |
| elevation | double | 1.0 | 阴影深度(值越大阴影越明显) |
| color | Color | Theme.cardColor | 卡片背景颜色 |
| shadowColor | Color | Theme.shadowColor | 阴影颜色 |
| shape | ShapeBorder | RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)) | 卡片形状(可自定义圆角) |
| clipBehavior | Clip | Clip.none | 内容裁剪方式(none/antiAlias/antiAliasWithSaveLayer/hardEdge) |
| borderOnForeground | bool | true | 是否在子组件前绘制边框 |
| surfaceTintColor | Color | null | 材料表面色调颜色(Material 3) |
基础用法示例
1. 基础卡片
dart
Card(
child: ListTile(
leading: Icon(Icons.person),
title: Text('用户姓名'),
subtitle: Text('用户描述信息'),
trailing: Icon(Icons.arrow_forward),
),
)2. 自定义卡片
dart
Card(
elevation: 8, // 更强的阴影效果
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), // 更大的圆角
),
color: Colors.blue[50], // 自定义背景色
margin: EdgeInsets.all(16), // 外边距
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('卡片标题', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('这里是卡片的内容区域,可以放置各种组件...'),
SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () {},
child: Text('操作按钮'),
),
),
],
),
),
)3. 图文卡片
dart
Card(
clipBehavior: Clip.antiAlias, // 平滑裁剪
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Image.network(
'https://example.com/image.jpg',
height: 150,
fit: BoxFit.cover,
),
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('产品名称', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('¥299', style: TextStyle(color: Colors.red, fontSize: 20)),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: List.generate(5, (i) =>
Icon(Icons.star, size: 16, color: i < 4 ? Colors.amber : Colors.grey)
),
),
Text('128条评价', style: TextStyle(color: Colors.grey)),
],
),
],
),
),
],
),
)高级用法与技巧
1. 响应式卡片设计
dart
LayoutBuilder(
builder: (context, constraints) {
bool isWide = constraints.maxWidth > 600;
return Card(
margin: EdgeInsets.symmetric(
vertical: 16,
horizontal: isWide ? 40 : 16,
),
child: Flex(
direction: isWide ? Axis.horizontal : Axis.vertical,
children: [
if (isWide) Expanded(
flex: 2,
child: Image.asset('product.jpg', fit: BoxFit.cover),
),
Expanded(
flex: 3,
child: Padding(
padding: EdgeInsets.all(20),
child: /* 内容区域 */
),
),
],
),
);
},
)2. 动画卡片
dart
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Card(
elevation: _isHovered ? 12 : 4, // 悬停时提升阴影
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_isHovered ? 20 : 10),
),
child: InkWell(
onTap: () {},
onHover: (hover) => setState(() => _isHovered = hover),
child: /* 卡片内容 */
),
),
)3. 卡片堆叠效果
dart
Stack(
children: [
Positioned(
top: 20,
left: 20,
right: 20,
child: Card(
elevation: 2,
color: Colors.grey[200],
child: Container(height: 120),
),
),
Positioned(
top: 40,
left: 40,
right: 40,
child: Card(
elevation: 6,
color: Colors.grey[300],
child: Container(height: 120),
),
),
Card(
elevation: 12,
margin: EdgeInsets.symmetric(horizontal: 60),
child: Container(
height: 120,
padding: EdgeInsets.all(16),
child: Center(child: Text('顶层卡片')),
),
),
],
)4. 卡片式表单
dart
Card(
margin: EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('登录账户', style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 24),
TextFormField(
decoration: InputDecoration(labelText: '用户名'),
validator: (value) => value!.isEmpty ? '请输入用户名' : null,
),
SizedBox(height: 16),
TextFormField(
obscureText: true,
decoration: InputDecoration(labelText: '密码'),
validator: (value) => value!.isEmpty ? '请输入密码' : null,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _submitForm,
child: Text('登录'),
),
],
),
),
),
)最佳实践指南
1. 内容组织原则
dart
Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. 标题区域
Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text('卡片标题', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
// 2. 分隔线(可选)
Divider(height: 1, thickness: 1),
// 3. 内容主体
Padding(
padding: EdgeInsets.all(16),
child: Text('这里是卡片的主要内容区域...'),
),
// 4. 操作区域
ButtonBar(
alignment: MainAxisAlignment.end,
children: [
TextButton(onPressed: () {}, child: Text('取消')),
ElevatedButton(onPressed: () {}, child: Text('确定')),
],
),
],
),
)2. 性能优化
dart
// 使用const减少重建
const Card(
child: ListTile(
title: Text('固定内容'),
),
)
// 复杂卡片提取为独立组件
class ProductCard extends StatelessWidget {
const ProductCard({super.key});
@override
Widget build(BuildContext context) {
return Card(/* ... */);
}
}3. 主题一致性
dart
MaterialApp(
theme: ThemeData(
cardTheme: CardTheme(
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
),
),
home: Scaffold(/* ... */),
)4. 无障碍支持
dart
Card(
semanticContainer: true, // 将卡片作为语义容器
child: MergeSemantics(
child: ListTile(
title: Text('无障碍卡片'),
subtitle: Text('屏幕阅读器会朗读整个卡片内容'),
onTap: () {},
),
),
)常见问题解决方案
1. 阴影效果不明显
dart
Card(
elevation: 8, // 增加阴影深度
shadowColor: Colors.black.withOpacity(0.5), // 加深阴影颜色
// ...
)2. 圆角不生效
dart
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), // 设置圆角半径
),
// ...
)3. 内容超出边界
dart
Card(
clipBehavior: Clip.antiAlias, // 裁剪超出内容
child: Column(
children: [
Image.asset('large-image.jpg'), // 可能超出边界
// ...
],
),
)4. 响应式边距
dart
Card(
margin: EdgeInsets.symmetric(
vertical: 16,
horizontal: MediaQuery.of(context).size.width > 600 ? 40 : 16,
),
// ...
)实际应用场景
1. 设置项卡片
dart
Card(
child: Column(
children: [
SwitchListTile(
title: Text('通知设置'),
value: _notificationsEnabled,
onChanged: (v) => setState(() => _notificationsEnabled = v),
),
Divider(height: 1),
SwitchListTile(
title: Text('夜间模式'),
value: _darkMode,
onChanged: (v) => setState(() => _darkMode = v),
),
],
),
)2. 数据展示卡片
dart
Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('今日数据', style: TextStyle(fontSize: 18)),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildDataItem(Icons.visibility, '访问量', '1,248'),
_buildDataItem(Icons.shopping_cart, '订单', '86'),
_buildDataItem(Icons.attach_money, '收入', '¥5,280'),
],
),
],
),
),
)
Widget _buildDataItem(IconData icon, String label, String value) {
return Column(
children: [
Icon(icon, size: 30, color: Colors.blue),
SizedBox(height: 8),
Text(label, style: TextStyle(color: Colors.grey)),
SizedBox(height: 4),
Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
],
);
}3. 用户资料卡片
dart
Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 40,
backgroundImage: NetworkImage('avatar.jpg'),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('张小明', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('高级UI设计师', style: TextStyle(color: Colors.grey)),
SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
Chip(label: Text('UI设计')),
Chip(label: Text('插画')),
Chip(label: Text('动效')),
],
),
],
),
),
],
),
),
)4. 天气信息卡片
dart
Card(
color: Colors.blue[700],
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('武汉市', style: TextStyle(fontSize: 24, color: Colors.white)),
Text('2023-07-15', style: TextStyle(color: Colors.white70)),
],
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
Text('28°', style: TextStyle(fontSize: 48, color: Colors.white)),
Text('多云', style: TextStyle(color: Colors.white)),
],
),
WeatherIcon(weather: 'cloudy', size: 80),
],
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('湿度: 65%', style: TextStyle(color: Colors.white)),
Text('风速: 3km/h', style: TextStyle(color: Colors.white)),
Text('气压: 1013hPa', style: TextStyle(color: Colors.white)),
],
),
],
),
),
)Card 实践建议
- 使用
elevation控制卡片层次感,但避免过度使用(建议 1-12 范围) - 结合
ClipRRect实现图片圆角效果更自然 - 使用
InkWell或InkResponse为卡片添加水波纹效果 - 在暗色模式下调整卡片颜色:
color: Theme.of(context).cardColor - 使用
Card嵌套Card创建分层设计效果 - 遵循 Material Design 指南,卡片间距至少 8dp,内容内边距至少 16dp
CircleAvatar 组件
CircleAvatar 是一个圆形头像组件,用于展示用户头像、图标或文字,支持图片、图标、文字等多种内容形式,是用户界面中展示个人信息的核心组件。
CircleAvatar 内容优先级
CircleAvatar 的内容显示遵循优先级:foregroundImage > backgroundImage > child(文字/图标)> backgroundColor 纯色背景。加载网络头像时建议同时设置 backgroundColor 作为占位色,避免图片加载前出现空白。
核心属性表
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| backgroundColor | Color | 主题默认色 | 圆形背景颜色(当没有背景图片时) |
| backgroundImage | ImageProvider | null | 背景图片(如 NetworkImage、AssetImage) |
| foregroundImage | ImageProvider | null | 前景图片(覆盖在背景之上) |
| child | Widget | null | 子组件(通常是文字或图标) |
| radius | double | 20 | 圆形半径(定义大小) |
| minRadius | double | null | 最小半径 |
| maxRadius | double | null | 最大半径 |
| onBackgroundImageError | Function | null | 背景图片加载失败的回调函数 |
| onForegroundImageError | Function | null | 前景图片加载失败的回调函数 |
基础用法示例
1. 文字头像
dart
CircleAvatar(
radius: 30,
backgroundColor: Colors.blue,
child: Text(
'AB',
style: TextStyle(fontSize: 20, color: Colors.white),
),
)2. 图片头像
dart
CircleAvatar(
radius: 40,
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
)3. 图标头像
dart
CircleAvatar(
radius: 30,
backgroundColor: Colors.amber,
child: Icon(Icons.person, color: Colors.white),
)4. 带边框的头像
dart
CircleAvatar(
radius: 40,
backgroundColor: Colors.white, // 边框颜色
child: CircleAvatar(
radius: 38,
backgroundImage: AssetImage('assets/avatar.jpg'),
),
)高级用法与技巧
1. 加载失败处理
dart
CircleAvatar(
radius: 40,
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
onBackgroundImageError: (exception, stackTrace) {
// 图片加载失败时显示默认头像
debugPrint('头像加载失败: $exception');
},
child: Text('加载失败', style: TextStyle(fontSize: 12)), // 备用显示
)2. 动态头像状态
dart
Stack(
clipBehavior: Clip.none,
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(user.avatarUrl),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(3),
decoration: BoxDecoration(
color: user.isOnline ? Colors.green : Colors.grey,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
width: 12,
height: 12,
),
),
],
)3. 头像组合
dart
Stack(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.grey[300],
child: Text('+5', style: TextStyle(color: Colors.black)),
),
Positioned(
left: 15,
child: CircleAvatar(
radius: 25,
backgroundColor: Colors.blue[300],
child: Text('+4'),
),
),
Positioned(
left: 30,
child: CircleAvatar(
radius: 25,
backgroundColor: Colors.green[300],
child: Text('+3'),
),
),
// 更多头像...
],
)4. 头像动画效果
dart
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _isExpanded ? 100 : 50,
height: _isExpanded ? 100 : 50,
child: CircleAvatar(
backgroundImage: NetworkImage('avatar.jpg'),
),
)最佳实践指南
1. 统一头像尺寸
dart
// 创建统一尺寸的头像组件
class AppAvatar extends StatelessWidget {
final String? imageUrl;
final String? text;
const AppAvatar({this.imageUrl, this.text});
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: 24,
backgroundColor: Theme.of(context).primaryColor,
backgroundImage: imageUrl != null ? NetworkImage(imageUrl!) : null,
child: text != null
? Text(text!.substring(0, 2),
: Icon(Icons.person),
);
}
}2. 性能优化
dart
// 使用const减少重建
const CircleAvatar(
radius: 30,
child: Icon(Icons.person),
)
// 本地资源使用AssetImage
CircleAvatar(
backgroundImage: AssetImage('assets/avatar.png'),
)
// 网络图片使用缓存
CachedNetworkImage(
imageUrl: 'https://example.com/avatar.jpg',
imageBuilder: (context, imageProvider) => CircleAvatar(
backgroundImage: imageProvider,
),
placeholder: (context, url) => CircleAvatar(
backgroundColor: Colors.grey[200],
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => CircleAvatar(
backgroundColor: Colors.grey[200],
child: Icon(Icons.error),
),
)3. 主题一致性
dart
MaterialApp(
theme: ThemeData(
avatarTheme: AvatarThemeData(
backgroundColor: Colors.blue[800],
radius: 22,
),
),
)4. 无障碍支持
dart
CircleAvatar(
backgroundImage: NetworkImage('avatar.jpg'),
child: ExcludeSemantics( // 隐藏文本内容
child: Text(user.name.substring(0, 2)),
),
semanticLabel: '${user.name}的头像', // 屏幕阅读器会朗读此内容
)实际应用场景
1. 用户列表项
dart
ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(user.avatarUrl),
),
title: Text(user.name),
subtitle: Text(user.email),
trailing: Icon(Icons.arrow_forward),
)2. 聊天界面
dart
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 20,
backgroundImage: NetworkImage(message.sender.avatar),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(message.sender.name, style: TextStyle(fontWeight: FontWeight.bold)),
Text(message.content),
],
),
),
],
)3. 个人资料页
dart
Column(
children: [
CircleAvatar(
radius: 60,
backgroundImage: NetworkImage(profile.avatar),
),
SizedBox(height: 16),
Text(profile.name, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
Text(profile.title, style: TextStyle(color: Colors.grey)),
],
)4. 评论组件
dart
Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(radius: 20, backgroundImage: NetworkImage(comment.user.avatar)),
SizedBox(width: 12),
Text(comment.user.name, style: TextStyle(fontWeight: FontWeight.bold)),
],
),
SizedBox(height: 12),
Text(comment.content),
SizedBox(height: 12),
Text(comment.timeAgo, style: TextStyle(color: Colors.grey, fontSize: 12)),
],
),
),
)5. 头像选择器
dart
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: avatarOptions.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => _selectAvatar(avatarOptions[index]),
child: CircleAvatar(
radius: 40,
backgroundColor: _selectedIndex == index
? Colors.blue.withOpacity(0.3)
: Colors.transparent,
child: CircleAvatar(
radius: 36,
backgroundImage: AssetImage(avatarOptions[index]),
),
),
);
},
)常见问题解决方案
1. 图片变形问题
dart
CircleAvatar(
backgroundImage: NetworkImage('avatar.jpg'),
child: ClipOval( // 确保圆形裁剪
child: Image.network(
'avatar.jpg',
fit: BoxFit.cover,
width: 80, // 需要指定宽高
height: 80,
),
),
)2. 加载状态处理
dart
CircleAvatar(
radius: 30,
child: FutureBuilder(
future: _loadAvatar(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Icon(Icons.error);
}
return Image.memory(snapshot.data!);
},
),
)3. 头像大小自适应
dart
LayoutBuilder(
builder: (context, constraints) {
double avatarSize = constraints.maxWidth > 600 ? 80 : 50;
return CircleAvatar(
radius: avatarSize,
backgroundImage: NetworkImage('avatar.jpg'),
);
},
)4. 文字自动缩放
dart
CircleAvatar(
radius: 40,
backgroundColor: Colors.blue,
child: FittedBox( // 文字自适应缩放
fit: BoxFit.scaleDown,
child: Text(
user.initials,
style: TextStyle(fontSize: 40), // 初始大字体
),
),
)CircleAvatar 实践建议
- 使用
CachedNetworkImage优化网络头像加载性能 - 对于未知用户,使用姓名首字母 + 随机背景色方案
- 使用
Hero动画实现头像详情页的平滑过渡 - 在暗色模式下调整背景色:
backgroundColor: Theme.of(context).colorScheme.surfaceVariant - 使用
Badge组件添加通知徽章 - 组合
Tooltip提供用户信息提示 - 重要用户头像添加
BoxShadow提升视觉层次
按钮组件(ElevatedButton / TextButton / OutlinedButton / IconButton 等)
Flutter 提供了丰富的按钮组件体系,满足不同场景下的交互需求。以下是完整的按钮组件分类与详解:
关键规则:Flutter 中按钮的“禁用态”通常是把
onPressed设为null。这会自动触发灰态/不可点击(并符合无障碍语义)。dartElevatedButton( onPressed: null, // 禁用 child: Text('提交'), )
按钮类型对比表
| 按钮类型 | 视觉风格 | 适用场景 | 核心组件 |
|---|---|---|---|
| 凸起按钮 | 有阴影和背景色 | 主要操作、强调操作 | ElevatedButton |
| 文本按钮 | 无背景、仅文字 | 次要操作、对话框操作 | TextButton |
| 描边按钮 | 带边框、无填充 | 中等重要性操作、取消操作 | OutlinedButton |
| 图标按钮 | 仅图标 | 工具栏、紧凑空间操作 | IconButton |
| 悬浮按钮 | 圆形悬浮按钮 | 主屏幕主要操作 | FloatingActionButton |
| 下拉按钮 | 带下拉菜单 | 选项选择 | DropdownButton |
| 弹出菜单按钮 | 三点菜单 | 更多操作菜单 | PopupMenuButton |
| 自定义按钮 | 完全自定义样式 | 品牌化设计、特殊交互 | InkWell/GestureDetector |
一、基础按钮组件
1. ElevatedButton (凸起按钮)
dart
ElevatedButton(
onPressed: () {
print('按钮被点击');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // 背景色
foregroundColor: Colors.white, // 文字/图标颜色
elevation: 5, // 阴影深度
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), // 内边距
shape: RoundedRectangleBorder( // 形状
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('确认提交'),
)2. TextButton (文本按钮)
dart
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: EdgeInsets.all(12),
),
child: const Text('取消操作'),
)3. OutlinedButton (描边按钮)
dart
OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
side: BorderSide(color: Colors.blue, width: 2), // 边框样式
shape: StadiumBorder(), // 圆角形状
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: const Text('查看详情'),
)4. IconButton (图标按钮)
dart
IconButton(
onPressed: () {},
icon: Icon(Icons.favorite),
iconSize: 30,
color: Colors.red,
tooltip: '收藏', // 长按提示
)5. FloatingActionButton (悬浮按钮)
dart
FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 6,
shape: CircleBorder(), // 圆形
child: Icon(Icons.add),
)二、高级按钮组件
1. DropdownButton (下拉按钮)
dart
String? _selectedValue = '选项1';
DropdownButton<String>(
value: _selectedValue,
onChanged: (newValue) {
setState(() => _selectedValue = newValue);
},
items: <String>['选项1', '选项2', '选项3']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
)2. PopupMenuButton (弹出菜单按钮)
dart
PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(value: 'edit', child: Text('编辑')),
PopupMenuItem(value: 'delete', child: Text('删除')),
],
onSelected: (value) {
if (value == 'delete') _deleteItem();
},
)3. ButtonBar (按钮组)
dart
ButtonBar(
alignment: MainAxisAlignment.end, // 对齐方式
children: [
TextButton(onPressed: () {}, child: Text('取消')),
ElevatedButton(onPressed: () {}, child: Text('确定')),
],
)三、按钮状态管理
1. 禁用状态
dart
ElevatedButton(
onPressed: isLoading ? null : _submitForm, // null表示禁用
child: isLoading
? CircularProgressIndicator(color: Colors.white)
: Text('提交'),
)2. 加载状态
dart
bool _isLoading = false;
ElevatedButton(
onPressed: _isLoading ? null : _fetchData,
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text('加载数据'),
)3. 按钮状态样式
dart
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.grey; // 禁用状态
}
if (states.contains(MaterialState.pressed)) {
return Colors.blue[700]!; // 按下状态
}
return Colors.blue; // 默认状态
},
),
),
// ...
)四、自定义按钮
1. 使用 InkWell 创建自定义按钮
dart
InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(8),
splashColor: Colors.blue[100], // 水波纹颜色
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue),
),
child: Text('自定义按钮'),
),
)2. 渐变按钮
dart
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero, // 清除默认内边距
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () {},
child: Ink(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(10),
),
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
child: Text('渐变按钮', style: TextStyle(color: Colors.white)),
),
)3. 图标+文字按钮
dart
ElevatedButton.icon(
onPressed: () {},
icon: Icon(Icons.cloud_upload),
label: Text('上传文件'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
),
)五、最佳实践指南
1. 按钮尺寸规范
dart
// 小尺寸按钮
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.all(8)),
child: Text('小按钮'),
)
// 中尺寸按钮(默认)
ElevatedButton(child: Text('中按钮'),)
// 大尺寸按钮
ElevatedButton(
style: ElevatedButton.styleFrom(padding: EdgeInsets.all(16)),
child: Text('大按钮'),
)2. 响应式按钮设计
dart
LayoutBuilder(
builder: (context, constraints) {
bool isMobile = constraints.maxWidth < 600;
return isMobile
? FloatingActionButton(onPressed: () {}, child: Icon(Icons.add))
: ElevatedButton.icon(
onPressed: () {},
icon: Icon(Icons.add),
label: Text('添加新项目'),
);
},
)3. 按钮主题统一
dart
MaterialApp(
theme: ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
),
),
),
)4. 按钮无障碍优化
dart
ElevatedButton(
onPressed: () {},
child: Text('确认'),
autofocus: true, // 自动获取焦点
focusNode: _focusNode, // 焦点控制
onFocusChange: (hasFocus) {
if (hasFocus) _announceAccessibility('确认按钮已聚焦');
},
)六、常见问题解决方案
1. 按钮点击无响应
dart
// 原因:onPressed为null
ElevatedButton(
onPressed: _isEnabled ? _handlePress : null, // 正确管理状态
// ...
)2. 按钮样式不生效
dart
// 解决方案:使用styleFrom代替已弃用属性
ElevatedButton(
style: ElevatedButton.styleFrom( // 正确方式
backgroundColor: Colors.red,
),
// ...
)3. 按钮尺寸异常
dart
// 解决方案:包裹约束容器
SizedBox(
width: double.infinity, // 撑满宽度
height: 50, // 固定高度
child: ElevatedButton(child: Text('登录')),
)4. 按钮水波纹效果超出边界
dart
Material(
borderRadius: BorderRadius.circular(10), // 设置裁剪边界
child: InkWell(
borderRadius: BorderRadius.circular(10), // 与Material一致
onTap: () {},
child: Container(
padding: EdgeInsets.all(16),
child: Text('自定义按钮'),
),
),
)七、实际应用场景
1. 表单提交按钮组
dart
Column(
children: [
// 表单字段...
ButtonBar(
alignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消'),
),
ElevatedButton(
onPressed: _submitForm,
child: Text('保存设置'),
),
],
),
],
)2. 购物车操作按钮
dart
Row(
children: [
IconButton(
icon: Icon(Icons.favorite_border),
onPressed: _toggleFavorite,
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
),
icon: Icon(Icons.shopping_cart),
label: Text('加入购物车'),
onPressed: _addToCart,
),
),
],
)3. 社交登录按钮组
dart
Column(
children: [
OutlinedButton.icon(
icon: Image.asset('assets/google.png', width: 24),
label: Text('使用Google登录'),
onPressed: _signInWithGoogle,
),
SizedBox(height: 12),
OutlinedButton.icon(
icon: Image.asset('assets/facebook.png', width: 24),
label: Text('使用Facebook登录'),
onPressed: _signInWithFacebook,
),
],
)4. 多功能悬浮按钮
dart
FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
child: Icon(Icons.add),
heroTag: 'main_fab', // 唯一标识
)
// 次级悬浮按钮
ScaleTransition(
scale: CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
child: FloatingActionButton(
heroTag: 'fab1', // 不同heroTag
mini: true, // 小型按钮
onPressed: () {},
child: Icon(Icons.camera),
),
)按钮实践建议
- 使用
ButtonStyle替代已弃用的按钮样式属性 - 对于重要操作,使用
ElevatedButton提高视觉优先级 - 在长列表中使用
NotificationListener优化滚动时按钮性能 - 使用
Focus和FocusTraversalGroup管理按钮焦点顺序 - 通过
MaterialStateProperty创建响应不同状态的按钮样式 - 在暗色模式下使用
ThemeData自动适配按钮颜色 - 为图标按钮添加
tooltip属性提升无障碍访问体验
Wrap 组件
Flutter 的 Wrap 组件是一种流式布局(Flow Layout)工具,用于在空间不足时自动换行排列子组件,特别适合标签云、动态列表等场景。以下是其核心特性和使用指南:
Wrap 自动换行原理
Row/Column 在空间不足时会溢出报错(黄黑警戒条),而 Wrap 会在当前行/列排不下时自动换到下一行/列。用 spacing 控制主轴间距、runSpacing 控制换行后的行间距。标签云、动态数量的按钮组首选 Wrap。
| 属性 | 说明 | 默认值 |
|---|---|---|
direction | 主轴方向(Axis.horizontal 水平 / Axis.vertical 垂直) | Axis.horizontal |
alignment | 主轴对齐方式(如 WrapAlignment.start、.center、.spaceEvenly) | WrapAlignment.start |
spacing | 主轴方向子组件的间距 | 0.0 |
runSpacing | 交叉轴方向行/列的间距(控制换行后的间隔) | 0.0 |
runAlignment | 交叉轴的行/列对齐方式(需父容器高度足够才生效) | WrapAlignment.start |
crossAxisAlignment | 子组件在交叉轴的对齐方式(如 WrapCrossAlignment.start) | WrapCrossAlignment.start |
children | 子组件列表 | 必填 |
关键特性:当主轴空间不足时,
Wrap会自动换行(水平布局换行、垂直布局换列),而Row/Column更容易出现溢出(RenderFlex overflowed)。
二、基本用法示例
水平标签布局(自动换行)
dart
Wrap(
spacing: 10.0, // 水平间距
runSpacing: 15.0, // 行间距
alignment: WrapAlignment.spaceEvenly,
children: List.generate(10, (index) =>
Chip(
label: Text("标签${index + 1}"),
backgroundColor: Colors.blue[100],
),
),
)垂直流式布局
dart
Wrap(
direction: Axis.vertical, // 垂直主轴
runSpacing: 20.0,
children: [
Icon(Icons.star),
Icon(Icons.favorite),
// 更多子组件...
],
)三、高级技巧
1. 响应式布局
根据屏幕宽度动态切换方向:
dart
Wrap(
direction: MediaQuery.of(context).size.width > 600
? Axis.horizontal
: Axis.vertical,
children: [/*...*/],
)2. 嵌套组合
嵌套 Wrap 实现复杂流式结构:
dart
Wrap(
children: [
Wrap( // 内层垂直Wrap
direction: Axis.vertical,
children: [Chip(label: Text("A")), Chip(label: Text("B"))],
),
Chip(label: Text("主内容区")),
],
)3. 与状态管理结合
动态添加/删除标签:
dart
List<String> tags = ["Flutter", "Dart"];
Wrap(
children: tags.map((tag) =>
InputChip(
label: Text(tag),
onDeleted: () => setState(() => tags.remove(tag)),
),
).toList(),
)四、常见问题解决
1. 换行后行对齐不生效
原因:runAlignment 需父容器有额外空间才能生效。
方案:为父容器设置足够高度:
dart
Container(
height: 300, // 明确高度
child: Wrap(runAlignment: WrapAlignment.end, children: [...]),
)2. 子组件间距异常
- 使用
spacing控制主轴间距,runSpacing控制行间距。 - 避免子组件自身外边距(如
Padding),优先用Wrap的间距属性。
3. 性能优化
- 子组件数量过大时,用
Wrap替代嵌套Row/Column避免布局溢出。 - 复杂子组件提取为
const或独立Widget减少重建。
五、应用场景
1. 标签选择器
dart
Wrap(
spacing: 8.0,
children: ["喜剧", "动作", "科幻"].map((genre) =>
FilterChip(
label: Text(genre),
selected: _selectedGenre == genre,
onSelected: (v) => setState(() => _selectedGenre = genre),
),
).toList(),
)2. 自适应按钮组
dart
Wrap(
runSpacing: 10.0,
children: [
ElevatedButton(onPressed: () {}, child: Text("保存")),
OutlinedButton(onPressed: () {}, child: Text("取消")),
// 更多按钮...
],
)3. 瀑布流图片布局
dart
Wrap(
spacing: 5.0,
runSpacing: 5.0,
children: imageUrls.map((url) =>
Image.network(url, width: 100, height: 100, fit: BoxFit.cover),
).toList(),
)六、对比其他布局
| 组件 | 特点 | 适用场景 |
|---|---|---|
| Wrap | 自动换行,简单易用 | 标签、动态内容流 |
| Flow | 精确控制子组件位置,性能更高但代码复杂 | 自定义复杂流式布局 |
| Row/Column | 单行/列布局,溢出报错 | 线性固定内容 |
Wrap vs Flow
多数场景下 Wrap 的自动换行已经足够;当你需要像素级布局控制(例如自定义排布算法)时,再考虑 Flow。
综合示例
dart
Wrap(
spacing: 12,
runSpacing: 16,
alignment: WrapAlignment.center,
children: [
_buildCategory("音乐", Icons.music_note),
_buildCategory("电影", Icons.movie),
// 添加更多分类...
],
)
Widget _buildCategory(String name, IconData icon) {
return Column(
children: [
Icon(icon, size: 36),
SizedBox(height: 8),
Text(name),
],
);
}通过灵活组合 Wrap 的属性,可轻松实现各类自适应布局,尤其适合内容动态生成的场景(如搜索关键词、用户兴趣标签等)。
BottomNavigationBar
BottomNavigationBar 底部导航栏
底部导航栏用于在 3~5 个主页面间切换,赋值给 Scaffold 的 bottomNavigationBar。核心是 currentIndex 记录当前选中项 + onTap 回调更新状态,通常配合 IndexedStack 或 PageView 切换页面内容。超过 4 项时需显式设置 type: BottomNavigationBarType.fixed 避免选中项放大位移。
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| items | List<BottomNavigationBarItem> | 必填 | 导航项集合(至少需要 2 个) |
| iconSize | double | 24.0 | 导航图标大小 |
| currentIndex | int | 0 | 当前选中项的索引 |
| onTap | ValueChanged<int> | - | 点击导航项的回调函数 |
| fixedColor | Color | 主题色 | 选中项的颜色 |
| type | BottomNavigationBarType | fixed | 导航栏类型:fixed 或 shifting |
| backgroundColor | Color | 主题背景色 | 导航栏背景颜色 |
| elevation | double | 8.0 | 导航栏阴影高度 |
| selectedFontSize | double | 14.0 | 选中项字体大小 |
| unselectedFontSize | double | 12.0 | 未选中项字体大小 |
| selectedItemColor | Color | fixedColor | 选中项颜色 |
| unselectedItemColor | Color | 主题未选中色 | 未选中项颜色 |
| selectedLabelStyle | TextStyle | - | 选中项标签样式 |
| unselectedLabelStyle | TextStyle | - | 未选中项标签样式 |
| showSelectedLabels | bool | true | 是否显示选中项标签 |
| showUnselectedLabels | bool | type == fixed | 是否显示未选中项标签 |
| mouseCursor | MouseCursor | SystemMouseCursors.click | 鼠标悬停效果 |
| enableFeedback | bool | true | 是否启用触觉反馈 |
| landscapeLayout | BottomNavigationBarLandscapeLayout | spread | 横屏布局模式 |
两种导航类型对比
| 特性 | Fixed 类型 | Shifting 类型 |
|---|---|---|
| 导航项宽度 | 固定相同宽度 | 动态变化宽度 |
| 文字标签 | 始终显示 | 仅选中项显示 |
| 背景颜色 | 统一背景色 | 选中项背景色变化 |
| 动画效果 | 简单 | 丰富动效 |
| 适用场景 | 项较多 (>3) | 项较少 (3-4) |
| 显示效果 | ![]() | ![]() |
完整使用示例
dart
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '底部导航示例',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final List<Widget> _pages = [
const Center(child: Text('首页', style: TextStyle(fontSize: 24))),
const Center(child: Text('搜索', style: TextStyle(fontSize: 24))),
const Center(child: Text('收藏', style: TextStyle(fontSize: 24))),
const Center(child: Text('个人', style: TextStyle(fontSize: 24))),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('底部导航示例')),
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) => setState(() => _currentIndex = index),
type: BottomNavigationBarType.fixed,
fixedColor: Colors.blue,
backgroundColor: Colors.white,
elevation: 10,
selectedFontSize: 14,
unselectedFontSize: 12,
showSelectedLabels: true,
showUnselectedLabels: true,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.search_outlined),
activeIcon: Icon(Icons.search),
label: '搜索',
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite_border),
activeIcon: Icon(Icons.favorite),
label: '收藏',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}高级用法示例
1. 添加徽章提示
dart
BottomNavigationBarItem(
icon: Badge(
label: Text('3'),
backgroundColor: Colors.red,
child: const Icon(Icons.notifications_none),
),
activeIcon: Badge(
label: Text('3'),
backgroundColor: Colors.red,
child: const Icon(Icons.notifications),
),
label: '通知',
),2. 横屏布局优化
dart
bottomNavigationBar: OrientationBuilder(
builder: (context, orientation) {
return BottomNavigationBar(
// 其他属性...
landscapeLayout: orientation == Orientation.landscape
? BottomNavigationBarLandscapeLayout.centered
: BottomNavigationBarLandscapeLayout.spread,
);
},
)3. 状态管理集成
dart
// 使用Provider管理状态
final navProvider = Provider<int>((ref) => 0);
// 在组件中
currentIndex: ref.watch(navProvider),
onTap: (index) => ref.read(navProvider.notifier).state = index,最佳实践建议
- 导航项数量:控制在 3-5 个之间,避免过多
- 标签文本:简洁明了(1-2 个单词)
- 图标选择:使用标准 Material 图标
- 视觉反馈:使用 activeIcon 提供状态变化
- 横屏适配:使用 landscapeLayout 优化横屏体验
- 无障碍支持:dart
BottomNavigationBarItem( icon: Icon(Icons.home), label: '首页', tooltip: '导航到首页', // 屏幕阅读器提示 )
FloatingActionButton
FloatingActionButton 悬浮按钮
FAB 是悬浮在页面右下角的圆形操作按钮,用于承载页面的主要操作(如新增、发消息)。赋值给 Scaffold 的 floatingActionButton,可通过 floatingActionButtonLocation 调整位置。一个页面建议只放一个 FAB,突出核心动作。
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onPressed | VoidCallback | 必填 | 按钮点击回调函数 |
| child | Widget | 通常为 Icon | 按钮内容(图标或文本) |
| tooltip | String | - | 长按提示文本(无障碍支持) |
| backgroundColor | Color | 主题色 | 按钮背景颜色 |
| foregroundColor | Color | 白色 | 前景色(图标/文字颜色) |
| elevation | double | 6.0 | 正常状态阴影高度 |
| highlightElevation | double | 12.0 | 按下状态阴影高度 |
| shape | ShapeBorder | CircleBorder() | 按钮形状(可自定义) |
| heroTag | Object | - | Hero 动画标识(多个 FAB 时需要) |
| mini | bool | false | 是否使用迷你尺寸 |
| isExtended | bool | false | 是否为扩展模式(带文字) |
| extendedIconLabelSpacing | double | 8.0 | 扩展模式图标与文字的间距 |
| extendedPadding | EdgeInsetsGeometry | EdgeInsets.symmetric(horizontal: 16) | 扩展模式内边距 |
基础使用示例
1. 基本悬浮按钮
dart
FloatingActionButton(
onPressed: () {
// 按钮点击逻辑
print('FAB 被点击');
},
child: const Icon(Icons.add),
tooltip: '添加新项目',
)2. 扩展型悬浮按钮(带文字)
dart
FloatingActionButton.extended(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('新建'),
backgroundColor: Colors.blue,
)3. 迷你尺寸按钮
dart
FloatingActionButton(
mini: true,
onPressed: () {},
child: const Icon(Icons.edit),
)4. 自定义形状按钮
dart
FloatingActionButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
onPressed: () {},
child: const Icon(Icons.star),
)在 Scaffold 中的定位
位置常量
dart
Scaffold(
floatingActionButton: FloatingActionButton(...),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
)| 位置常量 | 描述 |
|---|---|
centerFloat | 底部中心悬浮(默认) |
endFloat | 底部右端悬浮 |
centerDocked | 底部中心嵌入(与 BottomAppBar 配合) |
endDocked | 底部右端嵌入 |
startFloat | 底部左端悬浮 |
startDocked | 底部左端嵌入 |
centerTop | 顶部中心悬浮 |
endTop | 顶部右端悬浮 |
与 BottomAppBar 配合实现凸起效果
dart
Scaffold(
extendBody: true, // 重要:确保内容延伸到导航栏下方
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
shape: const CircularNotchedRectangle(),
child: Container(height: 56),
),
)高级用法与动画效果
1. 动态显示/隐藏动画
dart
bool _fabVisible = true;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: AnimatedOpacity(
opacity: _fabVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: FloatingActionButton(...),
),
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollUpdateNotification) {
// 滚动时隐藏,停止时显示
setState(() => _fabVisible = notification.scrollDelta! < 0);
}
return false;
},
child: ListView.builder(...),
),
);
}2. 多级悬浮按钮菜单
dart
bool _expanded = false;
Scaffold(
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_expanded) ...[
FloatingActionButton(
mini: true,
heroTag: 'btn1',
onPressed: () {},
child: const Icon(Icons.camera),
),
const SizedBox(height: 10),
FloatingActionButton(
mini: true,
heroTag: 'btn2',
onPressed: () {},
child: const Icon(Icons.image),
),
const SizedBox(height: 10),
],
FloatingActionButton(
heroTag: 'main',
onPressed: () => setState(() => _expanded = !_expanded),
child: AnimatedRotation(
turns: _expanded ? 0.125 : 0,
duration: const Duration(milliseconds: 300),
child: const Icon(Icons.add),
),
),
],
),
)3. 按钮变形动画
dart
bool _isExtended = false;
FloatingActionButton(
onPressed: () {
setState(() => _isExtended = !_isExtended);
},
isExtended: _isExtended,
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _isExtended
? const Text('保存更改')
: const SizedBox.shrink(),
),
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _isExtended
? const Icon(Icons.save)
: const Icon(Icons.edit),
),
)主题与样式定制
全局主题设置
dart
MaterialApp(
theme: ThemeData(
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
elevation: 8,
largeSizeConstraints: BoxConstraints.tightFor(width: 72, height: 72),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
),
)按钮尺寸类型
| 类型 | 尺寸 (直径) | 说明 |
|---|---|---|
| 标准 | 56.0 | 默认大小 |
| 迷你 | 40.0 | mini: true |
| 大型 | 72.0 | FloatingActionButton.large |
最佳实践与常见问题
最佳实践
- 单一主操作:每个屏幕只使用一个主要悬浮按钮
- 位置合理:避免遮挡重要内容
- 视觉层次:使用鲜艳颜色突出主要操作
- 响应式设计:在移动端和桌面端适配不同尺寸
- 无障碍支持:始终提供
tooltip属性
解决常见问题
多个 FAB 的 heroTag 冲突
dart
Stack(
children: [
Positioned(
bottom: 16,
right: 16,
child: FloatingActionButton(
heroTag: 'fab1', // 唯一标识
onPressed: () {},
child: const Icon(Icons.camera),
),
),
Positioned(
bottom: 16,
right: 80,
child: FloatingActionButton(
heroTag: 'fab2', // 唯一标识
onPressed: () {},
child: const Icon(Icons.image),
),
),
],
)按钮被键盘遮挡
dart
Scaffold(
resizeToAvoidBottomInset: false, // 防止键盘挤压
floatingActionButton: FloatingActionButton(...),
)自定义位置计算
dart
Scaffold(
floatingActionButton: FloatingActionButton(...),
floatingActionButtonLocation: (context, child) {
final mediaQuery = MediaQuery.of(context);
final bottomPadding = mediaQuery.padding.bottom;
return Offset(
mediaQuery.size.width - 72, // 右侧位置
mediaQuery.size.height - kBottomNavigationBarHeight - bottomPadding - 72,
);
},
)完整示例
dart
import 'package:flutter/material.dart';
void main() => runApp(const FABApp());
class FABApp extends StatelessWidget {
const FABApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FAB 示例',
theme: ThemeData(
primarySwatch: Colors.blue,
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
),
home: const FABDemoScreen(),
);
}
}
class FABDemoScreen extends StatefulWidget {
const FABDemoScreen({super.key});
@override
State<FABDemoScreen> createState() => _FABDemoScreenState();
}
class _FABDemoScreenState extends State<FABDemoScreen> {
bool _expanded = false;
bool _fabVisible = true;
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('悬浮按钮示例')),
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollUpdateNotification) {
// 滚动时隐藏,停止时显示
setState(() => _fabVisible = notification.scrollDelta! < 0);
}
return false;
},
child: ListView.builder(
itemCount: 50,
itemBuilder: (context, index) => ListTile(title: Text('项目 $index')),
),
),
floatingActionButton: AnimatedOpacity(
opacity: _fabVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if (_expanded) ...[
FloatingActionButton.small(
heroTag: 'counter',
onPressed: () => setState(() => _counter++),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.add, size: 18),
Text('$_counter', style: const TextStyle(fontSize: 12)),
],
),
),
const SizedBox(height: 12),
FloatingActionButton.small(
heroTag: 'info',
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('这是信息按钮')));
},
child: const Icon(Icons.info),
),
const SizedBox(height: 12),
],
FloatingActionButton(
heroTag: 'main',
onPressed: () => setState(() => _expanded = !_expanded),
child: AnimatedRotation(
turns: _expanded ? 0.125 : 0,
duration: const Duration(milliseconds: 300),
child: const Icon(Icons.menu),
),
),
],
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
);
}
}Drawer 组件全面指南
Drawer 侧滑抽屉结构
Drawer 是从屏幕侧边滑出的导航面板,赋值给 Scaffold 的 drawer(左侧)或 endDrawer(右侧)后,AppBar 会自动出现汉堡菜单图标。内部通常放 ListView + UserAccountsDrawerHeader + 若干 ListTile。点击项后用 Navigator.pop(context) 关闭抽屉再跳转。
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| backgroundColor | Color | 主题背景色 | 抽屉背景颜色 |
| elevation | double | 16.0 | 抽屉阴影高度 |
| width | double | 304.0 | 抽屉宽度 |
| shape | ShapeBorder | - | 抽屉形状(可自定义圆角) |
| semanticLabel | String | - | 无障碍语义标签 |
| child | Widget | 必填 | 抽屉内容(通常为 ListView) |
| clipBehavior | Clip | Clip.hardEdge | 内容裁剪方式 |
| surfaceTintColor | Color | - | 材料表面色调颜色(Material 3) |
基础使用示例
1. 基本抽屉实现
dart
Scaffold(
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text('应用名称', style: TextStyle(color: Colors.white, fontSize: 24)),
),
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
onTap: () => Navigator.pop(context),
),
],
),
),
appBar: AppBar(title: Text('主页面')),
body: Center(child: Text('内容区域')),
)常见坑:
打开抽屉时的
context:如果你在AppBar.leading里直接调用Scaffold.of(context).openDrawer(),经常会因为context不在Scaffold下而报错。推荐用Builder包一层:dartappBar: AppBar( leading: Builder( builder: (context) => IconButton( icon: const Icon(Icons.menu), onPressed: () => Scaffold.of(context).openDrawer(), ), ), )
2. 双抽屉实现(左侧+右侧)
dart
Scaffold(
drawer: Drawer(child: _buildLeftDrawer()), // 左侧抽屉
endDrawer: Drawer(child: _buildRightDrawer()), // 右侧抽屉
appBar: AppBar(
title: Text('双抽屉示例'),
leading: Builder(
builder: (context) => IconButton(
icon: Icon(Icons.menu),
onPressed: () => Scaffold.of(context).openDrawer(),
),
),
actions: [
Builder(
builder: (context) => IconButton(
icon: Icon(Icons.filter_list),
onPressed: () => Scaffold.of(context).openEndDrawer(),
),
),
],
),
body: // ...
)
Widget _buildLeftDrawer() {
return ListView(
children: [/* 导航菜单 */]
);
}
Widget _buildRightDrawer() {
return ListView(
children: [/* 过滤选项 */]
);
}高级用法与自定义
1. 自定义抽屉头部
dart
DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade700, Colors.blue.shade300],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage('avatar.jpg'),
),
SizedBox(height: 12),
Text('用户名', style: TextStyle(color: Colors.white, fontSize: 18)),
Text('user@example.com', style: TextStyle(color: Colors.white70)),
],
),
),2. 抽屉状态管理
dart
// 使用Provider管理抽屉状态
final drawerProvider = StateProvider((ref) => false);
// 打开/关闭抽屉
void toggleDrawer(BuildContext context) {
final isOpen = ref.read(drawerProvider);
if (isOpen) {
Scaffold.of(context).closeDrawer();
} else {
Scaffold.of(context).openDrawer();
}
ref.read(drawerProvider.notifier).state = !isOpen;
}
// 监听抽屉状态
Scaffold(
drawer: Drawer(
child: Listener(
onPointerMove: (event) => print('抽屉正在拖动'),
child: // ...
),
),
)3. 抽屉动画效果
dart
// 自定义抽屉打开动画
Scaffold(
drawer: Drawer(
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
transform: Matrix4.translationValues(
_drawerOpen ? 0 : -300, 0, 0),
child: // ...
),
),
)
// 抽屉项动画
ListTile(
leading: AnimatedRotation(
turns: _selectedItem == index ? 0.25 : 0,
duration: Duration(milliseconds: 300),
child: Icon(Icons.arrow_forward_ios),
),
title: Text('菜单项'),
),抽屉内容最佳实践
1. 导航菜单结构
dart
ListView(
padding: EdgeInsets.zero,
children: [
UserAccountsDrawerHeader( // 预定义头部
accountName: Text("用户名"),
accountEmail: Text("user@example.com"),
currentAccountPicture: CircleAvatar(
backgroundImage: NetworkImage("avatar.jpg"),
),
),
Divider(),
_buildDrawerItem(Icons.home, '首页', 0),
_buildDrawerItem(Icons.favorite, '收藏', 1),
_buildDrawerItem(Icons.history, '历史记录', 2),
Divider(),
_buildDrawerItem(Icons.settings, '设置', 3),
_buildDrawerItem(Icons.help, '帮助中心', 4),
Spacer(), // 将内容推到底部
AboutListTile(
icon: Icon(Icons.info),
applicationName: "应用名称",
applicationVersion: "1.0.0",
),
],
)
Widget _buildDrawerItem(IconData icon, String title, int index) {
return ListTile(
leading: Icon(icon, color: _selectedIndex == index
? Theme.of(context).colorScheme.primary
: null),
title: Text(title),
selected: _selectedIndex == index,
onTap: () {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
);
}2. 响应式抽屉设计
dart
LayoutBuilder(
builder: (context, constraints) {
bool isWideScreen = constraints.maxWidth > 600;
return Scaffold(
drawer: isWideScreen ? null : Drawer(child: _buildDrawerContent()),
body: Row(
children: [
if (isWideScreen) ...[
SizedBox(
width: 300,
child: Material(
elevation: 4,
child: _buildDrawerContent(),
),
),
],
Expanded(child: _buildMainContent()),
],
),
);
},
)3. 暗色模式适配
dart
Drawer(
backgroundColor: Theme.of(context).drawerTheme.backgroundColor,
child: ListView(
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
),
child: // ...
),
ListTile(
leading: Icon(Icons.dark_mode),
title: Text('暗色模式'),
trailing: Switch(
value: Theme.of(context).brightness == Brightness.dark,
onChanged: (value) => _toggleTheme(value),
),
),
],
),
)完整示例代码
dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() => runApp(const ProviderScope(child: MyApp()));
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '抽屉示例',
theme: ThemeData(
primarySwatch: Colors.blue,
drawerTheme: DrawerThemeData(
backgroundColor: Colors.white,
width: 300,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
),
darkTheme: ThemeData.dark().copyWith(
drawerTheme: DrawerThemeData(
backgroundColor: Colors.grey[900],
),
),
home: const MainScreen(),
);
}
}
class MainScreen extends ConsumerWidget {
const MainScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentIndex = ref.watch(navIndexProvider);
return Scaffold(
appBar: AppBar(
title: const Text('抽屉导航示例'),
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.menu),
onPressed: () => Scaffold.of(context).openDrawer(),
),
),
),
drawer: const AppDrawer(),
body: _buildPage(currentIndex),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) => ref.read(navIndexProvider.notifier).state = index,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.favorite), label: '收藏'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '个人'),
],
),
);
}
Widget _buildPage(int index) {
final pages = [
const Center(child: Text('首页内容')),
const Center(child: Text('收藏内容')),
const Center(child: Text('个人资料')),
];
return pages[index];
}
}
final navIndexProvider = StateProvider((ref) => 0);
class AppDrawer extends ConsumerWidget {
const AppDrawer({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentIndex = ref.watch(navIndexProvider);
return Drawer(
child: Column(
children: [
UserAccountsDrawerHeader(
accountName: const Text("张三"),
accountEmail: const Text("zhangsan@example.com"),
currentAccountPicture: CircleAvatar(
backgroundColor: Colors.white,
child: Text(
"张",
style: TextStyle(
fontSize: 24,
color: Theme.of(context).primaryColor,
),
),
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
_buildDrawerItem(Icons.home, '首页', 0, currentIndex, ref),
_buildDrawerItem(Icons.favorite, '收藏', 1, currentIndex, ref),
_buildDrawerItem(Icons.history, '历史记录', 2, currentIndex, ref),
const Divider(),
_buildDrawerItem(Icons.settings, '设置', 3, currentIndex, ref),
_buildDrawerItem(Icons.help, '帮助中心', 4, currentIndex, ref),
],
),
),
const AboutListTile(
icon: Icon(Icons.info),
applicationName: "抽屉导航示例",
applicationVersion: "1.0.0",
applicationIcon: Icon(Icons.apps),
),
],
),
);
}
Widget _buildDrawerItem(IconData icon, String title, int index,
int currentIndex, WidgetRef ref) {
final isSelected = currentIndex == index;
return ListTile(
leading: Icon(icon, color: isSelected
? Theme.of(context).colorScheme.primary
: null),
title: Text(title),
selected: isSelected,
onTap: () {
ref.read(navIndexProvider.notifier).state = index;
Navigator.pop(context);
},
trailing: isSelected
? const Icon(Icons.check, color: Colors.green)
: null,
);
}
}常见问题解决方案
1. 在无 Scaffold 上下文访问 Drawer
dart
// 使用 Builder 创建新上下文
Scaffold(
appBar: AppBar(
leading: Builder(
builder: (context) => IconButton(
icon: Icon(Icons.menu),
onPressed: () => Scaffold.of(context).openDrawer(),
),
),
),
drawer: Drawer(...),
)2. 抽屉背景透明问题
dart
Drawer(
backgroundColor: Colors.white.withOpacity(0.95), // 半透明白色
// 或者
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: // ...
),
)3. 抽屉内容滚动问题
dart
// 使用 SingleChildScrollView 或 ListView
Drawer(
child: SingleChildScrollView(
child: Column(
children: [/* 长内容 */],
),
),
)4. 抽屉状态监听
dart
// 使用 DrawerController
final drawerController = DrawerController();
Scaffold(
drawer: Drawer(
controller: drawerController,
child: // ...
),
)
// 添加监听
drawerController.addListener(() {
if (drawerController.isOpen) {
print('抽屉已打开');
} else {
print('抽屉已关闭');
}
});5. 全局控制抽屉
dart
// 使用 GlobalKey
final scaffoldKey = GlobalKey<ScaffoldState>();
Scaffold(
key: scaffoldKey,
drawer: Drawer(...),
)
// 在其他地方打开
scaffoldKey.currentState?.openDrawer();UserAccountsDrawerHeader 组件
UserAccountsDrawerHeader 是专门为 Drawer 设计的预置头部组件,用于展示用户账户信息,提供了一种快速创建美观用户信息区域的方式。
为什么用 UserAccountsDrawerHeader
它开箱即用地组合了头像(currentAccountPicture)、用户名(accountName)、邮箱(accountEmail)和背景,省去手写 DrawerHeader + Row/Column 布局的繁琐。作为 Drawer 的第一个子元素使用即可。
核心属性详解
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| accountName | Widget | 必填 | 用户名展示组件(通常为 Text) |
| accountEmail | Widget | 必填 | 用户邮箱展示组件 |
| currentAccountPicture | Widget | - | 当前账户头像(通常为 CircleAvatar) |
| otherAccountsPictures | List<Widget> | - | 其他账户头像列表 |
| decoration | Decoration | 主题色背景 | 头部装饰(可设置背景图/渐变) |
| margin | EdgeInsetsGeometry | EdgeInsets.only(bottom: 8.0) | 外边距 |
| onDetailsPressed | VoidCallback | - | 点击整个头部时的回调函数 |
| arrowColor | Color | 白色 | 右侧箭头的颜色 |
| accountNameTextStyle | TextStyle | 主题文字样式 | 用户名字体样式 |
| accountEmailTextStyle | TextStyle | 主题文字样式 | 邮箱字体样式 |
基础使用示例
1. 基本用户信息展示
dart
UserAccountsDrawerHeader(
accountName: Text("张三"),
accountEmail: Text("zhangsan@example.com"),
currentAccountPicture: CircleAvatar(
backgroundColor: Colors.white,
child: Text(
"张",
style: TextStyle(fontSize: 24, color: Colors.blue),
),
),
)2. 带背景图的头部
dart
UserAccountsDrawerHeader(
accountName: Text("李四"),
accountEmail: Text("lisi@example.com"),
currentAccountPicture: CircleAvatar(
backgroundImage: NetworkImage("https://example.com/avatar.jpg"),
),
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage("https://example.com/background.jpg"),
fit: BoxFit.cover,
),
),
)3. 多账户切换支持
dart
UserAccountsDrawerHeader(
accountName: Text("王五"),
accountEmail: Text("wangwu@company.com"),
currentAccountPicture: CircleAvatar(
backgroundImage: NetworkImage("https://example.com/work-avatar.jpg"),
),
otherAccountsPictures: [
CircleAvatar(
backgroundImage: NetworkImage("https://example.com/personal-avatar.jpg"),
),
CircleAvatar(
backgroundColor: Colors.grey,
child: Icon(Icons.add, color: Colors.white),
),
],
)高级用法与自定义
1. 添加点击交互
dart
UserAccountsDrawerHeader(
accountName: Text("赵六"),
accountEmail: Text("zhaoliu@example.com"),
currentAccountPicture: CircleAvatar(
child: Icon(Icons.person),
),
onDetailsPressed: () {
Navigator.pop(context); // 关闭抽屉
Navigator.push(context, MaterialPageRoute(
builder: (context) => ProfileScreen() // 跳转到个人资料页
));
},
)2. 自定义背景装饰
dart
UserAccountsDrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple.shade700, Colors.purple.shade300],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
// 其他属性...
)3. 完全自定义样式
dart
UserAccountsDrawerHeader(
accountName: Text(
"钱七",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
shadows: [Shadow(color: Colors.black, blurRadius: 2)],
),
),
accountEmail: Text(
"qianqi@example.com",
style: TextStyle(fontStyle: FontStyle.italic),
),
currentAccountPicture: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: CircleAvatar(
backgroundImage: NetworkImage("https://example.com/avatar.jpg"),
),
),
arrowColor: Colors.amber,
)在 Drawer 中的完整应用
dart
Drawer(
child: Column(
children: [
UserAccountsDrawerHeader(
accountName: Text("用户名"),
accountEmail: Text("user@example.com"),
currentAccountPicture: CircleAvatar(
backgroundImage: NetworkImage("avatar.jpg"),
),
decoration: BoxDecoration(
color: Colors.blue.shade700,
image: DecorationImage(
image: NetworkImage("background-pattern.jpg"),
fit: BoxFit.cover,
opacity: 0.2,
),
),
),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
onTap: () {/* ... */},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
onTap: () {/* ... */},
),
// 更多列表项...
],
),
),
AboutListTile(
icon: Icon(Icons.info),
applicationName: "应用名称",
applicationVersion: "1.0.0",
),
],
),
)响应式设计技巧
1. 根据屏幕尺寸调整布局
dart
LayoutBuilder(
builder: (context, constraints) {
bool isSmallScreen = constraints.maxWidth < 400;
return UserAccountsDrawerHeader(
accountName: Text("用户名",
style: TextStyle(fontSize: isSmallScreen ? 16 : 20)),
accountEmail: Text("user@example.com",
style: TextStyle(fontSize: isSmallScreen ? 12 : 14)),
currentAccountPicture: CircleAvatar(
radius: isSmallScreen ? 24 : 30,
// ...
),
);
},
)2. 横竖屏适配
dart
OrientationBuilder(
builder: (context, orientation) {
return UserAccountsDrawerHeader(
margin: orientation == Orientation.portrait
? EdgeInsets.only(bottom: 8.0)
: EdgeInsets.zero,
// 其他属性...
);
},
)暗色模式适配
dart
UserAccountsDrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
),
accountName: Text("用户名",
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer)),
accountEmail: Text("user@example.com",
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.8))),
arrowColor: Theme.of(context).colorScheme.onPrimaryContainer,
// ...
)最佳实践与技巧
头像优化:
- 使用
CachedNetworkImage加载网络头像 - 提供占位符和错误处理
dartcurrentAccountPicture: CachedNetworkImage( imageUrl: "avatar.jpg", imageBuilder: (context, imageProvider) => CircleAvatar( backgroundImage: imageProvider, ), placeholder: (context, url) => CircularProgressIndicator(), errorWidget: (context, url, error) => Icon(Icons.error), ),- 使用
动态数据绑定:
dartUserAccountsDrawerHeader( accountName: Text(userProvider.name), accountEmail: Text(userProvider.email), currentAccountPicture: CircleAvatar( backgroundImage: NetworkImage(userProvider.avatarUrl), ), )无障碍支持:
dartUserAccountsDrawerHeader( accountName: Semantics( label: "用户名", child: Text("实际用户名"), ), // ... )性能优化:
dartconst UserAccountsDrawerHeader( accountName: Text("固定用户名"), accountEmail: Text("固定邮箱"), // ... )
替代方案:自定义 DrawerHeader
如果需要更复杂的布局,可以直接使用 DrawerHeader 自定义:
dart
DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade800, Colors.blue.shade600],
),
),
child: Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage("avatar.jpg"),
),
SizedBox(width: 16),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("用户名", style: TextStyle(color: Colors.white, fontSize: 18)),
SizedBox(height: 4),
Text("user@example.com", style: TextStyle(color: Colors.white70)),
SizedBox(height: 8),
OutlinedButton(
onPressed: () {},
child: Text("查看资料", style: TextStyle(color: Colors.white)),
style: OutlinedButton.styleFrom(
side: BorderSide(color: Colors.white),
),
),
],
),
],
),
)完整示例
dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:provider/provider.dart';
void main() => runApp(
ChangeNotifierProvider(
create: (context) => UserProfile(),
child: const MyApp(),
),
);
class UserProfile with ChangeNotifier {
String name = "张三";
String email = "zhangsan@example.com";
String avatarUrl = "https://example.com/avatar.jpg";
void updateProfile(String newName, String newEmail) {
name = newName;
email = newEmail;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '用户抽屉头部示例',
theme: ThemeData(
primarySwatch: Colors.blue,
drawerTheme: DrawerThemeData(
backgroundColor: Colors.white,
),
),
darkTheme: ThemeData.dark(),
home: const MainScreen(),
);
}
}
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('用户抽屉示例')),
drawer: const AppDrawer(),
body: const Center(child: Text('主内容区域')),
);
}
}
class AppDrawer extends StatelessWidget {
const AppDrawer({super.key});
@override
Widget build(BuildContext context) {
final userProfile = Provider.of<UserProfile>(context);
final isDark = Theme.of(context).brightness == Brightness.dark;
return Drawer(
child: Column(
children: [
UserAccountsDrawerHeader(
accountName: Text(
userProfile.name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
accountEmail: Text(
userProfile.email,
style: TextStyle(
color: isDark ? Colors.white70 : Colors.black54,
),
),
currentAccountPicture: GestureDetector(
onTap: () => _editProfile(context, userProfile),
child: Hero(
tag: 'user-avatar',
child: CircleAvatar(
backgroundColor: Colors.grey.shade200,
child: CachedNetworkImage(
imageUrl: userProfile.avatarUrl,
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
),
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.person),
),
),
),
),
decoration: BoxDecoration(
color: isDark
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).primaryColor,
image: const DecorationImage(
image: NetworkImage("https://example.com/background-pattern.png"),
fit: BoxFit.cover,
opacity: 0.1,
),
),
onDetailsPressed: () => _editProfile(context, userProfile),
arrowColor: isDark ? Colors.white : Colors.white,
),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
_buildDrawerItem(Icons.home, '首页'),
_buildDrawerItem(Icons.favorite, '收藏'),
_buildDrawerItem(Icons.history, '历史记录'),
const Divider(),
_buildDrawerItem(Icons.settings, '设置'),
_buildDrawerItem(Icons.help, '帮助中心'),
],
),
),
const AboutListTile(
icon: Icon(Icons.info),
applicationName: "用户抽屉示例",
applicationVersion: "1.0.0",
),
],
),
);
}
Widget _buildDrawerItem(IconData icon, String title) {
return ListTile(
leading: Icon(icon),
title: Text(title),
onTap: () {},
);
}
void _editProfile(BuildContext context, UserProfile userProfile) {
Navigator.pop(context); // 关闭抽屉
showModalBottomSheet(
context: context,
builder: (context) {
String tempName = userProfile.name;
String tempEmail = userProfile.email;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('编辑资料', style: Theme.of(context).textTheme.titleLarge),
TextField(
decoration: const InputDecoration(labelText: '用户名'),
onChanged: (value) => tempName = value,
controller: TextEditingController(text: userProfile.name),
),
TextField(
decoration: const InputDecoration(labelText: '邮箱'),
onChanged: (value) => tempEmail = value,
controller: TextEditingController(text: userProfile.email),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
userProfile.updateProfile(tempName, tempEmail);
Navigator.pop(context);
},
child: const Text('保存'),
),
],
),
);
},
);
}
}UserAccountsDrawerHeader 提供了快速构建专业用户信息区域的能力,特别适合需要展示用户账户信息的应用场景。通过合理利用其属性和自定义选项,可以创建出既美观又实用的抽屉头部,提升应用的整体用户体验。
AppBar
AppBar 是 Flutter 应用的标准顶部栏组件,常用于显示标题、操作按钮和导航元素。
AppBar 结构速记
AppBar 从左到右三块:leading(左侧返回/菜单)+ title(标题)+ actions(右侧操作按钮组);底部 bottom 可挂 TabBar。赋值给 Scaffold.appBar 即自动占据顶部并处理状态栏留白。
核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title | Widget | - | 标题组件(通常为 Text) |
actions | List<Widget> | [] | 右侧操作按钮列表 |
bottom | PreferredSizeWidget | - | 底部组件(通常放置 TabBar) |
backgroundColor | Color | 主题色 | 背景颜色 |
elevation | double | 4.0 | 阴影高度 |
leading | Widget | - | 左侧导航元素(如返回按钮) |
基本用法
dart
AppBar(
title: Text('我的应用'),
actions: [
IconButton(icon: Icon(Icons.search), onPressed: () {}),
IconButton(icon: Icon(Icons.settings), onPressed: () {}),
],
)TabBar
TabBar 是一个水平标签导航组件,通常嵌入在 AppBar 的 bottom 属性中。
核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
tabs | List<Widget> | 必填 | Tab 组件列表 |
controller | TabController | - | 标签控制器 |
isScrollable | bool | false | 标签过多时是否可滚动 |
indicatorColor | Color | 主题色 | 指示器颜色 |
labelColor | Color | 主题色 | 选中标签文字颜色 |
unselectedLabelColor | Color | 灰色 | 未选中标签文字颜色 |
indicatorWeight | double | 2.0 | 指示器厚度 |
Tab 组件属性
dart
Tab(
icon: Icon(Icons.flight), // 图标
text: "航班", // 文字
child: Text('航班'), // 自定义子组件
)TabBarView
TabBarView 用于显示与 TabBar 标签对应的内容区域,支持左右滑动切换。
核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
children | List<Widget> | 必填 | 标签对应的内容组件列表 |
controller | TabController | - | 必须与 TabBar 的控制器一致 |
physics | ScrollPhysics | - | 滚动物理特性 |
特殊属性
- physics:
NeverScrollableScrollPhysics():禁用滑动BouncingScrollPhysics():弹性滚动效果
AppBar + TabBar + TabBarView 三者协同使用
AppBar + TabBar + TabBarView 构成完整的标签式布局体系。
三者协同关系
TabBar(标签栏)放在 AppBar.bottom,TabBarView(内容区)放在 Scaffold.body,二者通过同一个 TabController 联动——点标签切内容、滑内容切标签。用 DefaultTabController 包裹可自动创建并共享控制器,无需手动管理。
text
DefaultTabController(提供共享控制器)
│
▼
Scaffold
├── AppBar
│ └── bottom: TabBar(标签栏) ──┐
│ │ 同一 TabController 联动
└── body: TabBarView(内容区) ◄──────┘基本结构
dart
Scaffold(
appBar: AppBar(
bottom: TabBar(), // 嵌入TabBar
),
body: TabBarView(), // 内容区域
)完整示例
dart
import 'package:flutter/material.dart';
void main() => runApp(const TabLayoutDemo());
class TabLayoutDemo extends StatelessWidget {
const TabLayoutDemo({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController( // 自动管理Tab状态
length: 3, // 标签数量
child: Scaffold(
appBar: AppBar(
title: const Text('旅行预订'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.flight), text: "航班"),
Tab(icon: Icon(Icons.train), text: "火车"),
Tab(icon: Icon(Icons.directions_car), text: "自驾"),
],
indicatorColor: Colors.white,
labelColor: Colors.white,
),
),
body: TabBarView(
children: [
_buildPage("航班预订页面", Colors.blue[100]!),
_buildPage("火车票查询", Colors.green[100]!),
_buildPage("租车服务", Colors.orange[100]!),
],
),
),
),
);
}
Widget _buildPage(String msg, Color color) {
return Container(
color: color,
child: Center(
child: Text(
msg,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
);
}
}高级用法
1. 手动控制 TabController
dart
class _TabDemoState extends State<TabDemo>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabController,
tabs: [/*...*/],
),
),
body: TabBarView(
controller: _tabController,
children: [/*...*/],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _tabController.animateTo(2), // 切换到第三个标签
child: Icon(Icons.swap_horiz),
),
);
}
}2. 自定义标签样式
dart
TabBar(
tabs: [/*...*/],
indicator: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(10),
),
labelStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold
),
unselectedLabelColor: Colors.grey,
)3. 带图标的标签
dart
TabBar(
tabs: [
Tab(icon: Icon(Icons.chat), text: "聊天"),
Tab(icon: Icon(Icons.contacts), text: "联系人"),
Tab(icon: Icon(Icons.settings), text: "设置"),
],
)常见问题
1. 标签与内容数量不一致
错误信息: 'package:flutter/src/material/tab_controller.dart': Failed assertion: 'length == tabs.length'
解决方案: 确保 DefaultTabController 的 length 与 TabBar 的 tabs 数量和 TabBarView 的 children 数量完全一致。
2. 嵌套滚动冲突
现象:
TabBarView 内部有可滚动组件时,滑动冲突。
解决方案:
dart
TabBarView(
physics: NeverScrollableScrollPhysics(), // 禁用TabBarView滑动
children: [
ListView(/*...*/), // 内部组件自行处理滚动
],
)3. 动态更新标签
需求: 运行时添加/删除标签
解决方案:
dart
// 1. 使用StatefulWidget管理状态
// 2. 在TabController初始化时添加监听
_tabController = TabController(length: _tabs.length, vsync: this);
_tabController.addListener(_handleTabChange);
// 3. 更新数据后重建
setState(() {
_tabs.add(Tab(text: "新标签"));
_tabController = TabController(
length: _tabs.length,
vsync: this,
initialIndex: _tabController.index
);
});4. 自定义标签布局
使用 CustomScrollView + SliverAppBar 实现复杂布局:
dart
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(/*...*/),
bottom: TabBar(/*...*/),
),
SliverFillRemaining(
child: TabBarView(/*...*/),
)
],
)TabBar 状态管理
使用 DefaultTabController 可简化状态管理,适合大多数基础场景。需要复杂交互时使用手动控制方式(自建 TabController)。
PreferredSize 组件
为什么需要 PreferredSize
AppBar.bottom 只接受 PreferredSizeWidget(自带首选高度的组件)。当你想放一个普通 Widget 作为 AppBar 底部区域时,必须用 PreferredSize 包一层并通过 preferredSize 指定高度,否则无法确定布局尺寸。
PreferredSize可以改变AppBar的高度(常用于AppBar.bottom的自定义区域)
1. 自定义 AppBar 底部组件
当需要创建自定义的 AppBar 底部组件时,必须使用 PreferredSize 包装:
dart
AppBar(
title: Text('自定义 AppBar'),
bottom: PreferredSize(
preferredSize: Size.fromHeight(80.0),
child: Container(
height: 80,
color: Colors.blue,
child: Center(
child: Text('自定义底部组件'),
),
),
),
)2. 创建自定义 TabBar
实现自定义样式的 TabBar:
dart
PreferredSize(
preferredSize: Size.fromHeight(60.0),
child: Container(
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(30),
),
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TabBar(
tabs: [
Tab(text: '首页'),
Tab(text: '发现'),
Tab(text: '我的'),
],
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.purpleAccent,
),
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
),
),
)3. 固定高度的标题栏
创建固定高度的自定义标题栏:
dart
PreferredSize(
preferredSize: Size.fromHeight(100.0),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Padding(
padding: const EdgeInsets.only(top: 40.0),
child: Center(
child: Text(
'应用标题',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
),
)完整示例:自定义 AppBar
dart
import 'package:flutter/material.dart';
void main() => runApp(const CustomAppBarDemo());
class CustomAppBarDemo extends StatelessWidget {
const CustomAppBarDemo({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(150.0), // 自定义高度
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.deepPurple, Colors.purpleAccent],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, 4),
)
],
),
child: SafeArea(
child: Column(
children: [
// 顶部状态栏区域
SizedBox(height: MediaQuery.of(context).padding.top),
// 标题区域
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.menu, color: Colors.white),
onPressed: () {},
),
const Expanded(
child: Text(
'我的应用',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
IconButton(
icon: const Icon(Icons.search, color: Colors.white),
onPressed: () {},
),
],
),
),
// 标签栏
const PreferredSize(
preferredSize: Size.fromHeight(48.0),
child: TabBar(
tabs: [
Tab(text: '推荐'),
Tab(text: '热门'),
Tab(text: '最新'),
],
indicatorColor: Colors.white,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
),
),
],
),
),
),
),
body: TabBarView(
children: [
_buildPage('推荐内容', Colors.blue[100]!),
_buildPage('热门内容', Colors.green[100]!),
_buildPage('最新内容', Colors.orange[100]!),
],
),
),
);
}
Widget _buildPage(String msg, Color color) {
return Container(
color: color,
child: Center(
child: Text(
msg,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
);
}
}高级用法
响应式 PreferredSize
根据屏幕尺寸动态调整首选大小:
dart
PreferredSize(
preferredSize: Size(
double.infinity,
MediaQuery.of(context).size.height * 0.15,
),
child: // ...
)与 SliverAppBar 结合使用
在 CustomScrollView 中使用:
dart
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('弹性标题'),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(50.0),
child: TabBar(
tabs: [/* ... */],
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('项目 $index')),
childCount: 20,
),
),
],
)阴影效果被裁剪
问题:自定义 AppBar 的阴影效果被裁剪
解决方案:在容器外部添加阴影,而不是在内部
dart
PreferredSize(
preferredSize: Size.fromHeight(100),
child: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(color: Colors.black26, blurRadius: 10, spreadRadius: 2)
],
),
child: // 实际内容
),
)GetX Dialog(对话框)
为什么用 GetX 弹窗
原生 showDialog 需要 BuildContext,而 GetX 的 Get.dialog/Get.defaultDialog/Get.bottomSheet 无需 context 即可全局调用,适合在业务层、控制器中随处弹窗。前提是入口用 GetMaterialApp 而非 MaterialApp,用 Get.back() 关闭。
基础对话框
默认对话框
dart
Get.defaultDialog(
title: "提示",
middleText: "确定要删除这条记录吗?",
textConfirm: "确定",
textCancel: "取消",
confirmTextColor: Colors.white,
onConfirm: () {
// 确认操作
Get.back(result: true); // 返回结果
},
onCancel: () {
// 取消操作
Get.back(result: false);
},
);核心参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title | String | - | 对话框标题 |
titleStyle | TextStyle | - | 标题样式 |
middleText | String | - | 主要内容文本 |
middleTextStyle | TextStyle | - | 内容文本样式 |
textConfirm | String | "确认" | 确认按钮文本 |
textCancel | String | "取消" | 取消按钮文本 |
onConfirm | VoidCallback | - | 确认按钮回调 |
onCancel | VoidCallback | - | 取消按钮回调 |
confirmTextColor | Color | - | 确认按钮文字颜色 |
cancelTextColor | Color | - | 取消按钮文字颜色 |
backgroundColor | Color | Colors.white | 对话框背景色 |
radius | double | 20.0 | 对话框圆角半径 |
barrierDismissible | bool | true | 点击背景是否关闭对话框 |
自定义对话框
完全自定义对话框
dart
Get.dialog(
AlertDialog(
title: Text("自定义对话框"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(decoration: InputDecoration(labelText: "输入内容")),
SizedBox(height: 20),
Text("这是自定义内容区域"),
],
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text("取消"),
),
TextButton(
onPressed: () => Get.back(result: "提交结果"),
child: Text("提交"),
),
],
),
barrierDismissible: false,
);使用 GetBuilder 自定义
dart
class CustomDialog extends StatelessWidget {
const CustomDialog({super.key});
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.info, color: Colors.blue, size: 50),
SizedBox(height: 20),
Text("自定义对话框内容", style: TextStyle(fontSize: 18)),
SizedBox(height: 20),
GetBuilder<DialogController>(
init: DialogController(),
builder: (controller) {
return ElevatedButton(
onPressed: controller.updateCount,
child: Text("点击计数: ${controller.count}"),
);
},
)
],
),
),
);
}
}
class DialogController extends GetxController {
int count = 0;
void updateCount() {
count++;
update();
}
}
// 使用
Get.dialog(CustomDialog());对话框类型
加载对话框
dart
// 显示加载框
void showLoading() {
Get.dialog(
Center(
child: CircularProgressIndicator(),
),
barrierDismissible: false,
);
}
// 关闭加载框
void hideLoading() {
if (Get.isDialogOpen!) Get.back();
}底部对话框
dart
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Icons.camera),
title: Text("拍照"),
onTap: () {
Get.back(result: "camera");
},
),
ListTile(
leading: Icon(Icons.photo),
title: Text("从相册选择"),
onTap: () {
Get.back(result: "gallery");
},
),
ListTile(
leading: Icon(Icons.close),
title: Text("取消"),
onTap: () => Get.back(),
),
],
),
),
);全屏对话框
dart
Get.dialog(
Scaffold(
appBar: AppBar(
title: Text("全屏对话框"),
leading: IconButton(
icon: Icon(Icons.close),
onPressed: () => Get.back(),
),
),
body: Center(
child: Text("这是一个全屏对话框"),
),
),
barrierDismissible: false,
);对话框控制
检查对话框状态
dart
if (Get.isDialogOpen!) {
print("对话框已打开");
}关闭对话框
dart
Get.back(); // 关闭当前对话框
Get.back(result: "返回值"); // 关闭并返回结果等待对话框结果
dart
final result = await Get.dialog(MyDialog());
print("对话框返回结果: $result");全局对话框管理
dart
// 显示对话框
Get.dialog(MyDialog(), id: "myDialog");
// 检查特定对话框
if (Get.isDialogOpen("myDialog")) {
// 关闭特定对话框
Get.back(id: "myDialog");
}高级用法
对话框动画
dart
Get.dialog(
MyDialog(),
transitionDuration: Duration(milliseconds: 500),
transitionCurve: Curves.easeInOut,
barrierColor: Colors.black54,
);对话框主题
dart
Get.dialog(
AlertDialog(
backgroundColor: Get.theme.cardColor,
title: Text("主题对话框", style: Get.textTheme.headline6),
content: Text("使用当前主题样式", style: Get.textTheme.bodyText2),
),
);国际化对话框
dart
Get.dialog(
AlertDialog(
title: Text("dialog_title".tr),
content: Text("dialog_message".tr),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text("cancel".tr),
),
TextButton(
onPressed: () => Get.back(result: true),
child: Text("confirm".tr),
),
],
),
);对话框路由中间件
dart
class DialogMiddleware extends GetMiddleware {
@override
RouteSettings? redirect(String? route) {
if (needsLogin && !isLoggedIn) {
Get.dialog(LoginDialog()); // 显示登录对话框
return null; // 阻止原始路由
}
return super.redirect(route);
}
}常见问题
1. 对话框无法显示
原因:
- 未使用
GetMaterialApp - 在
build方法中同步调用
解决方案:
dart
// 正确调用方式
void showMyDialog() {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.dialog(MyDialog());
});
}2. 对话框内状态管理
问题: 对话框内状态无法更新
解决方案:
dart
// 使用 GetBuilder 或 GetX
GetBuilder<DialogController>(
init: DialogController(),
builder: (controller) {
return Text("计数: ${controller.count}");
},
)3. 嵌套对话框关闭问题
问题: Get.back() 关闭了所有对话框
解决方案:
dart
// 为每个对话框指定唯一ID
Get.dialog(Dialog1(), id: "dialog1");
Get.dialog(Dialog2(), id: "dialog2");
// 关闭指定对话框
Get.back(id: "dialog1");4. 对话框位置不正确
问题: 对话框出现在屏幕底部
原因: 在 Scaffold 的 body 中调用
解决方案:
dart
// 在按钮事件中调用,而不是在 build 方法中
ElevatedButton(
onPressed: () => Get.dialog(MyDialog()),
child: Text("打开对话框"),
)5. 对话框内存泄漏
问题: 控制器未正确释放
解决方案:
dart
Get.dialog(
GetBuilder<DialogController>(
init: DialogController(),
dispose: (_) => Get.delete<DialogController>(), // 明确释放
builder: (controller) => ...,
),
);6. 对话框自适应大小
dart
Get.dialog(
Dialog(
insetPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 40),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 600, // 最大宽度
maxHeight: MediaQuery.of(Get.context!).size.height * 0.8,
),
child: ...,
),
),
);组件选型速查
- 展示内容:单块盒子 →
Container;文字 →Text;图片 →Image;圆形头像 →CircleAvatar;图标 →Icon/字体图标。 - 排列布局:横排 →
Row;竖排 →Column;自动换行 →Wrap;层叠定位 →Stack+Positioned/Align;间距 →Padding。 - 列表网格:长列表/大数据 →
ListView.builder;网格 →GridView。 - 卡片区块:
Card(自带圆角阴影)。 - 页面骨架:顶部栏
AppBar、底部导航BottomNavigationBar、侧滑抽屉Drawer、悬浮按钮FloatingActionButton、标签页TabBar+TabBarView(同一TabController联动)。 - 交互反馈:按钮组件、
GetX Dialog(无需 context 全局弹窗)。
一句话:Flutter 一切皆 Widget,UI 就是把这些组件像积木一样组合成树。 记住「主轴/交叉轴」和「组合优于继承」,布局问题迎刃而解。

