在这里插入图片描述

前言

底部导航栏是移动应用中最常见的导航模式之一,它为用户提供了在应用主要功能模块之间快速切换的能力。在商城应用中,底部导航栏通常包含首页、分类、购物车、我的等核心入口,用户可以通过点击不同的标签页快速访问对应的功能模块。本文将详细介绍如何在Flutter和OpenHarmony平台上开发一个功能完善、视觉美观的底部导航栏组件。

一个优秀的底部导航栏设计需要考虑多个方面:图标的选择要直观易懂,让用户一眼就能理解每个标签的功能;选中状态的视觉反馈要明显,帮助用户确认当前所在位置;购物车角标要能实时显示商品数量,提醒用户购物车状态;整体样式要与应用的设计风格保持一致,营造统一的视觉体验。

Flutter底部导航栏基础结构

首先定义导航项的数据模型:

class NavItem {
  final String label;
  final IconData icon;
  final IconData activeIcon;
  final int? badge;

  const NavItem({
    required this.label,
    required this.icon,
    required this.activeIcon,
    this.badge,
  });
}

NavItem类定义了导航项的基本属性。label是标签文字,显示在图标下方帮助用户理解功能含义。icon和activeIcon分别是未选中和选中状态的图标,使用不同的图标样式可以增强选中状态的视觉反馈。badge是可选的角标数字,用于显示购物车商品数量或未读消息数等信息。这种数据模型的设计使得导航项的配置更加灵活,可以根据业务需求动态调整每个标签的显示内容。

定义导航栏组件:

class BottomNavBar extends StatelessWidget {
  final int currentIndex;
  final List<NavItem> items;
  final ValueChanged<int> onTap;

  const BottomNavBar({
    Key? key,
    required this.currentIndex,
    required this.items,
    required this.onTap,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      height: 56,
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: Row(
        children: List.generate(
          items.length,
          (index) => _buildNavItem(index),
        ),
      ),
    );
  }
}

BottomNavBar组件采用StatelessWidget实现,因为导航栏的状态由父组件管理。currentIndex表示当前选中的标签索引,items是导航项列表,onTap回调在用户点击标签时触发。Container设置了56像素的标准高度,这是Material Design推荐的底部导航栏高度。顶部阴影使用向上偏移的BoxShadow,营造出导航栏悬浮在内容之上的视觉效果。Row组件水平排列所有导航项,List.generate方法根据items列表动态生成子组件。

导航项组件实现

Widget _buildNavItem(int index) {
  final item = items[index];
  final isSelected = index == currentIndex;
  
  return Expanded(
    child: GestureDetector(
      onTap: () => onTap(index),
      behavior: HitTestBehavior.opaque,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          _buildIcon(item, isSelected),
          const SizedBox(height: 4),
          Text(
            item.label,
            style: TextStyle(
              fontSize: 10,
              color: isSelected 
                ? const Color(0xFFE53935) 
                : const Color(0xFF999999),
            ),
          ),
        ],
      ),
    ),
  );
}

每个导航项使用Expanded包裹,确保所有标签平均分配水平空间。GestureDetector的behavior设置为HitTestBehavior.opaque,使整个区域都可以响应点击事件,而不仅仅是子组件所在的区域。Column垂直排列图标和文字,mainAxisAlignment设置为center使内容垂直居中。文字颜色根据选中状态动态变化,选中时显示主题红色,未选中时显示灰色。10像素的字号在保证可读性的同时不会喧宾夺主,让用户的注意力集中在图标上。

图标组件的实现:

Widget _buildIcon(NavItem item, bool isSelected) {
  return Stack(
    clipBehavior: Clip.none,
    children: [
      Icon(
        isSelected ? item.activeIcon : item.icon,
        size: 24,
        color: isSelected 
          ? const Color(0xFFE53935) 
          : const Color(0xFF999999),
      ),
      if (item.badge != null && item.badge! > 0)
        Positioned(
          right: -8,
          top: -4,
          child: _buildBadge(item.badge!),
        ),
    ],
  );
}

图标组件使用Stack实现图标和角标的层叠布局。clipBehavior设置为Clip.none允许角标超出Stack的边界显示。Icon组件根据选中状态显示不同的图标和颜色,24像素是标准的导航图标尺寸。Positioned组件将角标定位在图标的右上角,负值的right和top使角标部分超出图标边界,这是常见的角标设计方式。条件渲染确保只有当badge存在且大于0时才显示角标,避免显示无意义的空角标。

角标组件实现

Widget _buildBadge(int count) {
  final displayText = count > 99 ? '99+' : count.toString();
  
  return Container(
    padding: const EdgeInsets.symmetric(
      horizontal: 4,
      vertical: 1,
    ),
    constraints: const BoxConstraints(minWidth: 16),
    decoration: BoxDecoration(
      color: const Color(0xFFE53935),
      borderRadius: BorderRadius.circular(8),
    ),
    child: Text(
      displayText,
      style: const TextStyle(
        fontSize: 10,
        color: Colors.white,
        fontWeight: FontWeight.w500,
      ),
      textAlign: TextAlign.center,
    ),
  );
}

角标组件用于显示购物车商品数量等数字信息。当数量超过99时显示"99+",避免角标过宽影响布局美观。Container设置了最小宽度约束,确保单位数字时角标也能保持圆形外观。红色背景和白色文字形成强烈对比,确保角标在各种背景下都清晰可见。圆角半径设为8像素,配合16像素的最小宽度,使角标呈现圆润的胶囊形状。这种角标设计在各大主流应用中广泛使用,用户已经形成了认知习惯。

