HarmonyOS掌上记账APP开发实践第21篇:Tabs 底部导航 — 鸿蒙应用的标准导航范式

Tabs 底部导航 --- 鸿蒙应用的标准导航范式

一、引言

底部导航栏是移动应用中最常见的主框架导航模式。鸿蒙系统通过 Tabs + TabContent 组件组合,为开发者提供了声明式、高性能的底部导航方案。Tabs 组件支持自定义标签栏、手势切换、动画模式等丰富能力,可以满足从简单到复杂的各种导航需求。MoneyTrack 在 entry 模块的主入口中采用 Tabs 实现首页、账单、统计、我的四个标签页的切换,通过自定义 TabBar 实现了品牌化的图标与文案展示。

二、核心知识点

2.1 Tabs + TabContent 组合

Tabs 是容器组件,TabContent 是每个标签页的内容区域。每个 TabContent 通过 tabBar 属性配置对应的标签栏按钮。Tabs 管理各 TabContent 的显示切换,内部采用懒加载机制,默认只渲染当前激活的 TabContent,切换时销毁并重建(或被缓存),兼顾性能和内存。

typescript 复制代码
Tabs({ barPosition: BarPosition.End }) {
  TabContent() {
    HomePage()
  }.tabBar(this.tabBarBuilder(0))

  TabContent() {
    BillsPage()
  }.tabBar(this.tabBarBuilder(1))
  // ...
}

2.2 属性详解

Tabs 提供了丰富的属性用于控制行为和样式:

属性 类型 说明
barPosition BarPosition 标签栏位置:Start(顶)/ End(底)/ Left / Right
swipeable boolean 是否支持手势滑动切换标签,默认 true
scrollable boolean 标签栏是否可滚动(标签过多时)
animationMode AnimationMode 切换动画:None(无动画)/ Spring(弹性动画)
barWidth number 标签栏宽度,默认充满父容器
barHeight number 标签栏高度,可自定义
vertical boolean 是否为纵向布局,侧边栏场景使用

2.3 barPosition 位置控制

barPosition 属性控制标签栏的位置:

  • BarPosition.End:底部(移动端标准)
  • BarPosition.Start:顶部(配合可滚动 TabBar)
  • BarPosition.Left/Right:侧边栏(平板横屏场景)

MoneyTrack 采用 BarPosition.End 实现底部导航。

2.4 动画模式

Tabs 支持 animationMode 属性配置切换动画:

  • AnimationMode.None:无动画,瞬间切换,适合快速切换场景。
  • AnimationMode.Spring:弹性动画,效果流畅,适合注重用户体验的场景。

Spring 动画会跟随手指滑动产生惯性效果,切换过程自然不突兀。

三、项目代码案例

3.1 MainEntry.ets 中的 TAB_LIST 配置

products/entry/src/main/ets/views/MainEntry.ets 中,定义了包含四个标签的 TAB_LIST:

索引 标签名 图标(选中) 图标(未选中)
0 首页 home_filled home_outlined
1 账单 bill_filled bill_outlined
2 统计 stats_filled stats_outlined
3 我的 mine_filled mine_outlined

3.2 tabBarBuilder 自定义实现

tabBarBuilder 函数接收索引参数,根据当前选中状态动态渲染图标颜色和文字样式:

typescript 复制代码
@Builder
tabBarBuilder(index: number) {
  Column() {
    Image(this.currentIndex === index
      ? TAB_LIST[index].iconFilled
      : TAB_LIST[index].iconOutlined)
      .width(24).height(24)
    Text(TAB_LIST[index].label)
      .fontSize(10)
      .fontColor(this.currentIndex === index
        ? $r('app.color.primary_blue')
        : $r('app.color.text_secondary'))
  }
  .width('100%')
  .padding({ top: 4 })
  .justifyContent(FlexAlign.Center)
}

3.3 TabController 控制跳转

TabController 提供了编程式标签切换能力,适用于外部事件触发导航的场景:

typescript 复制代码
private tabController: TabController = new TabController()

