开源鸿蒙 栏完整开发指南Flutter 实战|自定义底部导航
本文介绍了如何在开源鸿蒙平台上使用Flutter实现自定义底部导航栏的开发指南。文章面向新手开发者,详细讲解了如何基于Flutter原生组件打造具有特色的导航栏功能,包括中间凸起的发布按钮、点击交互动画和底部弹窗菜单等核心功能。 核心要点: 完全使用Flutter原生组件实现,无第三方依赖 实现中间悬浮发布按钮和导航项动画效果 支持深色/浅色模式自动适配 经过鸿蒙虚拟机实机验证 提供可复用的组件封
🎨 开源鸿蒙 Flutter 实战|自定义底部导航栏完整开发指南
欢迎加入开源鸿蒙跨平台社区→https://openharmonycrosplatform.csdn.net
【摘要】本文面向开源鸿蒙跨平台开发新手,基于 Flutter 官方原生组件实现高自定义底部导航栏,包含中间凸起发布按钮、点击交互动画、底部弹窗菜单等核心能力,完整覆盖组件封装、页面接入、鸿蒙适配要点与虚拟机实机运行验证,代码可直接复制复用,完美适配开源鸿蒙平台。
之前用系统自带的 BottomNavigationBar 总觉得不够有特色,这次直接自己动手撸了一个超好看的自定义底部导航栏!不仅有中间凸起的发布按钮,还有丝滑的点击缩放动画、弹出式功能菜单,还踩坑修复了按钮点击无响应的问题,全程用 Flutter 原生组件实现,100% 兼容开源鸿蒙,已经在虚拟机完整验证啦!
先给大家汇报一下这次的核心成果✨:
✅ 纯原生组件封装自定义底部导航栏,无额外三方依赖
✅ 中间凸起悬浮发布按钮,渐变背景 + 阴影质感拉满
✅ 导航项点击缩放动画、选中态指示条动效
✅ 发布按钮点击弹出底部功能菜单
✅ 深色 / 浅色模式自动适配,无视觉 bug
✅ 鸿蒙虚拟机实机验证,手势交互完全正常
✅ 低侵入接入,替换原生导航栏一行代码搞定
一、技术选型说明
这次我全程用Flutter 官方原生组件实现,没有引入额外三方库,完全规避了鸿蒙兼容风险,所有组件都在 OpenHarmony 兼容清单内,新手完全不用担心踩坑!