OpenHarmony底部导航栏实现

@Component
struct BottomNavBar {
  @Prop currentIndex: number
  @State items: NavItemInfo[] = []
  private onTabChange: (index: number) => void = () => {}

  build() {
    Row() {
      ForEach(this.items, (item: NavItemInfo, index: number) => {
        this.NavItem(item, index)
      })
    }
    .width('100%')
    .height(56)
    .backgroundColor(Color.White)
    .shadow({
      radius: 10,
      color: '#0D000000',
      offsetX: 0,
      offsetY: -2
    })
  }
}

OpenHarmony的底部导航栏使用@Component装饰器定义。@Prop装饰器标记currentIndex从父组件接收当前选中索引,@State装饰器标记items为组件内部状态。ForEach函数遍历items数组,为每个导航项生成对应的UI组件。Row容器水平排列所有导航项,设置100%宽度和56像素高度。shadow方法创建顶部阴影效果,颜色使用带透明度的十六进制值,offsetY为负值使阴影向上偏移。这种实现方式与Flutter版本保持一致的视觉效果。

导航项数据接口定义:

interface NavItemInfo {
  label: string
  icon: Resource
  activeIcon: Resource
  badge?: number
}

TypeScript的interface定义了导航项的类型结构。label是标签文字,icon和activeIcon是Resource类型的图标资源引用,badge是可选的角标数字。Resource类型是ArkUI中引用应用资源的标准方式,通过$r函数获取。可选属性使用问号标记,表示该属性可以不存在。这种类型定义为组件提供了类型安全保障,在编译时就能发现类型错误。

导航项组件ArkUI实现

@Builder
NavItem(item: NavItemInfo, index: number) {
  Column() {
    this.NavIcon(item, index)
    Text(item.label)
      .fontSize(10)
      .fontColor(this.currentIndex === index ? '#E53935' : '#999999')
      .margin({ top: 4 })
  }
  .layoutWeight(1)
  .justifyContent(FlexAlign.Center)
  .height('100%')
  .onClick(() => {
    this.onTabChange(index)
  })
}

@Builder装饰器定义了导航项的构建方法。Column垂直排列图标和文字,layoutWeight(1)使所有导航项平均分配宽度。justifyContent设置为FlexAlign.Center使内容垂直居中。Text组件的fontColor根据当前索引动态设置,实现选中状态的颜色变化。onClick事件处理器调用onTabChange回调,将点击的索引传递给父组件。这种声明式的UI构建方式使代码结构清晰,易于理解和维护。

图标组件的实现:

@Builder
NavIcon(item: NavItemInfo, index: number) {
  Stack() {
    Image(this.currentIndex === index ? item.activeIcon : item.icon)
      .width(24)
      .height(24)
    
    if (item.badge && item.badge > 0) {
      this.Badge(item.badge)
    }
  }
}

Stack容器实现图标和角标的层叠显示。Image组件根据选中状态加载不同的图标资源,三元表达式简洁地实现了条件判断。条件渲染使用if语句,只有当badge存在且大于0时才渲染角标组件。这种实现方式与Flutter的Stack和Positioned组合效果相同,但ArkUI的语法更加简洁直观。

角标组件ArkUI实现

@Builder
Badge(count: number) {
  Text(count > 99 ? '99+' : count.toString())
    .fontSize(10)
    .fontColor(Color.White)
    .fontWeight(FontWeight.Medium)
    .textAlign(TextAlign.Center)
    .backgroundColor('#E53935')
    .borderRadius(8)
    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
    .constraintSize({ minWidth: 16 })
    .position({ x: 12, y: -4 })
}

角标组件直接使用Text组件实现,通过链式调用设置所有样式属性。constraintSize方法设置最小宽度约束,确保单位数字时角标保持圆形。position方法设置角标相对于父容器的位置偏移,x值为12使角标位于图标右侧,y值为-4使角标向上偏移。这种定位方式比Flutter的Positioned更加直观,直接指定坐标值即可。

页面切换集成

class MainPage extends StatefulWidget {
  
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;
  
  final List<Widget> _pages = [
    const HomePage(),
    const CategoryPage(),
    const CartPage(),
    const ProfilePage(),
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: BottomNavBar(
        currentIndex: _currentIndex,
        items: _navItems,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );
  }
}

主页面使用StatefulWidget管理当前选中的标签索引。IndexedStack组件保持所有页面的状态,切换标签时不会重建页面,用户返回之前的标签时可以看到离开时的状态。这种实现方式适合需要保持页面状态的场景,但会增加内存占用。bottomNavigationBar属性将自定义导航栏放置在页面底部,onTap回调更新currentIndex触发页面切换。Scaffold提供了标准的页面结构,自动处理安全区域等问题。

总结

本文详细介绍了Flutter和OpenHarmony平台上底部导航栏组件的开发过程。底部导航栏作为应用的核心导航组件,其设计质量直接影响用户的操作效率和使用体验。通过合理的组件拆分和状态管理,我们实现了一个功能完善、视觉美观的底部导航栏组件。在实际项目中,还可以进一步添加切换动画、手势滑动切换等高级特性,为用户提供更加流畅的导航体验。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