// 外部触发切换到指定标签
jumpToBillTab() {
  this.tabController.changeIndex(1)  // 切换到账单页
}

// 在 Tabs 中传入 controller
Tabs({ controller: this.tabController, barPosition: BarPosition.End }) {
  // TabContent 列表...
}

3.4 页面懒加载实现

通过 onChange 事件与条件渲染结合,实现页面懒加载------只有标签被激活时才创建对应页面组件:

typescript 复制代码
@State currentIndex: number = 0
@State loadedPages: boolean[] = [true, false, false, false]

build() {
  Tabs({ barPosition: BarPosition.End }) {
    ForEach(TAB_LIST, (item: TabItem, index: number) => {
      TabContent() {
        if (this.loadedPages[index]) {
          this.getPageComponent(index)
        }
      }.tabBar(this.tabBarBuilder(index))
    })
  }
  .onChange((index: number) => {
    this.currentIndex = index
    if (!this.loadedPages[index]) {
      this.loadedPages[index] = true  // 首次激活时标记加载
    }
  })
}

四、Mermaid 图示:Tabs 层级结构

#mermaid-svg-SFK2epcXfxSD1Wcf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SFK2epcXfxSD1Wcf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SFK2epcXfxSD1Wcf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SFK2epcXfxSD1Wcf .error-icon{fill:#552222;}#mermaid-svg-SFK2epcXfxSD1Wcf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SFK2epcXfxSD1Wcf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SFK2epcXfxSD1Wcf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SFK2epcXfxSD1Wcf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SFK2epcXfxSD1Wcf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SFK2epcXfxSD1Wcf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SFK2epcXfxSD1Wcf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SFK2epcXfxSD1Wcf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SFK2epcXfxSD1Wcf .marker.cross{stroke:#333333;}#mermaid-svg-SFK2epcXfxSD1Wcf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SFK2epcXfxSD1Wcf p{margin:0;}#mermaid-svg-SFK2epcXfxSD1Wcf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SFK2epcXfxSD1Wcf .cluster-label text{fill:#333;}#mermaid-svg-SFK2epcXfxSD1Wcf .cluster-label span{color:#333;}#mermaid-svg-SFK2epcXfxSD1Wcf .cluster-label span p{background-color:transparent;}#mermaid-svg-SFK2epcXfxSD1Wcf .label text,#mermaid-svg-SFK2epcXfxSD1Wcf span{fill:#333;color:#333;}#mermaid-svg-SFK2epcXfxSD1Wcf .node rect,#mermaid-svg-SFK2epcXfxSD1Wcf .node circle,#mermaid-svg-SFK2epcXfxSD1Wcf .node ellipse,#mermaid-svg-SFK2epcXfxSD1Wcf .node polygon,#mermaid-svg-SFK2epcXfxSD1Wcf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SFK2epcXfxSD1Wcf .rough-node .label text,#mermaid-svg-SFK2epcXfxSD1Wcf .node .label text,#mermaid-svg-SFK2epcXfxSD1Wcf .image-shape .label,#mermaid-svg-SFK2epcXfxSD1Wcf .icon-shape .label{text-anchor:middle;}#mermaid-svg-SFK2epcXfxSD1Wcf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SFK2epcXfxSD1Wcf .rough-node .label,#mermaid-svg-SFK2epcXfxSD1Wcf .node .label,#mermaid-svg-SFK2epcXfxSD1Wcf .image-shape .label,#mermaid-svg-SFK2epcXfxSD1Wcf .icon-shape .label{text-align:center;}#mermaid-svg-SFK2epcXfxSD1Wcf .node.clickable{cursor:pointer;}#mermaid-svg-SFK2epcXfxSD1Wcf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SFK2epcXfxSD1Wcf .arrowheadPath{fill:#333333;}#mermaid-svg-SFK2epcXfxSD1Wcf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SFK2epcXfxSD1Wcf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SFK2epcXfxSD1Wcf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SFK2epcXfxSD1Wcf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SFK2epcXfxSD1Wcf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SFK2epcXfxSD1Wcf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SFK2epcXfxSD1Wcf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SFK2epcXfxSD1Wcf .cluster text{fill:#333;}#mermaid-svg-SFK2epcXfxSD1Wcf .cluster span{color:#333;}#mermaid-svg-SFK2epcXfxSD1Wcf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SFK2epcXfxSD1Wcf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SFK2epcXfxSD1Wcf rect.text{fill:none;stroke-width:0;}#mermaid-svg-SFK2epcXfxSD1Wcf .icon-shape,#mermaid-svg-SFK2epcXfxSD1Wcf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SFK2epcXfxSD1Wcf .icon-shape p,#mermaid-svg-SFK2epcXfxSD1Wcf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SFK2epcXfxSD1Wcf .icon-shape .label rect,#mermaid-svg-SFK2epcXfxSD1Wcf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SFK2epcXfxSD1Wcf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SFK2epcXfxSD1Wcf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SFK2epcXfxSD1Wcf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Tabs 容器组件
标签栏 bar
内容区域
tabBar 0 - 首页
tabBar 1 - 账单
tabBar 2 - 统计
tabBar 3 - 我的
TabContent 0 - HomePage
TabContent 1 - BillsPage
TabContent 2 - StatsPage
TabContent 3 - MinePage
属性配置
barPosition / swipeable
animationMode / barHeight

