🎨 开源鸿蒙 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 新手实战内容,和大家一起在开源鸿蒙的生态里慢慢进步✨

如果这篇文章有帮到你,或者你也有更好的自定义导航栏实现思路,欢迎在评论区和我交流呀!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