底部导航栏是 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,
),
四、核心封装设计技巧
- 模型化配置 :通过
BottomNavItem统一管理导航项的图标、文字、徽章、凸起状态,配置清晰,新增 / 修改导航项无需改动核心逻辑。 - 防重复点击 :通过
_isClickable锁 + 150ms 延迟,避免快速连续点击导致的页面切换异常,提升交互稳定性。 - 全面屏适配 :自动添加底部安全区间距(
MediaQuery.of(context).padding.bottom),避免导航栏被全面屏底部手势区域遮挡。 - 徽章智能适配:支持数字徽章(自动处理 99 + 溢出)和红点徽章,优先级清晰,满足消息提醒的核心需求。
- 多形态支持 :通过
is凸起参数支持普通 / 凸起两种导航项,无需额外封装组件,适配发布按钮等特殊场景。 - 样式可扩展:选中指示器、颜色、尺寸、间距均可自定义,既保证通用性,又能适配不同 APP 的品牌风格。
五、避坑指南(开发必看)
- 导航项数量控制:建议导航项数量控制在 3-5 个,过多会导致每个项宽度过窄,点击区域变小,影响交互体验;过少则布局松散,视觉不协调。
- 徽章显示逻辑:数字徽章需传入 "非 null 且大于 0" 的值才会显示,红点徽章需满足 "showRedDot=true 且 badgeCount=null",避免两种徽章同时生效。
- 高度适配规范:导航栏高度建议设置为 56-60px,配合底部安全区后总高度适中,过低会导致图标 / 文字挤压,过高影响页面内容展示。
- 颜色对比度要求:选中颜色与未选中颜色的对比度建议大于 3:1,确保视觉辨识度,适配无障碍需求(如弱视用户)。
- 凸起按钮注意事项:凸起按钮建议放在导航栏中间位置,且数量不超过 1 个,避免布局混乱;凸起高度建议控制在 10-15px,过高会显得突兀。