自定义 TabBar 实战:浮动标签栏与舵式标签栏

文章目录

    • [一、为什么需要自定义 TabBar?](#一、为什么需要自定义 TabBar?)
    • 二、公共数据类型
    • 三、浮动标签栏
      • [3.1 视觉效果图](#3.1 视觉效果图)
      • [3.2 FloatingTabBar 核心实现](#3.2 FloatingTabBar 核心实现)
      • [3.3 父页面:Stack 叠层组合](#3.3 父页面:Stack 叠层组合)
    • 四、舵式标签栏
      • [4.1 视觉结构](#4.1 视觉结构)
      • [4.2 SteeredTabBar 核心实现](#4.2 SteeredTabBar 核心实现)
      • [4.3 与 Swiper 联动实现手势滑动](#4.3 与 Swiper 联动实现手势滑动)
    • 五、尺寸规范参考
    • 总结

一、为什么需要自定义 TabBar?

系统默认的 Tabs 组件能快速搭建标准底部导航,但遇到以下场景时无能为力:

场景 系统 Tabs 是否支持
标准底部导航(图标+文字) 支持,直接用 BarPosition.End
带阴影和圆角的浮动标签栏 不支持,需完全自定义
突出中间项的舵式标签栏 不支持,需完全自定义
选中指示条动画 有限,需配合自定义实现

二、公共数据类型

两种标签栏都依赖项目的 TabItemTabBarTheme 接口:

typescript 复制代码
// entry/src/main/ets/types/TabBarTypes.ets

export interface TabItem {
  id: string;           // 唯一标识
  icon: Resource;       // 未选中图标
  activeIcon: Resource; // 选中图标
  title: string;        // 标签文字
  badge?: number;       // 角标数量(可选)
  showDot?: boolean;    // 是否显示红点(可选)
}

export interface TabBarTheme {
  backgroundColor: ResourceColor;
  activeColor: ResourceColor;
  inactiveColor: ResourceColor;
  height: number;
  iconSize: number;
  fontSize: number;
}

代码说明:把数据结构和主题配置拆成独立接口,修改外观只需换一个主题对象,UI 代码不需要改动。

三、浮动标签栏

3.1 视觉效果图

3.2 FloatingTabBar 核心实现

typescript 复制代码
 

@Component
struct FloatingTabBar {
  @Link currentIndex: number;  // 双向绑定,子组件可直接修改父组件状态
  @Prop theme: TabBarTheme;
  @Prop tabs: TabItem[];
  onTabChange?: (index: number) => void;

  build() {
    Row() {
      ForEach(this.tabs, (tab: TabItem, index: number) => {
        Column() {
          SymbolGlyph(this.currentIndex === index ? tab.activeIcon : tab.icon)
            .fontSize(24)
            // SymbolGlyph.fontColor 接受 ResourceColor[] 数组,必须用方括号包裹
            .fontColor(
              this.currentIndex === index
                ? [this.theme.activeColor]
                : [this.theme.inactiveColor]
            )

          Text(tab.title)
            .fontSize(this.theme.fontSize)
            // Text.fontColor 直接传 ResourceColor,无需数组
            .fontColor(
              this.currentIndex === index
                ? this.theme.activeColor
                : this.theme.inactiveColor
            )
            .margin({ top: 2 })
            .fontWeight(
              this.currentIndex === index ? FontWeight.Medium : FontWeight.Normal
            )

          // 选中指示条:使用 if/else + transition 实现淡入动画
          if (this.currentIndex === index) {
            Row()
              .width(20).height(3)
              .backgroundColor(this.theme.activeColor)
              .borderRadius(1.5)
              .margin({ top: 4 })
              .transition({ type: TransitionType.Insert, opacity: 0 })
              .animation({ duration: 200, curve: Curve.EaseInOut })
          } else {
            Row().width(20).height(3).margin({ top: 4 })
          }
        }
        .width(64)
        .height(this.theme.height)
        .justifyContent(FlexAlign.Center)
        .onClick(() => { this.onTabChange?.(index); })
      })
    }
    .height(this.theme.height + 16)
    .padding({ left: 16, right: 16, top: 8, bottom: 8 })
    .margin({ left: 24, right: 24, bottom: 20 }) // 不贴边,四周留白
    .backgroundColor(this.theme.backgroundColor)
    .borderRadius(28)                             // 大圆角
    .justifyContent(FlexAlign.SpaceEvenly)
    .shadow({
      radius: 20,
      color: 'rgba(0, 0, 0, 0.08)',
      offsetX: 0,
      offsetY: 4                                  // 向下偏移,模拟悬浮投影
    })
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
  }
}

代码说明:

  • @Link 双向绑定,父组件传递 $currentIndex,子组件内部可直接读写,两侧数据同步;
  • SymbolGlyph.fontColor() 参数类型是 ResourceColor[],一定要写成数组形式;而 Text.fontColor() 参数是 ResourceColor,不需要数组;
  • 指示条用 if/else 条件渲染配合 transition,这样在元素插入时可以触发淡入动画;
  • .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 让标签栏延伸到系统导航条区域,避免被遮挡。

3.3 父页面:Stack 叠层组合

typescript 复制代码
@Entry
@Component
struct FloatingTabsDemo {
  @State currentIndex: number = 0;
  @State theme: TabBarTheme = FloatingTheme;
  @State tabs: TabItem[] = [];

  build() {
    Stack({ alignContent: Alignment.Bottom }) {  // Stack 让标签栏叠在内容上方
      Column() {
        FloatingContentPage({ ... })
      }
      .width('100%').height('100%')

      FloatingTabBar({
        currentIndex: $currentIndex,   // $ 传递双向绑定引用
        theme: this.theme,
        tabs: this.tabs,
        onTabChange: (index: number) => {
          animateTo({ duration: 250, curve: Curve.EaseInOut }, () => {
            this.currentIndex = index;
          });
        }
      })
    }
    .width('100%').height('100%')
  }
}

代码说明:Stack 布局是浮动标签栏的关键,它让标签栏悬浮在内容上方,而不是和内容并排布局。animateTo 包裹状态变更,为内容区切换添加过渡动画。


四、舵式标签栏

4.1 视觉结构

标签数量必须是奇数(3、5、7),保证中间项落在正中央。

4.2 SteeredTabBar 核心实现

typescript 复制代码
// entry/src/main/ets/pages/demos/SteeredTabsDemo.ets

@Component
struct SteeredTabBar {
  @Link currentIndex: number;
  @State tabs: TabItem[] = [];

  // 动态计算中间索引,支持任意奇数个标签
  private isCenterItem(index: number): boolean {
    return index === Math.floor(this.tabs.length / 2);
  }

  build() {
    Row() {
      ForEach(this.tabs, (tab: TabItem, index: number) => {
        if (this.isCenterItem(index)) {
          // 中间突出项:圆形背景
          Column() {
            SymbolGlyph(this.currentIndex === index ? tab.activeIcon : tab.icon)
              .fontSize(28)
              .fontColor(['#FFFFFF'])
          }
          .width(56).height(56)
          .backgroundColor('#007AFF')
          .borderRadius(28)  // 宽高一半 = 正圆
          .justifyContent(FlexAlign.Center)
          .shadow({
            radius: 8,
            color: 'rgba(0, 122, 255, 0.3)', // 阴影颜色与背景色呼应
            offsetX: 0,
            offsetY: 4
          })
          .onClick(() => { this.currentIndex = index; })

        } else {
          // 普通项
          Column() {
            SymbolGlyph(this.currentIndex === index ? tab.activeIcon : tab.icon)
              .fontSize(24)
              .fontColor(this.currentIndex === index ? ['#007AFF'] : ['#8E8E93'])

            Text(tab.title)
              .fontSize(11)
              .fontColor(this.currentIndex === index ? '#007AFF' : '#8E8E93')
              .margin({ top: 2 })
          }
          .width(60).height(50)
          .justifyContent(FlexAlign.Center)
          .onClick(() => { this.currentIndex = index; })
        }
      })
    }
    .width('100%').height(70)
    .padding({ bottom: 8 })
    .justifyContent(FlexAlign.SpaceEvenly)
    .alignItems(VerticalAlign.Bottom)  // 底部对齐,中间圆形按钮自然向上凸出
  }
}

代码说明:

  • isCenterItem()Math.floor(length / 2) 动态计算中间索引,标签数量变化无需改代码;
  • 圆形通过 borderRadius 等于宽高的一半来实现,width(56) + borderRadius(28) 即正圆;
  • alignItems(VerticalAlign.Bottom) 让所有项底部对齐,中间项更高,视觉上自然向上凸起;
  • 中间项的阴影颜色用带透明度的蓝色,与背景色呼应,比黑色阴影更精致。

4.3 与 Swiper 联动实现手势滑动

本项目的舵式标签栏使用 Swiper 作为内容区,支持手势滑动:

typescript 复制代码
Swiper() {
  ForEach(this.contents, (item: string[], index: number) => {
    ContentPage({ ... })
  })
}
.index(this.currentIndex)  // 与 currentIndex 绑定
.loop(false)
.indicator(false)           // 隐藏默认指示点,标签栏已起到指示作用
.duration(300)
.onChange((index: number) => {
  this.currentIndex = index;  // 手势滑动时同步更新标签选中状态
})

代码说明:SwiperSteeredTabBar 共享同一个 currentIndex,互相监听 onChange,任一方的变化都会驱动另一方更新,无需手动同步。


五、尺寸规范参考

场景 属性 推荐值
浮动 左右外边距 20-24vp
浮动 圆角半径 24-28vp
浮动 阴影模糊半径 16-24vp
舵式 中间项直径 52-60vp
舵式 标签数量 3、5 或 7(奇数)

总结

浮动标签栏和舵式标签栏看起来效果很炫,但核心原理其实很简单:前者靠 Stack 叠层、圆角和 shadow 实现悬浮感,后者靠 borderRadius 等于宽高一半画出正圆突出中间项。两种样式背后的逻辑都是:把 barHeight 设为 0 隐藏系统默认标签栏,然后自己用组件拼出想要的 UI。希望这两个完整的例子能让你在拿到设计稿时,少走一些弯路,直接动手就能写出来。

相关推荐
前端不太难7 小时前
从单页面到系统化:鸿蒙 App 演进路径
华为·状态模式·harmonyos
想你依然心痛9 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“文思智脑“——PC端AI智能体沉浸式智能写作工作台
人工智能·ar·harmonyos·ai写作
小雨青年9 小时前
鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 09:展开态列表增加字段但不变复杂
华为·harmonyos
richard_yuu9 小时前
鸿蒙治愈游戏模块实战|四大轻量解压游戏、ArkTS动画交互与低功耗落地
游戏·交互·harmonyos
阿钱真强道13 小时前
24 鸿蒙LiteOS GPIO中断实战:从原理到上升沿/下降沿详解
harmonyos·中断·rk·liteos·开源鸿蒙·瑞芯微·rk2206
cd_9492172115 小时前
鸿蒙系统下抖音存储空间不足怎么办?缓存清理教程
缓存·华为·harmonyos
轻口味18 小时前
HarmonyOS 6.1 全栈实战录 - 14 渲染树透镜:FrameNode 渲染状态感知与高性能 UI 调优实战
ui·华为·harmonyos
HwJack2018 小时前
HarmonyOS NEXT 游戏APP开发中如何正确拦截退出手势
游戏·华为·harmonyos
HwJack2018 小时前
HarmonyOS APP开发中ArkTS/JS 类型错误全景拆解
javascript·华为·harmonyos
lqj_本人19 小时前
鸿蒙PC:鸿蒙版本 Electron 框架环境搭建并且实现 XH 笔记应用
笔记·electron·harmonyos