news 2025/12/20 13:24:02

Flutter 通用底部导航组件 CommonBottomNavWidget:状态保持 + 凸起按钮适配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter 通用底部导航组件 CommonBottomNavWidget:状态保持 + 凸起按钮适配

在 Flutter 开发中,底部导航是多页面切换核心载体。原生BottomNavigationBar存在状态不保持、样式扩展差、不支持凸起按钮等问题。本文封装的CommonBottomNavWidget整合 “状态保持 + 自定义样式 + 中间凸起按钮 + 未读提示” 四大能力,支持 2-5 个导航项,一行代码集成,覆盖首页、商城等多 tab 场景。

一、核心优势

  1. 状态保持:基于IndexedStack实现页面切换不刷新,解决原生刷新问题
  2. 样式自定义:导航栏背景、图标 / 文本颜色、边框均可配置,支持纯色 / 渐变
  3. 凸起按钮:内置中间凸起按钮(如 “发布”),自动调整布局,无需手动偏移
  4. 未读提示:支持红点 / 数字提示,适配消息通知场景
  5. 交互优化:点击带切换动画,禁止重复点击,贴合平台交互规范

二、核心配置速览

配置分类核心参数核心作用
必选配置itemspages导航项列表、对应页面列表
样式配置bgColorselectedColorunselectedColorheight背景色、选中 / 未选中颜色、高度
交互配置initialIndexonTapdisableRepeatTap初始索引、点击回调、禁止重复点击
凸起按钮配置hasCenterBtncenterBtnWidgetcenterBtnOnTap凸起按钮显示、组件、点击回调
扩展配置showBadgebadgeCountelevation未读提示、阴影高度

三、完整代码(可直接复制)

dart