二、核心组件完整实现
我把导航栏拆成了可复用的独立组件,一次编写,全项目都能用,新手直接复制就能用!
2.1 第一步:封装自定义底部导航栏主组件
在lib/widgets目录下新建custom_bottom_nav_bar.dart,完整代码如下:
import 'package:flutter/material.dart';
/// 自定义底部导航栏 鸿蒙适配版
class CustomBottomNavBar extends StatelessWidget {
/// 当前选中的索引
final int currentIndex;
/// 导航项点击回调
final Function(int) onItemTap;
/// 发布按钮点击回调
final VoidCallback onPublishTap;
/// 是否深色模式
final bool isDarkMode;
const CustomBottomNavBar({
super.key,
required this.currentIndex,
required this.onItemTap,
required this.onPublishTap,
required this.isDarkMode,
});
Widget build(BuildContext context) {
return Container(
height: 65,
decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(isDarkMode ? 0.3 : 0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Stack(
clipBehavior: Clip.none,
children: [
// 导航项区域
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
// 首页
_buildNavItem(
index: 0,
icon: Icons.home,
label: "首页",
),
// 发现
_buildNavItem(
index: 1,
icon: Icons.explore,
label: "发现",
),
// 中间占位 给凸起按钮
const SizedBox(width: 60),
// 消息
_buildNavItem(
index: 2,
icon: Icons.message,
label: "消息",
),
// 我的
_buildNavItem(
index: 3,
icon: Icons.person,
label: "我的",
),
],
),
// 中间凸起发布按钮
Positioned(
top: -20,
left: 0,
right: 0,
child: _AnimatedPublishButton(
onTap: onPublishTap,
isDarkMode: isDarkMode,
),
),
],
),
);
}
/// 构建单个导航项
Widget _buildNavItem({
required int index,
required IconData icon,
required String label,
}) {
final isSelected = currentIndex == index;
return GestureDetector(
onTap: () => onItemTap(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
transform: Matrix4.identity()..scale(isSelected ? 0.95 : 1.0),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isSelected
? const Color(0xFF6C63FF)
: (isDarkMode ? Colors.grey[400] : Colors.grey[600]),
size: 24,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: isSelected
? const Color(0xFF6C63FF)
: (isDarkMode ? Colors.grey[400] : Colors.grey[600]),
),
),
// 选中态底部指示条
AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(top: 2),
width: isSelected ? 16 : 0,
height: 2,
decoration: BoxDecoration(
color: const Color(0xFF6C63FF),
borderRadius: BorderRadius.circular(1),
),
),
],
),
),
);
}
}
/// 带动画的发布按钮
class _AnimatedPublishButton extends StatefulWidget {
final VoidCallback onTap;
final bool isDarkMode;
const _AnimatedPublishButton({
required this.onTap,
required this.isDarkMode,
});
State<_AnimatedPublishButton> createState() => _AnimatedPublishButtonState();
}
class _AnimatedPublishButtonState extends State<_AnimatedPublishButton> with SingleTickerProviderStateMixin {
bool _isPressed = false;
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) {
setState(() => _isPressed = false);
widget.onTap.call();
},
onTapCancel: () => setState(() => _isPressed = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
transform: Matrix4.identity()..scale(_isPressed ? 0.9 : 1.0),
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C63FF), Color(0xFF8A85FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFF6C63FF).withOpacity(0.4),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
Icons.add,
color: Colors.white,
size: 28,
),
),
);
}
}
/// 发布功能底部弹窗
class PublishBottomSheet extends StatelessWidget {
const PublishBottomSheet({super.key});
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
const Text(
"选择发布类型",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildSheetItem(
icon: Icons.edit_note,
label: "写文章",
color: Colors.blue,
onTap: () {
Navigator.pop(context);
// 后续可扩展跳转到文章编辑页
},
),
_buildSheetItem(
icon: Icons.code,
label: "分享项目",
color: Colors.green,
onTap: () {
Navigator.pop(context);
},
),
_buildSheetItem(
icon: Icons.question_answer,
label: "提问题",
color: Colors.orange,
onTap: () {
Navigator.pop(context);
},
),
],
),
const SizedBox(height: 30),
],
),
);
}
/// 构建弹窗功能项
Widget _buildSheetItem({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(icon, color: color, size: 30),
),
const SizedBox(height: 8),
Text(label, style: const TextStyle(fontSize: 14)),
],
),
);
}
}
2.2 第二步:主页面接入自定义导航栏
修改lib/main.dart,替换原来的系统 BottomNavigationBar,同时接入主题适配和发布弹窗:
// 导入自定义导航栏
import 'widgets/custom_bottom_nav_bar.dart';
import 'package:provider/provider.dart';
import 'providers/theme_provider.dart';
// 主Tab页面修改
class MainTabPage extends StatefulWidget {
const MainTabPage({super.key});
State<MainTabPage> createState() => _MainTabPageState();
}
class _MainTabPageState extends State<MainTabPage> {
int _currentIndex = 0;
final List<Widget> _pages = const [
HomePage(),
DiscoverPage(),
MessagePage(),
MinePage(),
];
// 导航项点击切换
void _onNavItemTap(int index) {
setState(() {
_currentIndex = index;
});
}
// 发布按钮点击 弹出底部菜单
void _onPublishTap(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => const PublishBottomSheet(),
);
}
Widget build(BuildContext context) {
// 获取主题状态
final themeProvider = Provider.of<ThemeProvider>(context);
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
// 替换为自定义底部导航栏
bottomNavigationBar: CustomBottomNavBar(
currentIndex: _currentIndex,
onItemTap: _onNavItemTap,
onPublishTap: () => _onPublishTap(context),
isDarkMode: themeProvider.isDarkMode,
),
);
}
}
三、鸿蒙平台适配与踩坑指南
作为新手,这次开发也踩了好几个鸿蒙专属的坑,整理出来给大家避坑,保证一次跑通👇
3.1 核心问题修复:发布按钮点击无响应
问题原因:
之前的代码里,发布按钮外层和内部都嵌套了GestureDetector,内部的手势检测器拦截了点击事件,导致外层的onTap回调无法触发,鸿蒙系统对手势事件的分发规则和 Android/iOS 有细微差异,这个问题在鸿蒙上会更明显。
修复方案:
移除发布按钮外层多余的GestureDetector
给内部的_AnimatedPublishButton添加onTap参数
在onTapUp手势回调中触发点击事件,保证手势不被拦截
3.2 深色模式适配
导航栏的背景色、文字色、阴影都做了深色模式适配,通过isDarkMode参数联动之前的主题 Provider,在鸿蒙设备上切换深色 / 浅色模式时,导航栏会自动适配,无视觉异常。
3.3 性能优化
用AnimatedContainer实现所有动画,避免不必要的重建
导航项点击动画时长控制在 200ms 内,符合鸿蒙系统交互规范
用Stack实现凸起按钮,避免布局溢出,在鸿蒙不同分辨率设备上都能正常显示
3.4 权限说明
整个导航栏都是纯 UI 组件,不需要申请任何鸿蒙系统权限,直接接入就能用!
四、鸿蒙虚拟机运行验证
一键运行命令
cd ohos
hvigorw assembleHap -p product=default -p buildMode=debug
hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap
hdc shell aa start -a EntryAbility -b com.example.demo1
Flutter 开源鸿蒙自定义导航栏 - 虚拟机全屏运行验证效果
Flutter 开源鸿蒙自定义导航栏 - 虚拟机运行验证效果
效果:应用在鸿蒙虚拟机全屏稳定运行,无闪退、无布局溢出、无渲染异常
五、新手学习总结
作为大一新生,这次自定义底部导航栏的开发真的让我收获满满!原来不用三方库,只用 Flutter 原生组件,就能做出这么好看又好用的导航栏,而且完全兼容开源鸿蒙平台,成就感直接拉满🥰
这次开发也让我明白了:
自定义组件要做好分层封装,后续修改和复用都会更方便
手势事件的嵌套一定要注意,很容易出现点击无响应的问题,尤其是在鸿蒙平台上做 UI 组件一定要做好深色模式适配,不然用户切换主题就会出现视觉 bug
开源鸿蒙对 Flutter 原生组件的支持真的越来越好了,新手开发门槛越来越低
后续我还会继续优化这个导航栏:
✅ 增加未读消息角标
✅ 自定义更多动画效果
✅ 适配鸿蒙折叠屏设备
✅ 增加滑动隐藏导航栏的能力
也会持续给大家分享我的鸿蒙 Flutter 新手实战内容,和大家一起在开源鸿蒙的生态里慢慢进步✨
如果这篇文章有帮到你,或者你也有更好的自定义导航栏实现思路,欢迎在评论区和我交流呀!
更多推荐


所有评论(0)