Flutter 通用底部导航栏:BottomNavWidget 一键实现样式统一与灵活切换

底部导航栏是 APP 核心布局组件,原生BottomNavigationBar存在样式固化、徽章配置繁琐、切换逻辑重复、全面屏适配差等问题。本文封装的BottomNavWidget,整合 "全样式自定义 + 徽章功能 + 防重复点击 + 全面屏适配" 四大核心能力,支持图标 + 文字、纯图标、中间凸起按钮等多场景,一行代码即可搭建交互流畅、风格统一的底部导航!

一、核心需求拆解(直击开发痛点)

✅ 样式全自定义:支持图标(选中 / 未选中)、文字、颜色、圆角、选中指示器自定义✅ 徽章功能:支持数字徽章(99 + 溢出处理)、红点徽章,适配消息提醒场景✅ 交互优化:防重复点击、选中状态记忆、切换回调即时反馈✅ 布局灵活:支持图标 + 文字、纯图标两种模式,适配中间凸起按钮(如发布按钮)✅ 适配全面屏:自动适配底部安全区,避免被手势区域遮挡✅ 扩展便捷:支持自定义导航项数量、尺寸、间距,无需修改核心逻辑

二、完整代码实现(精简高效版)

dart

复制代码
import 'package:flutter/material.dart';

// 导航项模型(统一配置单个导航项,清晰无冗余)
class BottomNavItem {
  final String label; // 导航文字(纯图标模式设为空)
  final IconData normalIcon; // 未选中图标
  final IconData selectedIcon; // 选中图标
  final int? badgeCount; // 数字徽章(null不显示)
  final bool showRedDot; // 红点徽章(优先级低于数字徽章)
  final bool is凸起; // 是否为凸起按钮(如发布按钮)
  final double?凸起Height; // 凸起按钮高度(默认高于导航栏10px)

  const BottomNavItem({
    required this.label,
    required this.normalIcon,
    required this.selectedIcon,
    this.badgeCount,
    this.showRedDot = false,
    this.is凸起 = false,
    this.凸起Height,
  });
}

/// 通用底部导航栏组件
class BottomNavWidget extends StatefulWidget {
  // 必选参数
  final List<BottomNavItem> items; // 导航项列表
  final Function(int index) onTap; // 切换回调(返回选中索引)

  // 样式配置(均含合理默认值,适配主流设计)
  final int initialIndex; // 初始选中索引(默认0)
  final Color normalColor; // 未选中颜色(默认灰色)
  final Color selectedColor; // 选中颜色(默认蓝色)
  final Color bgColor; // 导航栏背景色(默认白色)
  final double iconSize; // 图标大小(默认24)
  final double fontSize; // 文字大小(默认12)
  final double itemSpacing; // 图标与文字间距(默认4)
  final bool showLabel; // 是否显示文字(默认true)
  final double height; // 导航栏高度(默认56)
  final Widget? indicator; // 选中指示器(默认下划线)
  final double badgeMaxCount; // 徽章最大显示数(默认99)

  const BottomNavWidget({
    super.key,
    required this.items,
    required this.onTap,
    this.initialIndex = 0,
    this.normalColor = const Color(0xFF999999),
    this.selectedColor = Colors.blue,
    this.bgColor = Colors.white,
    this.iconSize = 24.0,
    this.fontSize = 12.0,
    this.itemSpacing = 4.0,
    this.showLabel = true,
    this.height = 56.0,
    this.indicator,
    this.badgeMaxCount = 99,
  });

  @override
  State<BottomNavWidget> createState() => _BottomNavWidgetState();
}

class _BottomNavWidgetState extends State<BottomNavWidget> {
  late int _currentIndex;
  bool _isClickable = true; // 防重复点击锁

  @override
  void initState() {
    super.initState();
    _currentIndex = widget.initialIndex;
  }

  /// 处理导航项点击(防重复点击+切换逻辑)
  void _handleTap(int index) {
    if (!_isClickable || _currentIndex == index) return;
    _isClickable = false;
    setState(() => _currentIndex = index);
    widget.onTap(index);
    // 150ms后解锁,避免快速连续点击导致页面切换异常
    Future.delayed(const Duration(milliseconds: 150), () => _isClickable = true);
  }

  /// 构建徽章组件(数字/红点自动适配)
  Widget _buildBadge(BottomNavItem item) {
    // 数字徽章(优先级更高)
    if (item.badgeCount != null && item.badgeCount! > 0) {
      final count = item.badgeCount! > widget.badgeMaxCount 
          ? '${widget.badgeMaxCount}+' 
          : item.badgeCount.toString();
      return Container(
        padding: EdgeInsets.symmetric(
          horizontal: count.length > 2 ? 4 : 6,
          vertical: 1,
        ),
        decoration: const BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Text(
          count,
          style: const TextStyle(color: Colors.white, fontSize: 10),
        ),
      );
    }
    // 红点徽章
    if (item.showRedDot && item.badgeCount == null) {
      return Container(
        width: 8,
        height: 8,
        decoration: const BoxDecoration(
          color: Colors.red,
          borderRadius: BorderRadius.circular(4),
        ),
      );
    }
    return const SizedBox.shrink();
  }

