A hand-drawn doodle illustration on pure white pap

前言

做 App 首页,十有八九要用到底部导航栏。我之前用自己写的方式实现,代码一堆,切换动画还卡。后来发现 HarmonyOS7 的 Tabs 组件,5 分钟就能搞定,而且自带丝滑切换。

这篇聊聊 Tabs 组件怎么用,从基础布局到自定义 TabBar,再到懒加载和 Navigation 联动,一次讲清楚。
说实话,我一开始没把 Tabs 当回事。觉得不就是个底部导航嘛,自己画几个图标不就完了?结果自己写了一版,切换的时候白屏闪烁,状态还各种丢。痛定思痛,老老实实用 Tabs,才发现真香——系统组件帮你想到了很多东西。

Tabs 组件能干什么

简单说,Tabs 就是一个页签容器,每个页签(TabContent)对应一个内容区,点击页签切换内容。

A hand-drawn doodle illustration on pure white pap

它能做的事:

  • 底部导航栏:微信那种底部 4 个 Tab,点击切换页面
  • 顶部标签页:今日头条那种频道切换
  • 侧边导航:设置页面的分类导航
  • 自带滑动切换:手指左右滑动就能切页
  • 预加载支持:可以提前加载相邻页签,切过去不卡

划重点:Tabs 的子组件只能是 TabContent,而 TabContent 的直接子组件只能有一个。需要多个组件就套一个 Column 或 Row。

基础 Tab 布局

最简单的 Tabs,3 行代码搞定:

Tabs() {
  TabContent() {
    Text('首页内容')
  }.tabBar('首页')

  TabContent() {
    Text('消息内容')
  }.tabBar('消息')

  TabContent() {
    Text('我的内容')
  }.tabBar('我的')
}
.barPosition(BarPosition.End)

逐行讲解:

  • Tabs() 是容器,里面放 TabContent 子组件
  • 每个 TabContent() 包裹当前页签的内容

A hand-drawn doodle illustration on pure white pap

  • .tabBar('首页') 设置页签栏显示的文字
  • .barPosition(BarPosition.End) 把导航栏放底部,默认是在顶部的

就这样,一个能滑动能点击的底部导航就出来了。不过默认样式比较素,实际开发肯定要自定义。

自定义 TabBar 样式

默认的纯文字 TabBar 太丑了,加上图标和选中态才像样。这就需要自定义 TabBar:

@Entry
@Component
struct MainPage {
  @State currentIndex: number = 0
  private tabsController: TabsController = new TabsController()

  @Builder
  tabItemBuilder(icon: Resource, text: string, index: number) {
    Column() {
      Image(this.currentIndex === index ? icon : $r('app.media.ic_default'))
        .width(24)
        .height(24)
        .fillColor(this.currentIndex === index ? '#007DFF' : '#999999')

      Text(text)
        .fontSize(10)
        .fontColor(this.currentIndex === index ? '#007DFF' : '#999999')
        .margin({ top: 2 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  build() {
    Column() {
      Tabs({ controller: this.tabsController, index: this.currentIndex }) {
        TabContent() {
          Text('首页内容区')
        }
        .tabBar(this.tabItemBuilder($r('app.media.ic_home_selected'), '首页', 0))

        TabContent() {
          Text('消息内容区')
        }
        .tabBar(this.tabItemBuilder($r('app.media.ic_msg_selected'), '消息', 1))

        TabContent() {
          Text('我的内容区')
        }
        .tabBar(this.tabItemBuilder($r('app.media.ic_mine_selected'), '我的', 2))
      }
      .barPosition(BarPosition.End)
      .onChange((index: number) => {
        this.currentIndex = index
      })
    }
    .width('100%')
    .height('100%')
  }
}

关键代码讲解:

  • @State currentIndex 跟踪当前选中页签,必须用 @State,否则切换不刷新 UI
  • TabsController 控制器,可以代码跳转页签,比如 this.tabsController.changeIndex(2)
  • @Builder tabItemBuilder 是自定义 TabBar 的核心——根据 currentIndex 判断选中态,切换图标和颜色
  • .onChange() 监听页签切换,更新 currentIndex
  • icon: Resource 参数类型用 Resource,这样能用 $r() 引用资源

小技巧:选中图标和未选中图标用同一个资源,通过 .fillColor() 改颜色就行,省得切两套图。

Tab 内容懒加载

Tabs 默认会把所有 TabContent 一口气加载。3 个 Tab 还好,要是 5 个 6 个,每个里面还请求数据,启动就卡成狗。

HarmonyOS7 提供了 preloadItems 方法,可以按需预加载:

.onAppear(() => {
  try {
    this.tabsController.preloadItems([1])
  } catch (e) {
    console.error('预加载失败: ' + e)
  }
})
.onChange((index: number) => {
  this.currentIndex = index
  let preloadList: number[] = []
  if (index > 0) {
    preloadList.push(index - 1)
  }
  if (index < 4) {
    preloadList.push(index + 1)
  }
  try {
    this.tabsController.preloadItems(preloadList)
  } catch (e) {
    console.error('预加载失败: ' + e)
  }
})

逻辑说明:

  • onAppear 里预加载第 2 个页签,用户大概率会切过去
  • onChange 里预加载当前页签的前后各一页
  • preloadItems 必须在 Tabs 和 Controller 绑定之后调用,放 aboutToAppear 里会不生效
  • 务必 try-catch,Controller 没绑定时会抛异常

说白了就是:不提前全加载,切到哪个附近就预加载哪个。用户体验不差,性能还好。

与 Navigation 联动

实际项目中,Tabs 通常嵌在 Navigation 的首页里,子页面用 NavDestination 跳转。这是最经典的组合:

@Entry
@Component
struct IndexPage {
  @Provide navPathStack: NavPathStack = new NavPathStack()
  @State currentIndex: number = 0

  build() {
    Navigation(this.navPathStack) {
      Tabs({ index: this.currentIndex }) {
        TabContent() {
          HomePage()
        }.tabBar('首页')

        TabContent() {
          MessagePage()
        }.tabBar('消息')

        TabContent() {
          MinePage()
        }.tabBar('我的')
      }
      .barPosition(BarPosition.End)
      .onChange((index: number) => {
        this.currentIndex = index
      })
    }
    .navDestination(this.pageMap)
    .hideTitleBar(true)
  }

  @Builder
  pageMap(name: string) {
    if (name === 'DetailPage') {
      DetailPage()
    } else if (name === 'SettingsPage') {
      SettingsPage()
    }
  }
}

要点:

  • Navigation 包在最外层,Tabs 作为首页内容
  • @Provide navPathStack 通过 @Consume 在子页面里就能拿到,不用层层传参
  • 子页面跳转用 this.navPathStack.pushPathByName('DetailPage', null)
  • navDestination 做路由映射,名字对应组件

这种结构的好处是:Tab 页签内的页面跳转和 Tab 切换互不干扰,各管各的。

写在最后

Tabs 组件说简单也简单,3 行代码能跑;说复杂也复杂,自定义样式、懒加载、Navigation 联动都要考虑。我的建议是:先用最简版跑起来,再逐步加自定义和优化。别一上来就搞复杂的自定义 TabBar,容易把自己绕进去。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