Flutter:如何实现底部导航栏的深凹槽居中FAB
解决Flutter底部导航栏FAB凹槽深度不足的问题
问题描述
需要实现带有深凹槽的底部导航栏,让中间的搜索图标(FAB)嵌入到深凹槽中。当前使用BottomAppBar的CircularNotchedRectangle形状,凹槽深度无法达到设计图要求。可参考animated_notch_bottom_bar: ^1.0.3包,但无需为所有导航项添加动画,仅需保证中间搜索图标处于深凹槽内。
现有代码
class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @override ConsumerState<ConsumerStatefulWidget> createState() => _HomeScreenState(); } class _HomeScreenState extends ConsumerState<HomeScreen> { int _selectedIndex = 2; static const List<Widget> _widgetOptions = <Widget>[ Text('1 Page', style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, color: Colors.amber)), Text('2 Page', style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, color: Colors.amber)), SearchDashboard(), Text('3 Page', style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, color: Colors.amber)), Text('4 Page', style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, color: Colors.amber)), ]; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, body: Center( child: _widgetOptions.elementAt(_selectedIndex), ), floatingActionButton: SizedBox( width: 52, height: 52, child: FloatingActionButton( isExtended: true, onPressed: () { _onItemTapped(2); }, child: SvgPicture.asset( AssetPaths.home_search, ), backgroundColor: context.color.primary.cyan500, shape: CircleBorder(), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, bottomNavigationBar: BottomAppBar( height: 86, surfaceTintColor: context.color.grayScale.grey11, notchMargin: 8, color: context.color.grayScale.grey11, shape: const CircularNotchedRectangle(), clipBehavior: Clip.antiAlias, child: Container( color: Colors.transparent, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ _buildNavItem(0, AssetPaths.home2, 'My Gyms'), _buildNavItem(1, AssetPaths.scanner, 'NFC check'), SizedBox( width: 50, ), _buildNavItem(3, AssetPaths.support, 'Support'), _buildNavItem(4, AssetPaths.profile, 'Profile'), ], ), ), ), ); } Widget _buildNavItem(int index, String icon, String label) { return GestureDetector( onTap: () => _onItemTapped(index), child: SizedBox( width: 60, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.only(bottom: 2), child: SvgPicture.asset( icon, color: _selectedIndex == index ? context.color.grayScale.grey01 : context.color.grayScale.grey07, ), ), Text( label, style: context.textTheme.cap5.copyWith( color: _selectedIndex == index ? context.color.grayScale.grey01 : context.color.grayScale.grey07), ), ], ), ), ); } }
解决方案
方法一:自定义BottomAppBar形状
默认的CircularNotchedRectangle凹槽半径固定,通过自定义形状类可以自由调整凹槽深度:
class DeepCircularNotchedRectangle extends NotchedShape { const DeepCircularNotchedRectangle(); @override Path getOuterPath(Rect host, Rect? guest) { if (guest == null || !host.overlaps(guest)) return Path()..addRect(host); // 调整凹槽半径为FAB直径的1.2倍,加深凹槽效果 final double notchRadius = guest.width * 1.2; final double notchCenterX = guest.center.dx; final double notchCenterY = host.bottom - notchRadius; return Path() ..moveTo(host.left, host.top) ..lineTo(notchCenterX - notchRadius, host.top) // 绘制左侧圆弧 ..arcTo( Rect.fromCenter( center: Offset(notchCenterX, notchCenterY), width: notchRadius * 2, height: notchRadius * 2, ), math.pi, math.pi, false, ) ..lineTo(host.right, host.top) ..lineTo(host.right, host.bottom) ..lineTo(host.left, host.bottom) ..close(); } }
在BottomAppBar中替换为自定义形状:
bottomNavigationBar: BottomAppBar( height: 86, surfaceTintColor: context.color.grayScale.grey11, notchMargin: 0, // 移除默认边距,让凹槽更贴合FAB color: context.color.grayScale.grey11, shape: const DeepCircularNotchedRectangle(), // 使用自定义深凹槽形状 clipBehavior: Clip.antiAlias, child: Container( color: Colors.transparent, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ _buildNavItem(0, AssetPaths.home2, 'My Gyms'), _buildNavItem(1, AssetPaths.scanner, 'NFC check'), SizedBox(width: 60), // 调整中间空白宽度匹配FAB尺寸 _buildNavItem(3, AssetPaths.support, 'Support'), _buildNavItem(4, AssetPaths.profile, 'Profile'), ], ), ), ),
方法二:使用animated_notch_bottom_bar包快速实现
不想自定义形状的话,可以直接用animated_notch_bottom_bar包,关闭多余动画只保留深凹槽效果:
- 添加依赖到
pubspec.yaml:
dependencies: animated_notch_bottom_bar: ^1.0.3
- 修改代码使用该组件:
class _HomeScreenState extends ConsumerState<HomeScreen> { final _pageController = PageController(initialPage: 2); int _selectedIndex = 2; final List<Widget> _widgetOptions = <Widget>[ Text('1 Page', style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, color: Colors.amber)), Text('2 Page', style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, color: Colors.amber)), SearchDashboard(), Text('3 Page', style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, color: Colors.amber)), Text('4 Page', style: TextStyle( fontSize: 35, fontWeight: FontWeight.bold, color: Colors.amber)), ]; @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, body: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), // 禁止页面滑动切换 children: _widgetOptions, ), bottomNavigationBar: AnimatedNotchBottomBar( pageController: _pageController, notchColor: context.color.grayScale.grey11, color: context.color.grayScale.grey11, showLabel: true, notchRadius: 28, // 调整凹槽半径实现深度效果 bottomBarItems: [ BottomBarItem( inActiveItem: SvgPicture.asset( AssetPaths.home2, color: context.color.grayScale.grey07, ), activeItem: SvgPicture.asset( AssetPaths.home2, color: context.color.grayScale.grey01, ), itemLabel: 'My Gyms', ), BottomBarItem( inActiveItem: SvgPicture.asset( AssetPaths.scanner, color: context.color.grayScale.grey07, ), activeItem: SvgPicture.asset( AssetPaths.scanner, color: context.color.grayScale.grey01, ), itemLabel: 'NFC check', ), BottomBarItem( inActiveItem: SizedBox.shrink(), // 中间项留空,用FAB替代 activeItem: SizedBox.shrink(), itemLabel: '', ), BottomBarItem( inActiveItem: SvgPicture.asset( AssetPaths.support, color: context.color.grayScale.grey07, ), activeItem: SvgPicture.asset( AssetPaths.support, color: context.color.grayScale.grey01, ), itemLabel: 'Support', ), BottomBarItem( inActiveItem: SvgPicture.asset( AssetPaths.profile, color: context.color.grayScale.grey07, ), activeItem: SvgPicture.asset( AssetPaths.profile, color: context.color.grayScale.grey01, ), itemLabel: 'Profile', ), ], onTap: (index) { setState(() => _selectedIndex = index); _pageController.jumpToPage(index); }, enableShadow: false, removeMargins: true, bottomBarWidth: MediaQuery.of(context).size.width, bottomBarHeight: 86, ), floatingActionButton: SizedBox( width: 52, height: 52, child: FloatingActionButton( isExtended: true, onPressed: () { _pageController.jumpToPage(2); setState(() => _selectedIndex = 2); }, child: SvgPicture.asset(AssetPaths.home_search), backgroundColor: context.color.primary.cyan500, shape: CircleBorder(), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, ); } }
内容的提问来源于stack exchange,提问作者Abhijith K Sabu
相关产品推荐
相关产品推荐

