小雨同学 2026-09-17 09:36:31 发布前言
已有 HarmonyOS 项目准备把普通底部导航改成悬浮页签时,代码变化其实不算多。真正容易出问题的地方,通常是页面已经换成了悬浮样式,脑子里还沿用着普通底栏的布局习惯。普通底栏很好理解。页面主体占一块区域,底部导航再占一块区域,两边各有自己的位置。内容写到导航栏上方,基本就结束了。
悬浮页签换了一种关系。页签栏可以盖在内容上面,页面内容还能继续往下延伸。这样做以后,底部会出现更明显的前后层次,图片、列表背景也能铺到页签后面。与此同时,底部按钮、输入框、最后几个列表项也有可能跑到页签下面。
所以,悬浮页签真正需要重新检查的地方,往往在页面底部。刚开始接触 HdsTabs 时,很容易把 barFloatingStyle 当成一个开关,觉得加上以后底栏自然就会浮起来。实际使用时,还需要 barPosition、vertical 和 barOverlap 一起配合。少了其中一项,页面表现就可能和预期不同。
这里可以先记住一个很实用的开发习惯:先保留能正常工作的普通页签,再逐项增加悬浮配置。这样做虽然多了一次对照,排查时却轻松很多。普通模式正常,悬浮模式出现问题,范围已经缩小到了底部导航和内容布局;如果普通模式本身就不正常,那就先回去检查 HdsTabs、资源或者工程环境。
对于刚开始接触 HDS 的开发者,这种写法也比较友好。页面里先别急着把 MiniBar、材质、智感握姿全塞进去。东西一多,哪里出了问题都不好找。第一轮只看普通底栏怎样变成悬浮底栏,反而更容易把几个关键属性记牢。
还有一条底部导航里很常用的设计经验也可以顺手记住:一级页签数量不要太多。三个、四个入口通常比较舒服,继续往上加,文字、图标和点击区域都会开始变挤。导航入口已经很多时,先整理页面层级,比继续缩图标更靠谱。
目前测试环境仍然以 HarmonyOS 7 模拟器为主。页签切换、悬浮位置和内容覆盖关系可以先在模拟器中确认;真实设备底部系统区域、操作距离和不同屏幕形态,还需要继续用对应设备检查。
一、先保留普通 HdsTabs
改导航时,最值得保护的其实是已经跑通的业务页面。首页可能有信息流,任务页可能有待办状态,个人中心里还有账号和设置。这些东西往往比底栏复杂得多。为了验证一个悬浮页签,把业务页面也一起重写,排查范围会一下子变大。
所以更稳的做法,是先保留三个 TabContent:
首页任务我的外层继续使用普通 HdsTabs。
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 可以用来设置底部页签的显示内容,例如图标和文本:
BottomTabBarStyle.of( $r('sys.media.ohos_app_icon'), '首页')示例里使用同一张系统图标只是为了减少额外资源依赖。项目真正接入时,首页、任务、我的应该换成各自的图标。
这里也有一个容易混淆的点。BottomTabBarStyle 负责页签本身怎么显示,HdsTabs 负责整个页签容器。后面用到的 barFloatingStyle 又负责悬浮区域。
可以把它们分开记:
TabContent页面内容BottomTabBarStyle图标和文字HdsTabs页签容器barFloatingStyle悬浮页签区域一级导航数量最好控制住
悬浮页签四周有留白,整体看起来会更集中,所以页签数量过多时,拥挤感也会更明显。
三个入口最容易观察。
四个也很常见。
继续增加到五个、六个以后,就要开始考虑图标宽度、文字长度和操作距离。
如果业务里已经出现:
首页推荐发现消息订单收藏我的这时候先别急着研究怎么把七个 Tab 塞进胶囊里。
更值得做的是重新整理导航。一级页签只保留最高频的入口,其他页面可以放到二级页面、菜单或者个人中心里。
底部导航的空间再漂亮,也解决不了信息架构太挤的问题。普通模式确认以后,就可以开始真正改变底部布局。
二、悬浮页签真正改变页面的是 barOverlap
普通 HdsTabs 进入悬浮状态,核心配置只有几项:
.barOverlap(true).barPosition(BarPosition.End).vertical(false).barFloatingStyle({ barBottomMargin: 28})代码很短,不过四个属性分别做不同的事情。把它们拆开理解,比直接背一段代码好用得多。
barPosition 决定页签放在页面哪一边
当前使用:
.barPosition(BarPosition.End)在这组页面结构里,页签栏放在底部。
普通底栏本身往往已经有这项配置,所以从普通 HdsTabs 改悬浮页签时,它通常不需要重新处理。
它解决的是位置。
vertical 决定页签横着排还是竖着排
底部导航使用:
.vertical(false)这表示页签按照横向排列。
所以:
BarPosition.End+vertical(false)可以先理解成底部横向页签。
到这里,页面仍然可以是普通底栏。
悬浮布局真正开始发生变化,要看 barOverlap。
barOverlap 会让内容继续跑到 TabBar 后面
开启:
.barOverlap(true)以后,TabBar 可以叠加在 TabContent 上。
普通底栏原来的关系更接近:
内容区域────────页签栏悬浮模式则更像:
内容区域一直延伸到底部 ↑悬浮页签盖在内容上方这就是两种布局最大的区别。
所以,看到悬浮页签以后,别只盯着胶囊圆角。真正会影响现有项目的,是内容区已经可以跑到页签后面。
这一点对列表、表单和底部操作按钮影响都很明显。比如页面最后是一个保存按钮。普通底栏模式里,按钮通常不会进入 TabBar 区域。
开启 barOverlap 后,按钮如果还贴着页面底部,就有可能和悬浮页签撞在一起。所以悬浮页签上线之前,底部内容一定要重新过一遍。
barFloatingStyle 再处理悬浮区域本身
布局关系准备好以后,再设置:
.barFloatingStyle({ barBottomMargin: 28})barBottomMargin 会影响悬浮页签离页面底部的距离。
示例使用 28vp,主要是为了让模拟器里悬浮感比较明显,同时给底部留出一段空间。这个值不能当成固定规范。
真实设备上,底部可能还有系统导航指示区域。不同设备、不同窗口状态、后续有没有 MiniBar,都会影响最终间距。所以,28vp 在这里是实验值。
项目里真正需要的是:
- 页签不要贴系统底部区域
- 操作起来舒服
- 页面视觉间距自然
满足这几个条件就行。
四个属性可以按问题来记
不用背代码顺序,可以按页面问题来记。
| 页面问题 | 对应配置 |
|---|---|
| 页签要放到底部 | barPosition |
| 页签要横向排列 | vertical |
| 内容要延伸到页签后面 | barOverlap |
| 页签要有悬浮区域 | barFloatingStyle |
这样排查也比较顺。
底栏位置不对,先查 barPosition。
排列方向不对,查 vertical。
内容没有进入页签后方,查 barOverlap。
页面已经有覆盖关系,但悬浮效果还没出来,再看 barFloatingStyle。
不用上来就把整个组件删了重写。
三、重新梳理底部内容
悬浮效果出来以后,页面看着已经差不多了,但现有项目真正容易翻车的地方还在下面。页面底部原来是什么内容,会直接决定改造量。
图片和背景通常可以继续往下铺
如果页面底部是图片、渐变或者普通背景,让它们延伸到悬浮页签后面通常没什么问题。
这种布局还能强化前后层次。
例如图片浏览、资讯流、内容首页,悬浮页签压在背景上方会比较自然。
这类内容不需要为了避开页签,强行在底部塞一大块空白。
按钮和输入框就不能这么处理
底部是这些内容时,要仔细检查:
- 保存
- 提交
- 下一步
- 发送
- 搜索输入框
- 播放器控制
这些都需要稳定点击。
悬浮页签挡住一部分,就算肉眼还能看到,体验也会明显变差。
所以页面底部内容可以粗略分成两类:
| 内容 | 处理方式 |
|---|---|
| 背景、图片、装饰 | 可以继续延伸 |
| 可点击按钮 | 保证完整露出 |
| 输入框 | 避开悬浮区域 |
| List 最后几个 Item | 保证可以滚到页签上方 |
| 重要状态信息 | 不要被导航挡住 |
这类判断比统一加一个固定 paddingBottom 更靠谱。
因为每个页面底部放的东西不一样。阅读页面可能只需要很小的留白。
任务列表要确保最后一个 Item 能完整出现。表单页有提交按钮,就要更保守。后面加入 MiniBar 后,底部占用空间还会继续变化。
Demo 里的 120vp 只是为了方便观察
当前实验页面在悬浮模式下给内容区留出:
.padding({ left: 20, right: 20, top: 18, bottom: 120})这个数值的作用很简单:保证最后一张内容卡片可以完整滚到悬浮页签上面。
普通模式下则使用更小的底部间距。这样切换两种模式时,页面能够直观看出差异。正式项目别照抄 120vp。
更合适的做法是根据:
- TabBar 高度
- barBottomMargin
- 系统底部区域
- 是否存在 MiniBar
- 页面最后一个操作组件
再决定底部需要留多少空间。
悬浮导航也别为了悬浮而悬浮
这里还有一个产品层面的判断。
悬浮页签比较适合内容可以延伸到底部的页面,比如图片、信息流、内容浏览和轻工具页面。
如果一个页面底部本来就有大量固定操作区,再塞一层悬浮 TabBar,空间可能会变得很拥挤。
这时候可以先问一句:底部到底有几层东西?
如果已经有:
- 播放器控制
- 进度条
- 快捷操作
- 输入框
再加悬浮导航,很容易把底部做成一层叠一层。
页面不是叠得越多越高级。层级太多以后,用户反而不知道应该先看哪一块。
这种情况下,普通底栏、顶部导航或者重新调整页面结构,可能更合适。
手机上的漂亮效果不要直接拉宽到平板
手机窄窗口里,悬浮胶囊导航通常比较容易控制。
到了平板和宽窗口,情况会变。如果还是把手机底部导航直接拉到全宽,页签之间的距离会非常大,操作也不舒服。
所以,多设备项目里最好把手机布局和宽屏布局分开考虑。
当前实验先把手机尺寸下的普通底栏和悬浮底栏关系跑通。宽窗口、折叠屏和平板的处理需要结合实际布局重新判断,不适合直接拿手机参数放大。
这也是为什么当前代码没有先塞很多响应式判断。先把最基础的布局关系搞懂,后面做多设备时会省很多力气。
两种模式放在一起看最容易发现问题
完整 Demo 提供两个按钮:
普通底栏悬浮页签两种模式使用相同业务内容。
切换以后可以重点看三个地方:
页签四周有没有悬空间距内容有没有延伸到页签后面最后一张内容卡片能不能完整滚出这三个现象确认以后,普通底栏到悬浮页签的核心变化基本就理解了。
总结
普通 HdsTabs 改成悬浮页签,真正需要改的代码不多。
最小配置就是:
.barPosition(BarPosition.End).vertical(false).barOverlap(true).barFloatingStyle({ barBottomMargin: 28})这几项配置可以分别记成位置、方向、重叠关系和悬浮区域。
真正影响页面改造量的,是 barOverlap 开启以后,内容可以继续延伸到页签后方。
所以现有项目更适合按下面的顺序处理:
先确认普通 HdsTabs 正常↓保留原来的 TabContent↓开启 barOverlap↓加入 barFloatingStyle↓检查最后几个内容项↓检查底部按钮和输入框↓真机确认系统底部区域这个顺序不花哨,但很实用。第一次接触悬浮页签时,先把这条路径跑顺,比同时研究 MiniBar、材质和握姿轻松很多。页面已经稳定以后,再往上加功能也不会那么乱。
当前 HarmonyOS 7 模拟器可以帮助确认页签切换、内容覆盖和悬浮位置。示例里的 barBottomMargin: 28 和底部 120vp 留白都属于当前实验值。真正进入项目以后,需要根据设备、页面内容和底部系统区域重新调整。
完整代码
Main.ets
/** * HarmonyOS 7 悬浮页签深度实战 02 * */import { HdsTabs, HdsTabsController} from '@kit.UIDesignKit';@Entry@Componentstruct 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') }}暂无评论数据
发布
相关推荐
小雨同学
产品总监、独立开发者社群主理人、资深全栈工程师,HarmonyOS应用开发者高级认证,PMP认证,CSDN博客专家,鸿蒙极客,Trae Fellow,阿里云社区专家博主、51CTO 博客专家、OpenTiny 优秀布道师、科大讯飞荣誉讲师。
帖子
提问
粉丝
【HarmonyOS 7 悬浮页签深度实战】04 HdsTabsController 如何协调页签切换与显隐
2026-09-19 13:17:00 发布【HarmonyOS 7 悬浮页签深度实战】03 barFloatingStyle 的宽度、底部间距与遮罩如何配置
2026-09-18 12:32:32 发布


0
京公网安备:11010502051901号