Flutter-Notebook 实战:用 BottomAppBar 实现底部导航与居中浮动按钮(FloatingActionButton)

原创2026-10-14 21:41:09609 阅读
文章标签:示例工程

Flutter-Notebook 实战:用 BottomAppBar 实现底部导航与居中浮动按钮(FloatingActionButton)

在 Flutter 的 Material 设计中,BottomAppBar 与 FloatingActionButton 的 centerDocked 定位组合,是打造“悬浮球 + 底部导航栏”交互形态的经典方案。本文以 Flutter-Notebook 仓库中的 bottom_appbar_demo 为例,讲解如何用 BottomAppBar 构建底部导航、通过 CircularNotchedRectangle 为导航栏开出缺口、并让浮动按钮“嵌入”缺口居中停靠。读完本文,你将掌握一套可直接复用的底部导航实现代码,以及两种浮动按钮外观定制思路。

样例1:CircularNotchedRectangle 缺口 + 默认样式 FloatingActionButton

样例2:自定义圆形 FAB 外观并控制阴影

一、demo 概览:README 的核心内容

该示例位于仓库 mecury_project/example/bottom_appbar_demo 目录下,README 的定位非常明确:

使用 bottomAppbar 进行底部导航,并居中浮动按钮的 demo

也就是说,这个 demo 集中解决两个问题:

  1. 用 BottomAppBar 承载底部导航项(图标按钮),替代传统的 BottomNavigationBar;
  2. 让 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 交互模式或在此基础上二次开发的起点。

登录后查看全文
Flutter-Notebook