You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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包,关闭多余动画只保留深凹槽效果:

  1. 添加依赖到pubspec.yaml:
dependencies:
  animated_notch_bottom_bar: ^1.0.3
  1. 修改代码使用该组件:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 19:47:31