Flutter-Notebook 实战:用 BottomAppBar 实现底部导航与居中浮动按钮(FloatingActionButton)
Flutter-Notebook 实战:用 BottomAppBar 实现底部导航与居中浮动按钮(FloatingActionButton)
在 Flutter 的 Material 设计中,BottomAppBar 与 FloatingActionButton 的 centerDocked 定位组合,是打造“悬浮球 + 底部导航栏”交互形态的经典方案。本文以 Flutter-Notebook 仓库中的 bottom_appbar_demo 为例,讲解如何用 BottomAppBar 构建底部导航、通过 CircularNotchedRectangle 为导航栏开出缺口、并让浮动按钮“嵌入”缺口居中停靠。读完本文,你将掌握一套可直接复用的底部导航实现代码,以及两种浮动按钮外观定制思路。
一、demo 概览:README 的核心内容
该示例位于仓库 mecury_project/example/bottom_appbar_demo 目录下,README 的定位非常明确:
使用 bottomAppbar 进行底部导航,并居中浮动按钮的 demo
也就是说,这个 demo 集中解决两个问题:
- 用
BottomAppBar承载底部导航项(图标按钮),替代传统的BottomNavigationBar; - 让
FloatingActionButton居中停靠在导航栏的缺口内,形成“dock(停靠)”效果。
示例提供了两种视觉风格(样例 1 与样例 2),二者的导航逻辑一致,主要差异在浮动按钮的外观与阴影处理上,非常适合对比学习。
二、工程结构与运行方式
示例的 lib 目录结构如下:
| 文件 | 职责 |
|---|---|
| main.dart | 应用入口,MaterialApp 的 home 指向 BottomAppBarDemo |
| bottom_appBar_demo.dart | 样例 1:默认样式的居中浮动按钮 + 缺口导航栏 |
| bottom_appBar_demo2.dart | 样例 2:自定义外观的浮动按钮 + 带阴影导航栏 |
| each_view.dart | 通用子页面,用于展示当前选中的 Tab 内容 |
入口 main.dart 很简单:创建一个 MaterialApp,主题色为 Colors.blue,home 直接指向 BottomAppBarDemo。默认展示的是样例 2(import 'bottom_appBar_demo2.dart'),若想切换为样例 1,把 import 改为 bottom_appBar_demo.dart 即可:
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: BottomAppBarDemo(),
);
}
}
运行方式与普通 Flutter 工程一致:进入示例目录后执行 flutter run 即可在模拟器或真机上查看效果。需要说明的是,pubspec.yaml 中声明的 Dart SDK 约束为 ">=2.0.0-dev.68.0 <3.0.0",这是一个早期 Flutter 时代的工程,若在现代 SDK 环境下运行遇到约束冲突,可结合当前 Flutter 版本调整该约束范围(仓库内代码使用的均为 Material 基础组件,未引入第三方依赖)。
三、样例 1:CircularNotchedRectangle 缺口 + 默认样式浮动按钮
样例 1 的完整实现位于 bottom_appBar_demo.dart,核心代码如下:
return new Scaffold(
body: _eachView[_index],
floatingActionButton: new FloatingActionButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context){
return EachView('New Page');
}));
},
tooltip: 'Increment',
child: new Icon(Icons.add),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
color: Colors.lightBlue,
shape: CircularNotchedRectangle(),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
IconButton(
icon: Icon(Icons.near_me),
color: Colors.white,
onPressed: () {
setState(() { _index = 0; });
},
),
IconButton(
icon: Icon(Icons.edit_location),
color: Colors.white,
onPressed: () {
setState(() { _index = 1; });
},
),
],
),
),
);
这段代码里有三处关键配置:
1. floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked
这是“居中停靠”的关键。centerDocked 会让浮动按钮水平居中、垂直方向“骑”在底部导航栏的上沿。与之相对的还有 endDocked(右侧停靠)、centerFloat(居中悬浮于导航栏上方)等取值,本例选择了最典型的居中停靠形态。
2. shape: CircularNotchedRectangle()
BottomAppBar 的 shape 参数用于描述导航栏的形状。CircularNotchedRectangle 是一个预置的“圆形缺口”实现,它会在导航栏上沿挖出一个半圆形凹槽,正好容纳居中的 FloatingActionButton,形成按钮与导航栏“咬合”的视觉效果。在 Flutter 2.x 之后,官方推荐使用 NotchedShape 的 CircularNotchedRectangle 扩展实现,但其缺口原理与本例一致。
3. 导航项用 Row + IconButton 自由拼装
BottomAppBar 本身不限定导航项组件,你可以在 child 中自由排列任意 Widget。本例使用 Row + mainAxisAlignment: MainAxisAlignment.spaceAround 将两个 IconButton(Icons.near_me 与 Icons.edit_location)均匀分布在导航栏两侧,点击后通过 setState 切换 _index,从而切换 body 中展示的页面:
List<Widget> _eachView;
int _index = 0;
@override
void initState() {
super.initState();
_eachView = List();
_eachView..add(EachView('home'))..add(EachView('me'));
}
同时,浮动按钮的 onPressed 里通过 Navigator.of(context).push(MaterialPageRoute(...)) 跳转到一个新页面 EachView('New Page'),演示了“浮动按钮 + 路由跳转”的组合用法。
四、样例 2:自定义 FAB 外观并控制阴影
样例 2 的实现位于 bottom_appBar_demo2.dart。与样例 1 相比,导航结构完全相同,差异集中在浮动按钮的视觉定制上:
return Scaffold(
body: _eachView[_index],
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.lightBlueAccent,
elevation: 0,
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) {
return EachView('New Page');
}));
},
tooltip: 'Increment',
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
child: SizedBox(
height: 48,
width: 48,
child: Icon(Icons.add, color: Colors.lightBlueAccent,)),
),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
elevation: 8,
color: Colors.lightBlueAccent,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
// 与样例 1 相同的两个 IconButton
],
),
),
);
样例 2 与样例 1 的核心差异可以整理成一张对照表:
| 对比项 | 样例 1 | 样例 2 |
|---|---|---|
BottomAppBar 背景色 |
Colors.lightBlue |
Colors.lightBlueAccent |
BottomAppBar 阴影(elevation) |
未显式设置(默认) | 8 |
| FAB 背景色 | 默认主题色 | Colors.lightBlueAccent |
| FAB 阴影(elevation) | 默认 | 0(去除阴影) |
| FAB 内容 | 直接使用 Icon(Icons.add) |
DecoratedBox 白色圆形内嵌 SizedBox(48×48) 图标 |
| FAB 整体形态 | Material 默认圆形 | 蓝底白圆、视觉更“扁平” |
阴影控制的思路值得注意:样例 2 将 FAB 的 elevation 设为 0,避免按钮悬浮感过强;同时给 BottomAppBar 设置 elevation: 8,让底部导航栏相对页面内容产生清晰的层级投影。一压一抬之间,视觉重心更集中在导航栏与按钮的“缺口咬合”区域。
FAB 的白色圆形内芯通过 DecoratedBox + BoxDecoration(shape: BoxShape.circle, color: Colors.white) 实现,并用 SizedBox(width: 48, height: 48) 固定内部图标区域尺寸,最终呈现出“浅蓝底、白色圆环、浅蓝加号图标”的双层圆环效果,比样例 1 的纯色默认按钮更精致。
五、通用子页面:each_view.dart
两个样例共用的子页面位于 each_view.dart,它只负责显示一个带标题的 AppBar 页面:
class EachView extends StatefulWidget {
String _title;
EachView(this._title);
@override
_EachViewState createState() => _EachViewState();
}
class _EachViewState extends State<EachView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget._title),
backgroundColor: Colors.lightBlueAccent,
),
);
}
}
这个组件的设计非常简洁:构造函数接收一个标题字符串,页面主体仅渲染一个 AppBar。它在 demo 中承担两种角色——作为底部导航切换的 Tab 页面(EachView('home')、EachView('me')),以及作为浮动按钮跳转的目标页面(EachView('New Page'))。实际项目中完全可以用真实的业务页面替换它,而 BottomAppBar 的导航框架无需改动。
六、关键参数速查
结合两个样例的源码,将 BottomAppBar 与 FloatingActionButton 的核心参数整理如下:
BottomAppBar
| 参数 | 取值/作用 | 样例中的应用 |
|---|---|---|
color |
导航栏背景色,任意 Color |
样例 1 用 Colors.lightBlue,样例 2 用 Colors.lightBlueAccent |
elevation |
阴影高度,数值越大投影越明显;设为 0 可去除阴影 | 样例 2 设为 8 |
shape |
导航栏形状,CircularNotchedRectangle() 可开出容纳 FAB 的圆形缺口 |
样例 1 使用,样例 2 未显式设置 |
child |
导航栏内容,可自由组合任意 Widget | 样例均为 Row + 两个 IconButton |
notchMargin |
缺口与 FAB 之间的留白间距(未在本例中使用,可按需扩展调整) | — |
FloatingActionButton
| 参数 | 取值/作用 | 样例中的应用 |
|---|---|---|
floatingActionButtonLocation(Scaffold 属性) |
控制 FAB 的位置,centerDocked 实现居中停靠 |
两个样例均使用 centerDocked |
backgroundColor |
按钮背景色 | 样例 2 使用 Colors.lightBlueAccent |
elevation |
按钮阴影高度,0 表示无阴影 | 样例 2 设为 0 |
tooltip |
长按提示文案 | 两个样例均为 'Increment' |
child |
按钮内容,常用 Icon |
样例 1 直接放图标;样例 2 用 DecoratedBox 嵌套 |
七、小结
通过 bottom_appbar_demo 这个示例,可以提炼出一套通用的“BottomAppBar 底部导航”实现套路:
- 结构:
Scaffold.body存放页面容器,Scaffold.bottomNavigationBar放置BottomAppBar,Scaffold.floatingActionButton+centerDocked让浮动按钮居中停靠; - 缺口:
BottomAppBar(shape: CircularNotchedRectangle())为导航栏开出圆形凹槽,与 FAB 形成咬合; - 导航:在
BottomAppBar.child中用Row+IconButton自由拼装导航项,通过setState切换索引完成 Tab 切换; - 定制:通过
elevation、backgroundColor、DecoratedBox等自由调整按钮与导航栏的视觉风格。
该示例只依赖 Flutter Material 内置组件、零第三方依赖(见 pubspec.yaml),非常适合作为学习 BottomAppBar 交互模式或在此基础上二次开发的起点。