import 'package:flutter/material.dart'; /// 底部导航项模型 class BottomNavItem { final IconData selectedIcon; final IconData unselectedIcon; final String title; bool showBadge; int? badgeCount; BottomNavItem({ required this.selectedIcon, required this.unselectedIcon, required this.title, this.showBadge = false, this.badgeCount, }); } /// 通用底部导航组件 class CommonBottomNavWidget extends StatefulWidget { // 必选参数 final List<BottomNavItem> items; final List<Widget> pages; // 样式配置 final Color bgColor; final Color selectedColor; final Color unselectedColor; final TextStyle titleStyle; final double iconSize; final double height; final double elevation; final Border topBorder; // 交互配置 final int initialIndex; final ValueChanged<int>? onTap; final bool disableRepeatTap; final Duration animationDuration; // 凸起按钮配置(仅4个导航项生效) final bool hasCenterBtn; final Widget? centerBtnWidget; final VoidCallback? centerBtnOnTap; final double centerBtnSize; final double centerBtnOffset; // 适配配置 final bool adaptDarkMode; const CommonBottomNavWidget({ super.key, required this.items, required this.pages, this.bgColor = Colors.white, this.selectedColor = Colors.blue, this.unselectedColor = Colors.grey, this.titleStyle = const TextStyle(fontSize: 12), this.iconSize = 24.0, this.height = 56.0, this.elevation = 4.0, this.topBorder = const Border(top: BorderSide(color: Color(0xFFE0E0E0), width: 0.5)), this.initialIndex = 0, this.onTap, this.disableRepeatTap = true, this.animationDuration = const Duration(milliseconds: 200), this.hasCenterBtn = false, this.centerBtnWidget, this.centerBtnOnTap, this.centerBtnSize = 60.0, this.centerBtnOffset = 20.0, this.adaptDarkMode = true, }) : assert(items.length >=2 && items.length <=5, "导航项数量需为2-5个"), assert(pages.length == items.length, "页面数量需与导航项一致"), assert(!hasCenterBtn || items.length ==4, "凸起按钮仅支持4个导航项"), assert(!hasCenterBtn || centerBtnOnTap != null, "凸起按钮需配置点击回调"); @override State<CommonBottomNavWidget> createState() => _CommonBottomNavWidgetState(); } class _CommonBottomNavWidgetState extends State<CommonBottomNavWidget> { late int _currentIndex; late List<GlobalKey<NavigatorState>> _navigatorKeys; @override void initState() { super.initState(); _currentIndex = widget.initialIndex; _navigatorKeys = List.generate(widget.pages.length, (_) => GlobalKey<NavigatorState>()); } void _onItemTap(int index) { if (widget.disableRepeatTap && index == _currentIndex) return; setState(() => _currentIndex = index); widget.onTap?.call(index); } void _onCenterBtnTap() => widget.centerBtnOnTap?.call(); Color _adaptDarkMode(Color lightColor, Color darkColor) { if (!widget.adaptDarkMode) return lightColor; return MediaQuery.platformBrightnessOf(context) == Brightness.dark ? darkColor : lightColor; } /// 构建单个导航项 Widget _buildNavItem(BottomNavItem item, int index) { final isSelected = index == _currentIndex; final itemColor = isSelected ? _adaptDarkMode(widget.selectedColor, Colors.blueAccent) : _adaptDarkMode(widget.unselectedColor, Colors.grey[400]!); return Expanded( child: GestureDetector( onTap: () => _onItemTap(index), child: Container( height: widget.height, padding: const EdgeInsets.only(top: 8), child: Stack( alignment: Alignment.topCenter, children: [ Column( mainAxisSize: MainAxisSize.min, children: [ Icon( isSelected ? item.selectedIcon : item.unselectedIcon, size: widget.iconSize, color: itemColor, ), const SizedBox(height: 4), Text(item.title, style: widget.titleStyle.copyWith(color: itemColor)), ], ), // 未读提示 if (item.showBadge) Positioned( top: 0, right: 20, child: item.badgeCount == null ? Container(width: 8, height: 8, decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle)) : Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(8)), child: Text( item.badgeCount! >99 ? "99+" : "${item.badgeCount}", style: const TextStyle(fontSize: 10, color: Colors.white), ), ), ), ], ), ), ), ); } /// 构建导航项列表(含凸起按钮适配) List<Widget> _buildNavItems() { final items = <Widget>[]; for (int i = 0; i < widget.items.length; i++) { // 4个导航项时,中间位置插入占位符 if (widget.hasCenterBtn && widget.items.length ==4 && i ==2) { items.add(Expanded(child: Container())); // 占位 items.add(_buildCenterBtn()); } items.add(_buildNavItem(widget.items[i], i)); } return items; } /// 构建中间凸起按钮 Widget _buildCenterBtn() { final btnWidget = widget.centerBtnWidget ?? Container( width: widget.centerBtnSize, height: widget.centerBtnSize, decoration: BoxDecoration(color: _adaptDarkMode(widget.selectedColor, Colors.blueAccent), shape: BoxShape.circle, boxShadow: [ BoxShadow(color: Colors.grey.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, 2)) ]), child: Icon(Icons.add, color: Colors.white, size: widget.iconSize), ); return GestureDetector( onTap: _onCenterBtnTap, child: Container( margin: EdgeInsets.only(top: -widget.centerBtnOffset), child: btnWidget, ), ); } @override Widget build(BuildContext context) { final adaptedBgColor = _adaptDarkMode(widget.bgColor, const Color(0xFF2D2D2D)); final adaptedBorder = widget.topBorder.copyWith( top: BorderSide(color: _adaptDarkMode(widget.topBorder.top.color, const Color(0xFF444444)), width: widget.topBorder.top.width), ); return Scaffold( body: IndexedStack( index: _currentIndex, children: widget.pages.asMap().entries.map((entry) { final index = entry.key; final page = entry.value; return Navigator( key: _navigatorKeys[index], onGenerateRoute: (_) => MaterialPageRoute(builder: (_) => page), ); }).toList(), ), bottomNavigationBar: Container( height: widget.height + MediaQuery.of(context).padding.bottom, decoration: BoxDecoration(color: adaptedBgColor, border: adaptedBorder, boxShadow: [ BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: widget.elevation) ]), child: Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), child: Row(children: _buildNavItems()), ), ), ); } }

