
问题从哪来
HarmonyOS NEXT 的底部页签在 API 12 以前主要靠 Tabs 组件硬撑。开发者自己调样式、自己管交互,每个项目写出来的底部栏长得都不一样。更麻烦的是页签和内容的层级关系------默认情况下列栏和内容区是上下排列的,想做出"悬浮""叠加""光感"效果,得自己叠层、算位置、处理事件穿透。这套逻辑本身不难,但每个项目都重写一遍就是纯体力活。
UI Design Kit 里的 HdsTabs 容器就是来解决这个问题的。它把底部页签常见的几种交互样式直接封装成了配置属性,不用再手动调层级。这篇文章只讲最基础的部分:怎么创建 HdsTabs、怎么让 tabBar 悬浮在内容之上、以及两个最核心的配置项 barOverlap 和 barPosition 到底是干什么的。
HdsTabs 解决了什么问题
普通 Tabs 组件布局是这样的:
+-------------------+
| TabContent 区域 |
+-------------------+
| tabBar 栏 |
+-------------------+
栏和内容区各自占位,互不重叠。这本身没问题,但一旦设计师要求"底栏要半透明悬浮在内容上""内容可以滑到底栏下面""底栏要有玻璃模糊效果",普通 Tabs 就有点吃力------需要自己写 position、zIndex、背景模糊,而且还要处理好内容区的内边距,防止内容被底栏挡住。
HdsTabs 容器把"叠加"这个行为变成了一个开关:barOverlap = true。打开后,tabBar 会悬浮在 TabContent 之上,不需要额外写任何布局代码。
barPosition 控制栏的位置,BarPosition.End 即底部。对于底部页签场景,这个值是固定的。
环境说明
text
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机
核心实现
第一步:导入模块
从 API 22 开始,HdsTabs 的导入方式有了变化。如果你用的是 HarmonyOS 6.1.0(23) 及以上版本,不需要手动导入 HdsTabsAttribute,直接导入组件和控制器就行。
typescript
import { HdsTabs, HdsTabsController } from '@kit.UIDesignKit';
如果你的 SDK 版本较旧(比如 6.0.0),需要额外导入属性类:
typescript
import { HdsTabs, HdsTabsAttribute, HdsTabsController } from '@kit.UIDesignKit';
这一点官方文档写得比较收敛------实际开发中,建议直接用最新版本,少一个导入就少一个出错点。
第二步:创建基础结构
下面是完整的 HdsTabs 容器示例,包含两个 TabContent 页签,并且开启了叠加效果。
typescript
@Entry
@Component
struct Index {
private controller: HdsTabsController = new HdsTabsController()
build() {
Column() {
HdsTabs({ controller: this.controller }) {
// 页签1
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#FFF3E0')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.children({
Text('首页内容')
.fontSize(20)
.fontColor('#333333')
})
}
.tabBar({ icon: $r('app.media.tab_home'), text: '首页' })
// 页签2
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#E3F2FD')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.children({
Text('发现内容')
.fontSize(20)
.fontColor('#333333')
})
}
.tabBar({ icon: $r('app.media.tab_explore'), text: '发现' })
}
// 关键属性:开启叠加,tabBar 悬浮在内容之上
.barOverlap(true)
// 关键属性:栏位置在底部
.barPosition(BarPosition.End)
// 必须为 false,否则叠加布局不生效
.vertical(false)
}
.width('100%')
.height('100%')
}
}
这段代码里需要重点关注三个属性:
| 属性 | 值 | 作用 |
|---|---|---|
barOverlap |
true |
让 tabBar 层叠在 TabContent 之上,而不是独占一行 |
barPosition |
BarPosition.End |
将 tabBar 定位到容器底部 |
vertical |
false |
横向布局模式,底部页签必须是这个值 |
如果你把 barOverlap 设为 false,tabBar 会回到普通 Tabs 的布局方式------栏和内容区各自占位,不叠加。 这个开关直接影响后续所有沉浸样式(模糊、悬浮、出血)的生效基础。
第三步:验证叠加效果
为了更直观地看到叠加发生,可以在 TabContent 底部留一些内容,验证内容是否被 tabBar 遮住。
typescript
TabContent() {
Column() {
// 填充区域
ForEach(Array.from({ length: 6 }), (_, index) => {
Row()
.width('100%')
.height(120)
.backgroundColor(index % 2 === 0 ? '#FFF8E1' : '#FFECB3')
.margin({ bottom: 8 })
})
// 这段文本在内容区底部,会被悬浮的 tabBar 遮住一部分
Text('底部内容,测试叠加效果')
.fontSize(14)
.fontColor('#999999')
.padding({ bottom: 100 }) // 避免被 tabBar 完全遮住
}
.width('100%')
.height('100%')
.padding(16)
}
.tabBar({ icon: $r('app.media.tab_home'), text: '首页' })

