文章目录
-
- 前言
- [一、先保留普通 HdsTabs](#一、先保留普通 HdsTabs)
-
- 为什么要在同一个页面里保留普通模式
- [BottomTabBarStyle 只管页签长什么样](#BottomTabBarStyle 只管页签长什么样)
- 一级导航数量最好控制住
- [二、悬浮页签真正改变页面的是 barOverlap](#二、悬浮页签真正改变页面的是 barOverlap)
-
- [barPosition 决定页签放在页面哪一边](#barPosition 决定页签放在页面哪一边)
- [vertical 决定页签横着排还是竖着排](#vertical 决定页签横着排还是竖着排)
- [barOverlap 会让内容继续跑到 TabBar 后面](#barOverlap 会让内容继续跑到 TabBar 后面)
- [barFloatingStyle 再处理悬浮区域本身](#barFloatingStyle 再处理悬浮区域本身)
- 四个属性可以按问题来记
- 三、重新梳理底部内容
-
- 图片和背景通常可以继续往下铺
- 按钮和输入框就不能这么处理
- [Demo 里的 120vp 只是为了方便观察](#Demo 里的 120vp 只是为了方便观察)
- 悬浮导航也别为了悬浮而悬浮
- 手机上的漂亮效果不要直接拉宽到平板
- 两种模式放在一起看最容易发现问题
- 总结
- 完整代码
前言
已有 HarmonyOS 项目准备把普通底部导航改成悬浮页签时,代码变化其实不算多。真正容易出问题的地方,通常是页面已经换成了悬浮样式,脑子里还沿用着普通底栏的布局习惯。普通底栏很好理解。页面主体占一块区域,底部导航再占一块区域,两边各有自己的位置。内容写到导航栏上方,基本就结束了。
悬浮页签换了一种关系。页签栏可以盖在内容上面,页面内容还能继续往下延伸。这样做以后,底部会出现更明显的前后层次,图片、列表背景也能铺到页签后面。与此同时,底部按钮、输入框、最后几个列表项也有可能跑到页签下面。
所以,悬浮页签真正需要重新检查的地方,往往在页面底部。刚开始接触 HdsTabs 时,很容易把 barFloatingStyle 当成一个开关,觉得加上以后底栏自然就会浮起来。实际使用时,还需要 barPosition、vertical 和 barOverlap 一起配合。少了其中一项,页面表现就可能和预期不同。
这里可以先记住一个很实用的开发习惯:先保留能正常工作的普通页签,再逐项增加悬浮配置。这样做虽然多了一次对照,排查时却轻松很多。普通模式正常,悬浮模式出现问题,范围已经缩小到了底部导航和内容布局;如果普通模式本身就不正常,那就先回去检查 HdsTabs、资源或者工程环境。
对于刚开始接触 HDS 的开发者,这种写法也比较友好。页面里先别急着把 MiniBar、材质、智感握姿全塞进去。东西一多,哪里出了问题都不好找。第一轮只看普通底栏怎样变成悬浮底栏,反而更容易把几个关键属性记牢。
还有一条底部导航里很常用的设计经验也可以顺手记住:一级页签数量不要太多。三个、四个入口通常比较舒服,继续往上加,文字、图标和点击区域都会开始变挤。导航入口已经很多时,先整理页面层级,比继续缩图标更靠谱。
目前测试环境仍然以 HarmonyOS 7 模拟器为主。页签切换、悬浮位置和内容覆盖关系可以先在模拟器中确认;真实设备底部系统区域、操作距离和不同屏幕形态,还需要继续用对应设备检查。

一、先保留普通 HdsTabs
改导航时,最值得保护的其实是已经跑通的业务页面。首页可能有信息流,任务页可能有待办状态,个人中心里还有账号和设置。这些东西往往比底栏复杂得多。为了验证一个悬浮页签,把业务页面也一起重写,排查范围会一下子变大。
所以更稳的做法,是先保留三个 TabContent:
text
首页
任务
我的
外层继续使用普通 HdsTabs。
ts
HdsTabs({
controller: this.normalController
}) {
TabContent() {
this.tabPage(
'首页',
'当前页面保留原来的首页内容。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'首页'
)
)
TabContent() {
this.tabPage(
'任务',
'任务页面仍然由 TabContent 承载。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'任务'
)
)
TabContent() {
this.tabPage(
'我的',
'个人中心保持原来的业务结构。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'我的'
)
)
}
.barPosition(BarPosition.End)
.vertical(false)
这里可以先只看三件事:
-
三个 TabContent 能正常显示
-
底部三个页签可以正常切换
-
页面底部没有异常遮挡
这就是普通模式的基线。
基线这个词听起来有点工程味,其实意思很简单:先留一个确定没问题的版本。
后面加悬浮配置以后,页面有变化,就和这个版本对着看。对比很直观,也不容易把别的问题混进来。
为什么要在同一个页面里保留普通模式
真实项目改 UI 时,最麻烦的情况往往不是报错,而是页面看起来有点不对。
比如最后一张卡片少了一截,底部按钮还能点但被挡了一半,列表滚到底以后总感觉空间怪怪的。这类问题没有编译错误,单看代码也不一定马上发现。
这时候保留普通模式就很有用。
普通模式正常:
- 内容完整
- 底栏正常
- 最后一个 Item 能完整显示
切到悬浮模式以后:
- 最后一个 Item 被盖住
问题范围已经很明确了,直接检查 barOverlap 和底部留白就行。
这种做法和调参数时一次只改一项是一个道理。变量少一点,脑子轻松一点。别一股脑全加上,最后自己给自己上强度。
BottomTabBarStyle 只管页签长什么样
底部导航除了内容切换,还会有图标和文字。
BottomTabBarStyle 可以用来设置底部页签的显示内容,例如图标和文本:
ts
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'首页'
)
示例里使用同一张系统图标只是为了减少额外资源依赖。项目真正接入时,首页、任务、我的应该换成各自的图标。
这里也有一个容易混淆的点。BottomTabBarStyle 负责页签本身怎么显示,HdsTabs 负责整个页签容器。后面用到的 barFloatingStyle 又负责悬浮区域。
可以把它们分开记:
text
TabContent
页面内容
BottomTabBarStyle
图标和文字
HdsTabs
页签容器
barFloatingStyle
悬浮页签区域
一级导航数量最好控制住
悬浮页签四周有留白,整体看起来会更集中,所以页签数量过多时,拥挤感也会更明显。
三个入口最容易观察。
四个也很常见。
继续增加到五个、六个以后,就要开始考虑图标宽度、文字长度和操作距离。
如果业务里已经出现:
text
首页
推荐
发现
消息
订单
收藏
我的
这时候先别急着研究怎么把七个 Tab 塞进胶囊里。
更值得做的是重新整理导航。一级页签只保留最高频的入口,其他页面可以放到二级页面、菜单或者个人中心里。
底部导航的空间再漂亮,也解决不了信息架构太挤的问题。普通模式确认以后,就可以开始真正改变底部布局。
二、悬浮页签真正改变页面的是 barOverlap
普通 HdsTabs 进入悬浮状态,核心配置只有几项:
ts
.barOverlap(true)
.barPosition(BarPosition.End)
.vertical(false)
.barFloatingStyle({
barBottomMargin: 28
})
代码很短,不过四个属性分别做不同的事情。把它们拆开理解,比直接背一段代码好用得多。
barPosition 决定页签放在页面哪一边
当前使用:
ts
.barPosition(BarPosition.End)
在这组页面结构里,页签栏放在底部。
普通底栏本身往往已经有这项配置,所以从普通 HdsTabs 改悬浮页签时,它通常不需要重新处理。
它解决的是位置。
vertical 决定页签横着排还是竖着排
底部导航使用:
ts
.vertical(false)
这表示页签按照横向排列。
所以:
text
BarPosition.End
+
vertical(false)
可以先理解成底部横向页签。
到这里,页面仍然可以是普通底栏。
悬浮布局真正开始发生变化,要看 barOverlap。
barOverlap 会让内容继续跑到 TabBar 后面
开启:
ts
.barOverlap(true)
以后,TabBar 可以叠加在 TabContent 上。
普通底栏原来的关系更接近:
text
内容区域
────────
页签栏
悬浮模式则更像:
text
内容区域一直延伸到底部
↑
悬浮页签盖在内容上方
这就是两种布局最大的区别。
所以,看到悬浮页签以后,别只盯着胶囊圆角。真正会影响现有项目的,是内容区已经可以跑到页签后面。
这一点对列表、表单和底部操作按钮影响都很明显。比如页面最后是一个保存按钮。普通底栏模式里,按钮通常不会进入 TabBar 区域。
开启 barOverlap 后,按钮如果还贴着页面底部,就有可能和悬浮页签撞在一起。所以悬浮页签上线之前,底部内容一定要重新过一遍。
barFloatingStyle 再处理悬浮区域本身
布局关系准备好以后,再设置:
ts
.barFloatingStyle({
barBottomMargin: 28
})
barBottomMargin 会影响悬浮页签离页面底部的距离。
示例使用 28vp,主要是为了让模拟器里悬浮感比较明显,同时给底部留出一段空间。这个值不能当成固定规范。
真实设备上,底部可能还有系统导航指示区域。不同设备、不同窗口状态、后续有没有 MiniBar,都会影响最终间距。所以,28vp 在这里是实验值。
项目里真正需要的是:
- 页签不要贴系统底部区域
- 操作起来舒服
- 页面视觉间距自然
满足这几个条件就行。
四个属性可以按问题来记
不用背代码顺序,可以按页面问题来记。
| 页面问题 | 对应配置 |
|---|---|
| 页签要放到底部 | barPosition |
| 页签要横向排列 | vertical |
| 内容要延伸到页签后面 | barOverlap |
| 页签要有悬浮区域 | barFloatingStyle |
这样排查也比较顺。
底栏位置不对,先查 barPosition。
排列方向不对,查 vertical。
内容没有进入页签后方,查 barOverlap。
页面已经有覆盖关系,但悬浮效果还没出来,再看 barFloatingStyle。
不用上来就把整个组件删了重写。

三、重新梳理底部内容
悬浮效果出来以后,页面看着已经差不多了,但现有项目真正容易翻车的地方还在下面。页面底部原来是什么内容,会直接决定改造量。
图片和背景通常可以继续往下铺
如果页面底部是图片、渐变或者普通背景,让它们延伸到悬浮页签后面通常没什么问题。
这种布局还能强化前后层次。
例如图片浏览、资讯流、内容首页,悬浮页签压在背景上方会比较自然。
这类内容不需要为了避开页签,强行在底部塞一大块空白。
按钮和输入框就不能这么处理
底部是这些内容时,要仔细检查:
- 保存
- 提交
- 下一步
- 发送
- 搜索输入框
- 播放器控制
这些都需要稳定点击。
悬浮页签挡住一部分,就算肉眼还能看到,体验也会明显变差。
所以页面底部内容可以粗略分成两类:
| 内容 | 处理方式 |
|---|---|
| 背景、图片、装饰 | 可以继续延伸 |
| 可点击按钮 | 保证完整露出 |
| 输入框 | 避开悬浮区域 |
| List 最后几个 Item | 保证可以滚到页签上方 |
| 重要状态信息 | 不要被导航挡住 |
这类判断比统一加一个固定 paddingBottom 更靠谱。
因为每个页面底部放的东西不一样。阅读页面可能只需要很小的留白。
任务列表要确保最后一个 Item 能完整出现。表单页有提交按钮,就要更保守。后面加入 MiniBar 后,底部占用空间还会继续变化。
Demo 里的 120vp 只是为了方便观察
当前实验页面在悬浮模式下给内容区留出:
ts
.padding({
left: 20,
right: 20,
top: 18,
bottom: 120
})
这个数值的作用很简单:保证最后一张内容卡片可以完整滚到悬浮页签上面。
普通模式下则使用更小的底部间距。这样切换两种模式时,页面能够直观看出差异。正式项目别照抄 120vp。
更合适的做法是根据:
- TabBar 高度
- barBottomMargin
- 系统底部区域
- 是否存在 MiniBar
- 页面最后一个操作组件
再决定底部需要留多少空间。
悬浮导航也别为了悬浮而悬浮
这里还有一个产品层面的判断。
悬浮页签比较适合内容可以延伸到底部的页面,比如图片、信息流、内容浏览和轻工具页面。
如果一个页面底部本来就有大量固定操作区,再塞一层悬浮 TabBar,空间可能会变得很拥挤。
这时候可以先问一句:底部到底有几层东西?
如果已经有:
- 播放器控制
- 进度条
- 快捷操作
- 输入框
再加悬浮导航,很容易把底部做成一层叠一层。
页面不是叠得越多越高级。层级太多以后,用户反而不知道应该先看哪一块。
这种情况下,普通底栏、顶部导航或者重新调整页面结构,可能更合适。
手机上的漂亮效果不要直接拉宽到平板
手机窄窗口里,悬浮胶囊导航通常比较容易控制。
到了平板和宽窗口,情况会变。如果还是把手机底部导航直接拉到全宽,页签之间的距离会非常大,操作也不舒服。
所以,多设备项目里最好把手机布局和宽屏布局分开考虑。
当前实验先把手机尺寸下的普通底栏和悬浮底栏关系跑通。宽窗口、折叠屏和平板的处理需要结合实际布局重新判断,不适合直接拿手机参数放大。
这也是为什么当前代码没有先塞很多响应式判断。先把最基础的布局关系搞懂,后面做多设备时会省很多力气。
两种模式放在一起看最容易发现问题
完整 Demo 提供两个按钮:
text
普通底栏
悬浮页签
两种模式使用相同业务内容。
切换以后可以重点看三个地方:
text
页签四周有没有悬空间距
内容有没有延伸到页签后面
最后一张内容卡片能不能完整滚出
这三个现象确认以后,普通底栏到悬浮页签的核心变化基本就理解了。


总结
普通 HdsTabs 改成悬浮页签,真正需要改的代码不多。
最小配置就是:
ts
.barPosition(BarPosition.End)
.vertical(false)
.barOverlap(true)
.barFloatingStyle({
barBottomMargin: 28
})
这几项配置可以分别记成位置、方向、重叠关系和悬浮区域。
真正影响页面改造量的,是 barOverlap 开启以后,内容可以继续延伸到页签后方。
所以现有项目更适合按下面的顺序处理:
text
先确认普通 HdsTabs 正常
↓
保留原来的 TabContent
↓
开启 barOverlap
↓
加入 barFloatingStyle
↓
检查最后几个内容项
↓
检查底部按钮和输入框
↓
真机确认系统底部区域
这个顺序不花哨,但很实用。第一次接触悬浮页签时,先把这条路径跑顺,比同时研究 MiniBar、材质和握姿轻松很多。页面已经稳定以后,再往上加功能也不会那么乱。
当前 HarmonyOS 7 模拟器可以帮助确认页签切换、内容覆盖和悬浮位置。示例里的 barBottomMargin: 28 和底部 120vp 留白都属于当前实验值。真正进入项目以后,需要根据设备、页面内容和底部系统区域重新调整。
完整代码
Main.ets
ts
/**
* HarmonyOS 7 悬浮页签深度实战 02
*
*/
import {
HdsTabs,
HdsTabsController
} from '@kit.UIDesignKit';
@Entry
@Component
struct Main {
/**
* 两种模式分别使用控制器,
* 避免切换结构时共享控制状态。
*/
private normalController: HdsTabsController =
new HdsTabsController();
private floatingController: HdsTabsController =
new HdsTabsController();
/**
* false:普通底栏
* true:悬浮页签
*/
@State private floatingEnabled: boolean = false;
@Builder
private modeSelector() {
Column({ space: 10 }) {
Text('底部页签模式')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#17203A')
.width('100%')
Row({ space: 10 }) {
Button('普通底栏')
.layoutWeight(1)
.height(40)
.fontSize(13)
.fontColor(
this.floatingEnabled
? '#5065E8'
: Color.White
)
.backgroundColor(
this.floatingEnabled
? '#EEF1FF'
: '#5065E8'
)
.onClick(() => {
this.floatingEnabled = false;
})
Button('悬浮页签')
.layoutWeight(1)
.height(40)
.fontSize(13)
.fontColor(
this.floatingEnabled
? Color.White
: '#5065E8'
)
.backgroundColor(
this.floatingEnabled
? '#5065E8'
: '#EEF1FF'
)
.onClick(() => {
this.floatingEnabled = true;
})
}
.width('100%')
Text(
this.floatingEnabled
? '当前:barOverlap + barFloatingStyle'
: '当前:普通底部 HdsTabs'
)
.fontSize(12)
.fontColor('#68708A')
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(20)
.alignItems(HorizontalAlign.Start)
}
@Builder
private infoCard(
index: string,
title: string,
description: string
) {
Column({ space: 7 }) {
Text(index)
.fontSize(11)
.fontWeight(FontWeight.Medium)
.fontColor('#5065E8')
.width('100%')
Text(title)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#17203A')
.width('100%')
Text(description)
.fontSize(13)
.fontColor('#68708A')
.lineHeight(20)
.width('100%')
}
.width('100%')
.height(112)
.padding(16)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
.backgroundColor(Color.White)
.borderRadius(20)
}
/**
* 普通模式和悬浮模式复用同一套业务内容。
*/
@Builder
private tabPage(
title: string,
description: string
) {
Scroll() {
Column({ space: 12 }) {
Column({ space: 6 }) {
Text(title)
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor('#11182C')
.width('100%')
Text(description)
.fontSize(14)
.fontColor('#68708A')
.lineHeight(21)
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
this.infoCard(
'01',
'业务内容保持原样',
'切换页签模式时,三个 TabContent 使用同一套页面内容。'
)
this.infoCard(
'02',
'观察页面底部',
'悬浮模式开启后,内容可以继续延伸到页签栏后方。'
)
this.infoCard(
'03',
'检查可操作区域',
'按钮、输入框和最后几个列表项需要避开悬浮页签。'
)
this.infoCard(
'04',
'保留背景延伸',
'图片和普通背景可以继续显示在悬浮页签下方。'
)
Column({ space: 6 }) {
Text('BOTTOM CONTENT')
.fontSize(11)
.fontWeight(FontWeight.Medium)
.fontColor('#5065E8')
.width('100%')
Text('页面底部内容区域')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#17203A')
.width('100%')
Text(
this.floatingEnabled
? '当前内容可以进入悬浮页签后方,'
+ '底部留白保证最后一块内容能够完整滚出。'
: '普通底栏按照正常布局占据页面底部空间。'
)
.fontSize(13)
.fontColor('#68708A')
.lineHeight(20)
.width('100%')
}
.width('100%')
.height(128)
.padding(16)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
.backgroundColor('#E9EDFF')
.borderRadius(20)
}
.width('100%')
.padding({
left: 20,
right: 20,
top: 18,
bottom: this.floatingEnabled ? 120 : 24
})
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.backgroundColor('#F4F6FB')
}
@Builder
private normalTabs() {
HdsTabs({
controller: this.normalController
}) {
TabContent() {
this.tabPage(
'首页',
'当前页面使用普通 HdsTabs 底部导航。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'首页'
)
)
TabContent() {
this.tabPage(
'任务',
'业务内容和悬浮模式保持一致。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'任务'
)
)
TabContent() {
this.tabPage(
'我的',
'当前底栏按照正常布局占据页面空间。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'我的'
)
)
}
.barOverlap(false)
.barPosition(BarPosition.End)
.vertical(false)
.width('100%')
.height('100%')
}
@Builder
private floatingTabs() {
HdsTabs({
controller: this.floatingController
}) {
TabContent() {
this.tabPage(
'首页',
'当前页面用于观察悬浮页签和内容区域的布局关系。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'首页'
)
)
TabContent() {
this.tabPage(
'任务',
'业务内容保持不变,底部页签进入悬浮状态。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'任务'
)
)
TabContent() {
this.tabPage(
'我的',
'页面底部的可操作内容需要检查悬浮页签遮挡范围。'
)
}
.tabBar(
BottomTabBarStyle.of(
$r('sys.media.ohos_app_icon'),
'我的'
)
)
}
.barOverlap(true)
.barPosition(BarPosition.End)
.vertical(false)
.barFloatingStyle({
/**
* 28vp 只用于当前实验。
* 真机需要结合底部系统区域重新确认。
*/
barBottomMargin: 28
})
.width('100%')
.height('100%')
}
build() {
Column({ space: 12 }) {
Column({ space: 5 }) {
Text('HarmonyOS 7 悬浮页签')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#11182C')
.width('100%')
Text('普通底栏与悬浮页签对比')
.fontSize(16)
.fontColor('#68708A')
.width('100%')
}
.width('100%')
.padding({
left: 20,
right: 20,
top: 24
})
.alignItems(HorizontalAlign.Start)
Column() {
this.modeSelector()
}
.width('100%')
.padding({
left: 20,
right: 20
})
Column() {
if (this.floatingEnabled) {
this.floatingTabs()
} else {
this.normalTabs()
}
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F4F6FB')
}
}