五、最佳实践

  1. 图标资源命名规范 :遵循 模块_filled模块_outlined 的命名约定,便于管理和批量替换。选中态使用实心图标,非选中态使用线性图标,视觉上明确区分层级。
  2. 懒加载策略 :默认只渲染首屏页面,其余标签页在首次激活时创建,避免首屏加载过多组件导致性能问题。可通过 loadedPages 标志数组控制。
  3. 动画选择 :使用 Spring 弹性动画提升切换手感,但注意在低端设备上可降级为 None 以保证帧率。
  4. TabController 统一管理:将 TabController 提升到全局或 ViewModel 层,方便从任意子页面触发 Tab 跳转,如未读消息提醒后自动跳转到"我的"页面。
  5. 角标适配:在 tabBarBuilder 中预留 Badge 位置,方便后续添加消息角标或红点提示。

六、总结

Tabs + TabContent 组合是鸿蒙应用底部导航的标准实现方案。通过自定义 tabBarBuilder 可以实现品牌化的视觉风格,通过 TabController 提供编程式控制能力,再结合懒加载策略和动画配置,可以构建高性能、高可用的主框架导航架构。配合规范的图标命名和状态管理,Tabs 组件是鸿蒙应用首屏框架的不二之选。Tabs 组件指南 | TabContent API

相关推荐
子非鱼94271 小时前
03-Flutter 鸿蒙实战 03:登录注册与本地会话持久化
网络·flutter·华为·harmonyos
子非鱼94271 小时前
01-Flutter 鸿蒙实战 01:会说话的相册项目架构拆解
flutter·华为·harmonyos
不羁的木木1 小时前
《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第10篇:多设备协同——构建星闪网络实现数据同步
网络·华为·harmonyos
春卷同学2 小时前
HarmonyOS掌上记账APP开发实践第29篇:条件渲染与 ForEach — 动态 UI 的构建策略
ui·华为·harmonyos
春卷同学2 小时前
HarmonyOS掌上记账APP开发实践第22篇:Scroll + List 虚拟列表 — 长列表性能优化全攻略
性能优化·list·harmonyos
不羁的木木2 小时前
《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第4篇:广播通信——一对多广播应用实现
华为·harmonyos
w139548564222 小时前
鸿蒙应用开发实战【91】— 完整功能串联从添加到换绑的全链路
华为·harmonyos·鸿蒙系统
2501_919749032 小时前
华为鸿蒙免费训练口才APP—小羊口才
华为·harmonyos
木木子222 小时前
# [特殊字符] 画画模拟器 — 鸿蒙ArkTS完整技术解析
华为·harmonyos