  /// 构建单个导航项(支持普通/凸起两种类型)
  Widget _buildNavItem(BottomNavItem item, int index) {
    final isSelected = _currentIndex == index;
    final color = isSelected ? widget.selectedColor : widget.normalColor;
    final navHeight = item.is凸起 
        ? (widget.height + (item.凸起Height ?? 10)) 
        : widget.height;

    return GestureDetector(
      onTap: () => _handleTap(index),
      child: SizedBox(
        height: navHeight,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 图标+徽章组合
            Stack(
              alignment: Alignment.topRight,
              children: [
                Icon(
                  isSelected ? item.selectedIcon : item.normalIcon,
                  size: widget.iconSize,
                  color: color,
                ),
                _buildBadge(item),
              ],
            ),
            // 文字(可选显示)
            if (widget.showLabel && item.label.isNotEmpty)
              Padding(
                padding: EdgeInsets.only(top: widget.itemSpacing),
                child: Text(
                  item.label,
                  style: TextStyle(color: color, fontSize: widget.fontSize),
                ),
              ),
          ],
        ),
      ),
    );
  }

  /// 构建选中指示器(默认下划线,支持自定义)
  Widget _buildIndicator() {
    return widget.indicator ??
        Container(
          height: 3,
          width: 24,
          decoration: BoxDecoration(
            color: widget.selectedColor,
            borderRadius: BorderRadius.circular(3),
          ),
        );
  }

  @override
  Widget build(BuildContext context) {
    // 底部安全区间距(适配全面屏)
    final bottomPadding = MediaQuery.of(context).padding.bottom;

    return Container(
      height: widget.height + bottomPadding,
      color: widget.bgColor,
      padding: EdgeInsets.only(bottom: bottomPadding),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: widget.items.asMap().entries.map((entry) {
          final index = entry.key;
          final item = entry.value;
          final isSelected = _currentIndex == index;

          return Expanded(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                _buildNavItem(item, index),
                // 选中指示器(仅普通导航项显示)
                if (isSelected && widget.showLabel && !item.is凸起)
                  Padding(
                    padding: EdgeInsets.only(top: widget.itemSpacing / 2),
                    child: _buildIndicator(),
                  ),
              ],
            ),
          );
        }).toList(),
      ),
    );
  }
}

三、三大高频场景实战示例

场景 1:基础导航(图标 + 文字,APP 首页)

适配大多数 APP 首页导航,支持消息徽章,样式简洁统一:

dart

复制代码
BottomNavWidget(
  items: [
    const BottomNavItem(
      label: '首页',
      normalIcon: Icons.home_outlined,
      selectedIcon: Icons.home,
    ),
    const BottomNavItem(
      label: '发现',
      normalIcon: Icons.explore_outlined,
      selectedIcon: Icons.explore,
    ),
    const BottomNavItem(
      label: '消息',
      normalIcon: Icons.message_outlined,
      selectedIcon: Icons.message,
      badgeCount: 3, // 数字徽章
    ),
    const BottomNavItem(
      label: '我的',
      normalIcon: Icons.person_outlined,
      selectedIcon: Icons.person,
      showRedDot: true, // 红点徽章
    ),
  ],
  onTap: (index) {
    debugPrint('选中导航项:$index');
    // 实际场景:切换页面(如使用PageView或Navigator)
  },
  selectedColor: Colors.orange,
  normalColor: const Color(0xFF666666),
  bgColor: const Color(0xFFFAFAFA),
  indicator: Container(
    height: 3,
    width: 30,
    decoration: BoxDecoration(
      color: Colors.orange,
      borderRadius: BorderRadius.circular(3),
    ),
  ),
),

场景 2:纯图标导航(简洁风格,工具类 APP)

适配工具类、社交类 APP,隐藏文字仅保留图标,布局更紧凑:

dart

复制代码
BottomNavWidget(
  items: [
    const BottomNavItem(
      label: '', // 纯图标模式,文字为空
      normalIcon: Icons.home_outlined,
      selectedIcon: Icons.home,
    ),
    const BottomNavItem(
      label: '',
      normalIcon: Icons.search_outlined,
      selectedIcon: Icons.search,
    ),
    const BottomNavItem(
      label: '',
      normalIcon: Icons.add_circle_outline,
      selectedIcon: Icons.add_circle,
    ),
    const BottomNavItem(
      label: '',
      normalIcon: Icons.favorite_outlined,
      selectedIcon: Icons.favorite,
    ),
    const BottomNavItem(
      label: '',
      normalIcon: Icons.person_outlined,
      selectedIcon: Icons.person,
    ),
  ],
  onTap: (index) => debugPrint('选中纯图标导航:$index'),
  showLabel: false, // 隐藏文字
  iconSize: 28,
  height: 60,
  bgColor: Colors.white,
  selectedColor: Colors.blueAccent,
),

