2026/4/6 3:40:02
网站建设
项目流程
织梦网站怎么做备份,商贸企业网站建设设计方案,itc 做市场分析的网站,网站登录慢在 Flutter 开发中#xff0c;底部导航是多页面切换核心载体。原生 BottomNavigationBar 存在状态不保持、样式扩展差、不支持凸起按钮等问题。本文封装的 CommonBottomNavWidget 整合 “状态保持 自定义样式 中间凸起按钮 未读提示” 四大能力#xff0c;支持 2-5 个导航…在 Flutter 开发中底部导航是多页面切换核心载体。原生BottomNavigationBar存在状态不保持、样式扩展差、不支持凸起按钮等问题。本文封装的CommonBottomNavWidget整合 “状态保持 自定义样式 中间凸起按钮 未读提示” 四大能力支持 2-5 个导航项一行代码集成覆盖首页、商城等多 tab 场景。一、核心优势状态保持基于IndexedStack实现页面切换不刷新解决原生刷新问题样式自定义导航栏背景、图标 / 文本颜色、边框均可配置支持纯色 / 渐变凸起按钮内置中间凸起按钮如 “发布”自动调整布局无需手动偏移未读提示支持红点 / 数字提示适配消息通知场景交互优化点击带切换动画禁止重复点击贴合平台交互规范二、核心配置速览配置分类核心参数核心作用必选配置items、pages导航项列表、对应页面列表样式配置bgColor、selectedColor、unselectedColor、height背景色、选中 / 未选中颜色、高度交互配置initialIndex、onTap、disableRepeatTap初始索引、点击回调、禁止重复点击凸起按钮配置hasCenterBtn、centerBtnWidget、centerBtnOnTap凸起按钮显示、组件、点击回调扩展配置showBadge、badgeCount、elevation未读提示、阴影高度三、完整代码可直接复制dartimport 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 ListBottomNavItem items; final ListWidget 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 ValueChangedint? 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 StateCommonBottomNavWidget createState() _CommonBottomNavWidgetState(); } class _CommonBottomNavWidgetState extends StateCommonBottomNavWidget { late int _currentIndex; late ListGlobalKeyNavigatorState _navigatorKeys; override void initState() { super.initState(); _currentIndex widget.initialIndex; _navigatorKeys List.generate(widget.pages.length, (_) GlobalKeyNavigatorState()); } 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), ), ), ), ], ), ), ), ); } /// 构建导航项列表含凸起按钮适配 ListWidget _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 个导航项dartclass 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 个导航项dartclass 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深色模式适配dartclass 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(设置)); }五、核心封装技巧状态保持通过IndexedStack包裹页面配合Navigator实现子页面跳转与状态保留凸起按钮适配4 个导航项时中间插入占位符按钮通过margin向上偏移布局自动对齐未读提示通过Stack叠加红点 / 数字支持动态更新showBadge和badgeCount深色适配所有颜色通过_adaptDarkMode统一处理无需单独配置交互优化disableRepeatTap禁止重复点击避免页面重复构建六、避坑指南页面数量pages数量必须与items一致否则会报断言错误凸起按钮仅支持 4 个导航项其他数量时hasCenterBtn不生效底部安全区自动适配全面屏底部间距无需额外添加SafeArea子页面跳转通过_navigatorKeys[index].currentState?.push实现子页面跳转不影响底部导航样式统一建议导航项文本长度一致2-3 字避免布局错位https://openharmonycrossplatform.csdn.net/content