四、三大高频场景示例

场景 1:基础导航(3 个导航项)

dart

class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { final items = [ BottomNavItem( selectedIcon: Icons.home_filled, unselectedIcon: Icons.home_outlined, title: "首页", ), BottomNavItem( selectedIcon: Icons.shopping_cart, unselectedIcon: Icons.shopping_cart_outlined, title: "商城", showBadge: true, badgeCount: 3, // 未读数量 ), BottomNavItem( selectedIcon: Icons.person, unselectedIcon: Icons.person_outlined, title: "我的", ), ]; final pages = [const HomeTab(), const MallTab(), const MineTab()]; return CommonBottomNavWidget( items: items, pages: pages, selectedColor: Colors.orangeAccent, unselectedColor: Colors.grey[600]!, bgColor: Colors.white, elevation: 2, onTap: (index) => debugPrint("切换到第$index页"), ); } } // 示例页面(实际项目替换为真实页面) class HomeTab extends StatelessWidget { const HomeTab({super.key}); @override Widget build(BuildContext context) => const Center(child: Text("首页")); } class MallTab extends StatelessWidget { const MallTab({super.key}); @override Widget build(BuildContext context) => const Center(child: Text("商城")); } class MineTab extends StatelessWidget { const MineTab({super.key}); @override Widget build(BuildContext context) => const Center(child: Text("我的")); }

场景 2:带凸起按钮(4 个导航项)

dart

class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { final items = [ BottomNavItem(selectedIcon: Icons.home_filled, unselectedIcon: Icons.home_outlined, title: "首页"), BottomNavItem(selectedIcon: Icons.message, unselectedIcon: Icons.message_outlined, title: "消息", showBadge: true), BottomNavItem(selectedIcon: Icons.discover, unselectedIcon: Icons.discover_outlined, title: "发现"), BottomNavItem(selectedIcon: Icons.mine, unselectedIcon: Icons.mine_outlined, title: "我的"), ]; final pages = [const HomeTab(), const MessageTab(), const DiscoverTab(), const MineTab()]; return CommonBottomNavWidget( items: items, pages: pages, hasCenterBtn: true, centerBtnOnTap: () { // 凸起按钮点击逻辑(如打开发布弹窗) showModalBottomSheet( context: context, builder: (_) => const SizedBox(height: 200, child: Center(child: Text("发布内容"))), ); }, // 自定义凸起按钮样式 centerBtnWidget: Container( width: 56, height: 56, decoration: BoxDecoration( gradient: const LinearGradient(colors: [Colors.orange, Colors.redAccent]), shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.orange.withOpacity(0.3), blurRadius: 6)], ), child: const Icon(Icons.add, color: Colors.white, size: 28), ), selectedColor: Colors.orange, bgColor: const Color(0xFFFAFAFA), ); } }

场景 3:深色模式适配

dart

class DarkModeNavPage extends StatelessWidget { @override Widget build(BuildContext context) { final items = [ BottomNavItem(selectedIcon: Icons.home_filled, unselectedIcon: Icons.home_outlined, title: "首页"), BottomNavItem(selectedIcon: Icons.settings, unselectedIcon: Icons.settings_outlined, title: "设置"), ]; final pages = [const HomeTab(), const SettingsTab()]; return CommonBottomNavWidget( items: items, pages: pages, adaptDarkMode: true, bgColor: Colors.white, selectedColor: Colors.blue, unselectedColor: Colors.grey, topBorder: const Border(top: BorderSide(color: Color(0xFFE0E0E0))), // 深色模式下自动适配颜色 ); } } class SettingsTab extends StatelessWidget { const SettingsTab({super.key}); @override Widget build(BuildContext context) => const Center(child: Text("设置")); }

五、核心封装技巧

  1. 状态保持:通过IndexedStack包裹页面,配合Navigator实现子页面跳转与状态保留
  2. 凸起按钮适配:4 个导航项时中间插入占位符,按钮通过margin向上偏移,布局自动对齐
  3. 未读提示:通过Stack叠加红点 / 数字,支持动态更新showBadgebadgeCount
  4. 深色适配:所有颜色通过_adaptDarkMode统一处理,无需单独配置
  5. 交互优化:disableRepeatTap禁止重复点击,避免页面重复构建

六、避坑指南

  1. 页面数量:pages数量必须与items一致,否则会报断言错误
  2. 凸起按钮:仅支持 4 个导航项,其他数量时hasCenterBtn不生效
  3. 底部安全区:自动适配全面屏底部间距,无需额外添加SafeArea
  4. 子页面跳转:通过_navigatorKeys[index].currentState?.push实现子页面跳转,不影响底部导航
  5. 样式统一:建议导航项文本长度一致(2-3 字),避免布局错位

https://openharmonycrossplatform.csdn.net/content

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/14 12:48:59

FastDepth深度估计算法:从入门到实战的完整指南

FastDepth深度估计算法&#xff1a;从入门到实战的完整指南 【免费下载链接】fast-depth ICRA 2019 "FastDepth: Fast Monocular Depth Estimation on Embedded Systems" 项目地址: https://gitcode.com/gh_mirrors/fa/fast-depth FastDepth是一个专为嵌入式系…

作者头像 李华
网站建设 2025/12/14 12:48:37

揭秘腾讯混元3D-Part:从零掌握3D文件格式的实战指南

在3D内容创作的世界里&#xff0c;文件格式就像是不同语言之间的翻译器&#xff0c;而腾讯混元3D-Part正是那个精通多种"语言"的顶级翻译官。想要驾驭这个强大的3D部件生成与分割工具&#xff0c;首先需要理解它的文件格式支持体系&#xff0c;这是开启高效3D创作之旅…

作者头像 李华
网站建设 2025/12/14 12:47:55

Blender资源宝库:一站式掌握3D创作完整指南

Blender资源宝库&#xff1a;一站式掌握3D创作完整指南 【免费下载链接】awesome-blender &#x1fa90; A curated list of awesome Blender addons, tools, tutorials; and 3D resources for everyone. 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-blender …

作者头像 李华
网站建设 2025/12/14 12:47:52

Rust全栈开发新篇章:Loco框架与Tauri桌面应用实战指南

Rust全栈开发新篇章&#xff1a;Loco框架与Tauri桌面应用实战指南 【免费下载链接】loco &#x1f682; &#x1f980; The one-person framework for Rust for side-projects and startups 项目地址: https://gitcode.com/GitHub_Trending/lo/loco 在当今桌面应用开发领…

作者头像 李华
网站建设 2025/12/14 12:47:47

Granite Docling 258M:轻量化文档智能处理的技术突破

Granite Docling 258M&#xff1a;轻量化文档智能处理的技术突破 【免费下载链接】granite-docling-258M 项目地址: https://ai.gitcode.com/hf_mirrors/ibm-granite/granite-docling-258M 在数字化办公需求激增的当下&#xff0c;IBM Research推出的Granite Docling 2…

作者头像 李华
网站建设 2025/12/14 12:47:19

Ursa.Avalonia样式系统完整教程:构建专业级跨平台界面

Ursa.Avalonia样式系统完整教程&#xff1a;构建专业级跨平台界面 【免费下载链接】Ursa.Avalonia Ursa是一个用于开发Avalonia程序的控件库 项目地址: https://gitcode.com/IRIHI_Technology/Ursa.Avalonia 在现代企业级应用开发中&#xff0c;一套强大而灵活的样式系统…

作者头像 李华