5.7 页面骨架(Scaffold)
本节介绍《Flutter实战·第二版》第5章的 5.7 节内容,讲解 Scaffold 组件的使用,这是 Material Design 中最重要的页面骨架组件。
📚 学习内容
- Scaffold 基础 - Material Design 页面骨架
- AppBar - 顶部导航栏
- Drawer - 左右抽屉菜单
- FloatingActionButton - 悬浮操作按钮
- BottomNavigationBar - 底部导航栏
- BottomAppBar - 打洞效果的底部导航栏
- 完整示例 - 综合所有功能
🎯 核心概念
Scaffold 是什么?
Scaffold 是 Material Design 中的页面骨架,它提供了一套标准的页面结构,包括:
- 顶部导航栏(AppBar)
- 左右抽屉菜单(Drawer)
- 页面主体(Body)
- 悬浮按钮(FloatingActionButton)
- 底部导航栏(BottomNavigationBar/BottomAppBar)
使用 Scaffold 可以快速构建符合 Material Design 规范的页面。
📖 基础用法
Scaffold 完整示例
class MyPage extends StatefulWidget {
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
// 1. 顶部导航栏
appBar: AppBar(
title: Text('我的页面'),
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.share),
onPressed: () {},
),
],
),
// 2. 左侧抽屉
drawer: Drawer(
child: ListView(...),
),
// 3. 右侧抽屉
endDrawer: Drawer(
child: ListView(...),
),
// 4. 页面主体
body: Center(
child: Text('页面内容'),
),
// 5. 悬浮按钮
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
// 6. 悬浮按钮位置
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
// 7. 底部导航栏
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
);
}
}
🎨 主要组件详解
1. AppBar(导航栏)
AppBar 是页面顶部的导航栏,提供标题、导航按钮和操作按钮。
基本属性
AppBar(
// 标题
title: Text('标题'),
// 是否居中
centerTitle: true,
// 左侧按钮(默认是返回按钮)
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {},
),
// 右侧操作按钮
actions: [
IconButton(icon: Icon(Icons.search), onPressed: () {}),
IconButton(icon: Icon(Icons.share), onPressed: () {}),
],
// 背景色
backgroundColor: Colors.blue,
// 阴影高度
elevation: 4.0,
// 底部组件(如 TabBar)
bottom: TabBar(
tabs: [
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
],
),
)
AppBar 示例
class MyAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('我的应用'),
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
// 打开抽屉
Scaffold.of(context).openDrawer();
},
),
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {},
),
],
),
body: Center(child: Text('内容')),
);
}
}
2. Drawer(抽屉菜单)
Drawer 是从屏幕左侧或右侧滑出的侧边菜单。
基本用法
Scaffold(
// 左侧抽屉
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text('头部'),
),
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
onTap: () {
Navigator.pop(context);
},
),
],
),
),
// 右侧抽屉
endDrawer: Drawer(
child: ListView(...),
),
)
打开抽屉的方式
// 方式1:手势滑动(自动支持)
// 从屏幕左边缘向右滑动 → 打开 drawer
// 从屏幕右边缘向左滑动 → 打开 endDrawer
// 方式2:通过 ScaffoldState
Scaffold.of(context).openDrawer(); // 打开左侧抽屉
Scaffold.of(context).openEndDrawer(); // 打开右侧抽屉
// 方式3:使用 Builder 确保 context 正确
Builder(
builder: (context) => IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
)
自定义抽屉菜单
class MyDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: MediaQuery.removePadding(
context: context,
removeTop: true, // 移除顶部默认留白
child: Column(
children: [
// 头部
Container(
height: 150,
color: Colors.blue,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 40,
child: Icon(Icons.person, size: 50),
),
SizedBox(height: 8),
Text(
'用户名',
style: TextStyle(color: Colors.white, fontSize: 18),
),
],
),
),
),
// 菜单列表
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
onTap: () => Navigator.pop(context),
),
Divider(),
ListTile(
leading: Icon(Icons.info),
title: Text('关于'),
onTap: () => Navigator.pop(context),
),
],
),
),
],
),
),
);
}
}
3. FloatingActionButton(悬浮按钮)
FloatingActionButton(简称 FAB)是 Material Design 中的悬浮操作按钮,通常用于页面的主要操作。
基本用法
Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
tooltip: '添加',
),
)
FAB 位置
通过 floatingActionButtonLocation 属性指定 FAB 的位置:
Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
)
常用位置:
| 位置 | 说明 |
|---|---|
endFloat | 右下角(默认) |
startFloat | 左下角 |
centerFloat | 中间底部 |
endDocked | 右下角停靠在 BottomAppBar |
centerDocked | 中间底部停靠在 BottomAppBar |
endTop | 右上角 |
startTop | 左上角 |
扩展 FAB
FloatingActionButton.extended(
onPressed: () {},
icon: Icon(Icons.add),
label: Text('添加'),
)
小尺寸 FAB
FloatingActionButton.small(
onPressed: () {},
child: Icon(Icons.add),
)
⚠️ Hero 动画冲突
如果页面中有多个 FAB,需要指定不同的 heroTag:
FloatingActionButton(
heroTag: 'fab1', // 必须唯一
onPressed: () {},
child: Icon(Icons.add),
)
FloatingActionButton(
heroTag: 'fab2', // 不同的 tag
onPressed: () {},
child: Icon(Icons.edit),
)
4. BottomNavigationBar(底部导航栏)
BottomNavigationBar 是标准的 Material Design 底部导航栏。
基本用法
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _selectedIndex = 0;
final List<Widget> _pages = [
HomePage(),
MessagePage(),
ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.message),
label: '消息',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
),
],
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
);
}
}
属性说明
BottomNavigationBar(
// 导航项列表
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
activeIcon: Icon(Icons.home_filled), // 选中时的图标
label: '首页',
backgroundColor: Colors.blue, // 背景色(type 为 shifting 时有效)
),
],
// 当前选中的索引
currentIndex: 0,
// 点击回调
onTap: (index) {},
// 类型
type: BottomNavigationBarType.fixed, // fixed 或 shifting
// 选中项颜色
selectedItemColor: Colors.blue,
// 未选中项颜色
unselectedItemColor: Colors.grey,
// 背景色
backgroundColor: Colors.white,
// 阴影高度
elevation: 8.0,
// 是否显示选中标签
showSelectedLabels: true,
// 是否显示未选中标签
showUnselectedLabels: true,
)
BottomNavigationBarType
fixed:固定模式,所有项平分空间(默认,适用于 2-3 项)shifting:切换模式,选中项会变大,有动画效果(适用于 4-5 项)
5. BottomAppBar(打洞效果)
BottomAppBar 可以配合 FAB 实现"打洞"效果。
基本用法
Scaffold(
body: Center(child: Text('内容')),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
// FAB 位置必须是 centerDocked 才能实现打洞
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
color: Colors.white,
shape: CircularNotchedRectangle(), // 圆形打洞
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(icon: Icon(Icons.home), onPressed: () {}),
SizedBox(width: 40), // 中间留空给 FAB
IconButton(icon: Icon(Icons.person), onPressed: () {}),
],
),
),
)
shape 属性
BottomAppBar 的 shape 属性决定洞的外形:
CircularNotchedRectangle():圆形洞(最常用)- 也可以自定义
NotchedShape实现其他形状(如钻石形)
打洞位置
打洞的位置取决于 FloatingActionButton 的位置:
// 中间打洞
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked
// 右侧打洞
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked
🔍 获取 ScaffoldState
方式1:Scaffold.of(context)
// 在 Scaffold 的子组件中
ElevatedButton(
onPressed: () {
Scaffold.of(context).openDrawer(); // 打开抽屉
},
child: Text('打开抽屉'),
)
方式2:使用 Builder
当在 Scaffold 的同一个 build 方法中使用时,需要用 Builder 包裹:
Scaffold(
appBar: AppBar(
leading: Builder(
builder: (context) => IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer(); // 正确的 context
},
),
),
),
drawer: Drawer(...),
body: Text('内容'),
)
方式3:使用 GlobalKey
class MyPage extends StatelessWidget {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState!.openDrawer();
},
),
),
drawer: Drawer(...),
body: Text('内容'),
);
}
}
💡 实战技巧
1. 显示 SnackBar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('这是一条消息'),
duration: Duration(seconds: 2),
action: SnackBarAction(
label: '撤销',
onPressed: () {},
),
),
);
2. 移除 Drawer 默认留白
Drawer(
child: MediaQuery.removePadding(
context: context,
removeTop: true, // 移除顶部留白
removeBottom: true, // 移除底部留白
child: ListView(...),
),
)
3. 底部导航栏切换页面
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
final List<Widget> _pages = [
Page1(),
Page2(),
Page3(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages, // 使用 IndexedStack 保持页面状态
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
);
}
}
4. 自定义 AppBar 高度
AppBar(
title: Text('标题'),
toolbarHeight: 80, // 自定义高度(默认 56)
)
⚠️ 注意事项
1. Context 问题
在 Scaffold 的同一个 build 方法中调用 Scaffold.of(context) 时,context 是错误的,需要使用 Builder:
// ❌ 错误
Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer(); // context 不对
},
),
),
)
// ✅ 正确
Scaffold(
appBar: AppBar(
leading: Builder(
builder: (context) => IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer(); // context 正确
},
),
),
),
)
2. BottomNavigationBar 项数限制
type: fixed模式:适用于 2-3 个项type: shifting模式:适用于 4-5 个项- 超过 5 个项时,考虑使用其他导航方式
3. FAB 的 heroTag 冲突
多个 FAB 时必须设置不同的 heroTag,否则会有 Hero 动画冲突。
📊 完整示例
class CompleteScaffoldDemo extends StatefulWidget {
@override
_CompleteScaffoldDemoState createState() => _CompleteScaffoldDemoState();
}
class _CompleteScaffoldDemoState extends State<CompleteScaffoldDemo> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
// 1. AppBar
appBar: AppBar(
title: Text('完整示例'),
centerTitle: true,
actions: [
IconButton(icon: Icon(Icons.search), onPressed: () {}),
IconButton(icon: Icon(Icons.share), onPressed: () {}),
],
),
// 2. 左侧 Drawer
drawer: Drawer(
child: ListView(
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text('左侧菜单'),
),
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
onTap: () => Navigator.pop(context),
),
],
),
),
// 3. 右侧 Drawer
endDrawer: Drawer(
child: ListView(
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.green),
child: Text('右侧菜单'),
),
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
onTap: () => Navigator.pop(context),
),
],
),
),
// 4. Body
body: Center(
child: Text('页面 $_selectedIndex'),
),
// 5. FloatingActionButton
floatingActionButton: FloatingActionButton(
heroTag: 'complete_fab',
onPressed: () {},
child: Icon(Icons.add),
),
// 6. BottomNavigationBar
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: '消息'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
);
}
}
🎓 练习题
练习1:实现带搜索的 AppBar
目标: 创建一个可切换搜索模式的 AppBar
要求:
- 默认显示标题和搜索图标
- 点击搜索图标后 AppBar 变成搜索框
- 可以取消搜索返回普通模式
💡 查看答案
class SearchAppBarPage extends StatefulWidget {
const SearchAppBarPage({super.key});
@override
State<SearchAppBarPage> createState() => _SearchAppBarPageState();
}
class _SearchAppBarPageState extends State<SearchAppBarPage> {
bool _isSearching = false;
final TextEditingController _searchController = TextEditingController();
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: _isSearching
? TextField(
controller: _searchController,
autofocus: true,
decoration: const InputDecoration(
hintText: '搜索...',
border: InputBorder.none,
hintStyle: TextStyle(color: Colors.white70),
),
style: const TextStyle(color: Colors.white),
onSubmitted: (value) {
print('搜索: $value');
},
)
: const Text('我的应用'),
actions: [
if (_isSearching)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_isSearching = false;
_searchController.clear();
});
},
)
else
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
setState(() {
_isSearching = true;
});
},
),
],
),
body: Center(
child: Text('搜索模式: $_isSearching'),
),
);
}
}
练习2:实现带未读消息数的 BottomNavigationBar
目标: 创建一个底部导航栏,在图标上显示未读消息数徽章
要求:
- 3-4 个导航项
- 消息页显示未读数量徽章
- 点击后清除未读数
💡 查看答案
class BadgeBottomNavPage extends StatefulWidget {
const BadgeBottomNavPage({super.key});
@override
State<BadgeBottomNavPage> createState() => _BadgeBottomNavPageState();
}
class _BadgeBottomNavPageState extends State<BadgeBottomNavPage> {
int _selectedIndex = 0;
int _unreadCount = 5;
// 创建带徽章的图标
Widget _buildIconWithBadge(IconData icon, int count) {
return Stack(
clipBehavior: Clip.none,
children: [
Icon(icon),
if (count > 0)
Positioned(
right: -8,
top: -8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
count > 99 ? '99+' : count.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('带徽章的底部导航')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('当前页面: $_selectedIndex'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
_unreadCount += 1;
});
},
child: const Text('增加未读消息'),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
if (index == 1) {
// 点击消息页时清除未读数
_unreadCount = 0;
}
});
},
items: [
const BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: _buildIconWithBadge(Icons.message, _unreadCount),
label: '消息',
),
const BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
练习3:实现电商应用首页布局
目标: 创建一个完整的电商应用首页,包含所有常用 Scaffold 组件
要求:
- AppBar:搜索框 + 购物车图标(带徽章)
- Drawer:用户信息 + 菜单列表
- Body:商品列表
- FloatingActionButton:快速发布按钮(居中停靠)
- BottomNavigationBar:4 个导航项
💡 查看答案
class EcommerceHomePage extends StatefulWidget {
const EcommerceHomePage({super.key});
@override
State<EcommerceHomePage> createState() => _EcommerceHomePageState();
}
class _EcommerceHomePageState extends State<EcommerceHomePage> {
int _selectedIndex = 0;
int _cartCount = 3;
final List<String> _pageTitles = ['首页', '分类', '购物车', '我的'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Container(
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: TextField(
decoration: InputDecoration(
hintText: '搜索商品',
prefixIcon: const Icon(Icons.search, color: Colors.grey),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
),
),
),
actions: [
Stack(
children: [
IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
setState(() => _selectedIndex = 2);
},
),
if (_cartCount > 0)
Positioned(
right: 8,
top: 8,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
_cartCount.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
textAlign: TextAlign.center,
),
),
),
],
),
],
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
UserAccountsDrawerHeader(
accountName: const Text('张三'),
accountEmail: const Text('zhangsan@example.com'),
currentAccountPicture: const CircleAvatar(
backgroundColor: Colors.white,
child: Icon(Icons.person, size: 40, color: Colors.blue),
),
decoration: BoxDecoration(
color: Colors.blue[700],
),
),
ListTile(
leading: const Icon(Icons.favorite),
title: const Text('我的收藏'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.history),
title: const Text('浏览历史'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.local_shipping),
title: const Text('我的订单'),
onTap: () => Navigator.pop(context),
),
const Divider(),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('设置'),
onTap: () => Navigator.pop(context),
),
],
),
),
body: IndexedStack(
index: _selectedIndex,
children: [
// 首页 - 商品列表
GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: 20,
itemBuilder: (context, index) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
color: Colors.grey[300],
child: const Center(child: Icon(Icons.image, size: 50)),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('商品 ${index + 1}'),
Text(
'¥${(index + 1) * 99}.00',
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
);
},
),
// 分类
Center(child: Text(_pageTitles[1], style: TextStyle(fontSize: 24))),
// 购物车
Center(child: Text(_pageTitles[2], style: TextStyle(fontSize: 24))),
// 我的
Center(child: Text(_pageTitles[3], style: TextStyle(fontSize: 24))),
],
),
floatingActionButton: FloatingActionButton(
heroTag: 'ecommerce_fab',
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('快速发布商品')),
);
},
child: const Icon(Icons.add),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
shape: const CircularNotchedRectangle(),
notchMargin: 6,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavButton(Icons.home, '首页', 0),
_buildNavButton(Icons.category, '分类', 1),
const SizedBox(width: 48), // FAB 占位
_buildNavButton(Icons.shopping_cart, '购物车', 2),
_buildNavButton(Icons.person, '我的', 3),
],
),
),
);
}
Widget _buildNavButton(IconData icon, String label, int index) {
final isSelected = _selectedIndex == index;
return InkWell(
onTap: () => setState(() => _selectedIndex = index),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected ? Colors.blue : Colors.grey,
),
Text(
label,
style: TextStyle(
fontSize: 12,
color: isSelected ? Colors.blue : Colors.grey,
),
),
],
),
),
);
}
}