运行后可以看到,tabBar 浮在内容上方,内容可以滚动到 tabBar 下面。如果在真机上操作,滑动内容时 tabBar 不会跟随滚动,一直停留在底部------这和普通 Tabs 的行为不同,普通模式下 tabBar 会随着内容一起滚动。
第四步:控制内容不被 tabBar 遮挡
叠加效果打开后,TabContent 的底部会被 tabBar 遮住一部分。实际项目中需要给内容区留出安全边距,避免交互按钮或文字被遮挡。
常用做法是在内容区底部加一个占位 padding:
typescript
Column() {
// ... 内容
}
.padding({ bottom: 80 }) // tabBar 高度通常在 64vp 左右,留 80vp 保证安全
这个数值不是固定的。不同设备、不同主题下 tabBar 高度会有差异。更稳妥的方式是通过 HdsTabsController 获取 tabBar 的高度,或者直接用 LayoutWeight 让内容区自适应。
常见问题
问题1:设置了 barOverlap=true 但没有叠加效果
现象:tabBar 依然在内容区下方,没有悬浮。
原因 :vertical 没有显式设为 false。在 HdsTabs 容器中,vertical 的默认值是 true(纵向排列),这时 barOverlap 不会生效。
解决方案:
typescript
HdsTabs({ controller: this.controller }) {
// ...
}
.barOverlap(true)
.barPosition(BarPosition.End)
.vertical(false) // 必须显式设置
问题2:页签图标显示不出来
现象:tabBar 上的图标位置是空的,或者显示问号。
原因 :$r('app.media.xxx') 资源引用路径不对,或者资源文件没有被正确放到 media 目录下。
解决方案:
-
检查
src/main/resources/base/media/目录下是否存在对应文件 -
文件名不能包含中文和特殊字符
-
引用时不需要写文件后缀
// 正确
$r('app.media.tab_home')// 错误
r('app.media.tab_home.png') r('app.media.首页')
验证方法 :在 DevEco Studio 中按住 Ctrl 点击 $r 引用,如果能跳转到资源文件说明路径正确。
问题3:页面返回后 tabBar 状态丢失
现象:从二级页面返回后,底部页签的选中状态重置了,或者当前显示的页签不对。
原因 :HdsTabsController 的生命周期和页面绑定,页面销毁后控制器状态会丢失。
解决方案 :使用 LocalStorage 或 AppStorage 持久化当前选中页签的索引。
typescript
@Entry
@Component
struct Index {
@StorageLink('currentTabIndex') currentIndex: number = 0
private controller: HdsTabsController = new HdsTabsController()
onPageShow() {
// 页面显示时恢复选中状态
this.controller.changeIndex(this.currentIndex)
}
build() {
HdsTabs({ controller: this.controller, index: this.currentIndex }) {
// ...
}
.onChange((index: number) => {
this.currentIndex = index
})
}
}
最佳实践
1. barOverlap 配合 barBackgroundStyle 使用
只开叠加不加样式,底栏看起来就是一块纯色条,没有"沉浸光感"。建议配合 barBackgroundStyle 设置半透明遮罩或模糊效果,才能真正体现 HdsTabs 的设计意图。
typescript
.barOverlap(true)
.barPosition(BarPosition.End)
.vertical(false)
.barBackgroundStyle({
maskColor: 'rgba(255, 255, 255, 0.85)',
maskHeight: 80
})
为什么 :半透明遮罩让底栏和内容之间产生层次感,而不是生硬地叠在上面。maskHeight 控制遮罩的高度范围,可以让模糊效果从底栏向上渐变。(注:barBackgroundStyle 的具体参数在后续文章会详细展开,这里先有一个印象。)
2. 控制器不要重复创建
HdsTabsController 实例应该和组件同生命周期,不要每次 build 时 new 一个新的。
错误写法:
typescript
build() {
HdsTabs({ controller: new HdsTabsController() }) {
// ...
}
}
正确写法:
typescript
private controller: HdsTabsController = new HdsTabsController()
为什么:每次 build 新建控制器会导致页签状态重置,而且还会造成内存泄漏------旧控制器不会被释放。
3. 内容区 padding 不要硬编码
不同设备的 tabBar 高度可能不同,直接用固定 padding 容易出现适配问题。推荐通过 LayoutWeight 或者 Column 的弹性布局来分配空间。
typescript
Column() {
// 内容区占据剩余空间
Scroll() {
// ...
}
.layoutWeight(1)
// 底部预留安全区域
Row()
.height(64) // 与 tabBar 高度匹配
.width('100%')
}
为什么:这样即使 tabBar 高度在不同设备上发生变化,内容区也不会被遮挡或者留出过大空白。
完整 Demo 入口
typescript
// Index.ets
import { HdsTabs, HdsTabsController } from '@kit.UIDesignKit'
@Entry
@Component
struct Index {
private controller: HdsTabsController = new HdsTabsController()
@State private currentIndex: number = 0
build() {
Column() {
HdsTabs({ controller: this.controller, index: this.currentIndex }) {
TabContent() {
Column() {
Text('首页')
.fontSize(24)
.fontWeight(FontWeight.Bold)
ForEach(Array.from({ length: 8 }), (_, i) => {
Row()
.width('100%')
.height(80)
.backgroundColor(i % 2 === 0 ? '#FAFAFA' : '#F0F0F0')
.margin({ bottom: 4 })
.padding(16)
.children({
Text(`列表项 ${i + 1}`)
.fontSize(16)
})
})
Text('底部区域 - 验证叠加效果')
.fontSize(14)
.fontColor('#999999')
.padding({ top: 8, bottom: 72 })
}
.width('100%')
.height('100%')
.padding(16)
}
.tabBar({ icon: $r('app.media.tab_home'), text: '首页' })
TabContent() {
Column() {
Text('发现')
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 卡片内容
ForEach(Array.from({ length: 4 }), (_, i) => {
Row() {
Circle()
.width(40)
.height(40)
.fill('#BBDEFB')
Column() {
Text(`推荐内容 ${i + 1}`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text('HarmonyOS NEXT 开发实践')
.fontSize(13)
.fontColor('#666666')
}
.margin({ left: 12 })
}
.width('100%')
.height(72)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(12)
.margin({ bottom: 12 })
})
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#F5F5F5')
}
.tabBar({ icon: $r('app.media.tab_explore'), text: '发现' })
}
.barOverlap(true)
.barPosition(BarPosition.End)
.vertical(false)
.onChange((index: number) => {
this.currentIndex = index
console.info(`当前选中页签: ${index}`)
})
}
.width('100%')
.height('100%')
}
}
FAQ
Q:HdsTabs 和普通 Tabs 组件能不能混用?
A:不建议。HdsTabs 本身是对 Tabs 的封装,内部已经处理了叠加、模糊、悬浮等样式逻辑。如果在一个页面上同时使用普通 Tabs 和 HdsTabs,容易出现样式冲突和布局异常。如果项目已经用了普通 Tabs,想迁移到 HdsTabs,建议一次性替换完,不要局部混用。
Q:为什么只在底部页签场景推荐使用 HdsTabs?顶部页签能用吗?
A:HdsTabs 的设计目标是底部导航场景,barOverlap、barPosition(BarPosition.End) 这些属性都是为底部栏定制的。顶部页签用普通 Tabs 组件更合适,HdsTabs 的模糊效果和悬浮样式在顶部场景下体验不太自然。
Q:HdsTabs 是否支持无障碍和键盘导航?
A:支持。HdsTabs 继承了 Tabs 组件的无障碍能力,默认支持读屏焦点、TalkBack 播报、键盘方向键切换页签。不需要额外配置。但如果你自定义了 tabBar 的 builder,需要手动设置 accessibilityText 和 accessibilityLevel。
Q:真机运行正常,预览器里 tabBar 不显示叠加效果?
A:DevEco Studio 预览器对 UI Design Kit 组件的渲染支持有限,部分动画效果和模糊效果在预览器中无法正常显示。这是预览器本身的限制,不是代码问题。建议以真机运行为准。