场景 3:中间凸起导航(带发布按钮,电商 / 社交 APP)

适配需要中间发布按钮的场景(如电商发布商品、社交发动态):

dart

复制代码
BottomNavWidget(
  items: [
    const BottomNavItem(
      label: '首页',
      normalIcon: Icons.home_outlined,
      selectedIcon: Icons.home,
    ),
    const BottomNavItem(
      label: '分类',
      normalIcon: Icons.category_outlined,
      selectedIcon: Icons.category,
    ),
    const BottomNavItem(
      label: '', // 凸起按钮无需文字
      normalIcon: Icons.add_circle,
      selectedIcon: Icons.add_circle,
      is凸起: true, // 标记为凸起按钮
      凸起Height: 15, // 凸起高度
    ),
    const BottomNavItem(
      label: '购物车',
      normalIcon: Icons.shopping_cart_outlined,
      selectedIcon: Icons.shopping_cart,
      badgeCount: 12,
    ),
    const BottomNavItem(
      label: '我的',
      normalIcon: Icons.person_outlined,
      selectedIcon: Icons.person,
    ),
  ],
  onTap: (index) {
    if (index == 2) {
      debugPrint('点击发布按钮');
      // 实际场景:弹出发布选项弹窗
    } else {
      debugPrint('选中导航项:$index');
    }
  },
  selectedColor: Colors.red,
  normalColor: const Color(0xFF999999),
  indicator: Container(
    width: 8,
    height: 8,
    decoration: const BoxDecoration(
      color: Colors.red,
      shape: BoxShape.circle,
    ),
  ),
  badgeMaxCount: 99,
),

四、核心封装设计技巧

  1. 模型化配置 :通过BottomNavItem统一管理导航项的图标、文字、徽章、凸起状态,配置清晰,新增 / 修改导航项无需改动核心逻辑。
  2. 防重复点击 :通过_isClickable锁 + 150ms 延迟,避免快速连续点击导致的页面切换异常,提升交互稳定性。
  3. 全面屏适配 :自动添加底部安全区间距(MediaQuery.of(context).padding.bottom),避免导航栏被全面屏底部手势区域遮挡。
  4. 徽章智能适配:支持数字徽章(自动处理 99 + 溢出)和红点徽章,优先级清晰,满足消息提醒的核心需求。
  5. 多形态支持 :通过is凸起参数支持普通 / 凸起两种导航项,无需额外封装组件,适配发布按钮等特殊场景。
  6. 样式可扩展:选中指示器、颜色、尺寸、间距均可自定义,既保证通用性,又能适配不同 APP 的品牌风格。

五、避坑指南(开发必看)

  1. 导航项数量控制:建议导航项数量控制在 3-5 个,过多会导致每个项宽度过窄,点击区域变小,影响交互体验;过少则布局松散,视觉不协调。
  2. 徽章显示逻辑:数字徽章需传入 "非 null 且大于 0" 的值才会显示,红点徽章需满足 "showRedDot=true 且 badgeCount=null",避免两种徽章同时生效。
  3. 高度适配规范:导航栏高度建议设置为 56-60px,配合底部安全区后总高度适中,过低会导致图标 / 文字挤压,过高影响页面内容展示。
  4. 颜色对比度要求:选中颜色与未选中颜色的对比度建议大于 3:1,确保视觉辨识度,适配无障碍需求(如弱视用户)。
  5. 凸起按钮注意事项:凸起按钮建议放在导航栏中间位置,且数量不超过 1 个,避免布局混乱;凸起高度建议控制在 10-15px,过高会显得突兀。

https://openharmonycrossplatform.csdn.net/content

相关推荐
克喵的水银蛇2 小时前
Flutter 通用轮播图组件:BannerSlider 一键实现自动轮播与灵活定制
windows·microsoft
小白|2 小时前
OpenHarmony + Flutter 混合开发实战:深度集成 Health Kit 实现跨设备健康数据同步与隐私保护
flutter
ujainu2 小时前
Flutter实战避坑指南:从架构设计到性能优化的全链路方案
flutter
解局易否结局3 小时前
Flutter:跨平台开发的“效率与体验”双优解
flutter
技术小甜甜3 小时前
【系统实战排坑】电脑重启后总是直接进入 Windows,按键无效进不了 BIOS?最全解决方案在这里!
windows·电脑·蓝屏
永远都不秃头的程序员(互关)3 小时前
鸿蒙Electron平台:Flutter技术深度解读及学习笔记
笔记·学习·flutter
阿富软件园3 小时前
文档搜索利器——“搜索文本”全能版 支持多格式 加 内容搜索
windows·电脑·开源软件
取个名字太难了a3 小时前
任务段提权实验
windows
tangweiguo030519874 小时前
Riverpod 2.x 完全指南:从 StateNotifierProvider 到现代状态管理
flutter