积分中心
【HarmonyOS 7 悬浮页签深度实战】03 barFloatingStyle 的宽度、底部间距与遮罩如何配置 原创
头像 小雨同学 2026-09-18 12:32:32    发布
1226 浏览 2 点赞 0 收藏

前言

悬浮页签能够正常显示以后,宽度、底部距离和渐变遮罩就要开始调整了。宽度不合适,三个入口会显得拥挤或相隔太远;底部距离不合适,页签可能贴近系统导航区域,也可能与底边相距过大;遮罩的不透明度过高,正文还没靠近页签就开始变淡。

手机端窗口宽度较小时,三个入口排列在一条两百多 vp 的页签栏中,图标和文字通常还能保持紧凑。窗口宽度增加以后,如果页签栏也跟着一直伸展,三个入口之间会出现大段空白,视线和手指的移动距离也会增加。底部距离也没有一个适合所有页面的数字:同样的 28vp,在模拟器、带导航指示器的手机和可调整窗口中,有时会让页签贴近导航指示器,有时又会让它与底边相距较大。

barOverlap 开启后,TabContent 会继续延伸到 TabBar 后方。彩色卡片、图片和文字从悬浮栏下面经过时,导航图标可能被背景细节干扰;遮罩的不透明度过高或覆盖范围过大,正文又会提前淡掉。调试时一次只修改一项,三项数值确定以后,还要结合窗口宽度、滚动位置和主题检查。

barFloatingStyle 接收可选的 HdsTabsFloatingStyle,这组悬浮样式从 6.1.0(23) 开始提供。它要求页签位于底部 barPosition=BarPosition.End、保持横向排列 vertical=false,同时开启内容覆盖 barOverlap=true;页签内容采用 BottomTabBarStyle 或 CustomBuilder。缺少任何一项,继续调整宽度、间距或遮罩也无法解决结构问题。

一、宽度跟着窗口变化

三个入口在手机宽度下排列正常,窗口宽度增加后,悬浮栏可能同步伸展为横向尺寸过大的页签栏,图标之间留下大段空白。barFloatingStyle 里的 barWidth 用于限制悬浮 TabBar 自身的宽度,类型是 HdsBarWidthRangeOptions,其中包含 smallWidth、mediumWidth 和 largeWidth 三档。Demo 在一个对象里同时设置三档值,HdsTabs 根据组件所处的宽度区间选择一档。HdsTabs 自身还有接收 Length 的普通 barWidth 属性,两处同名参数结构不同,编写悬浮配置时不要混用。

这里要按照 HdsTabs 自身宽度选择档位,设备型号不能直接代替档位判断。当手机全屏时,HdsTabs 往往处在小窗口档;平板分屏以后,组件宽度仍可能较小;桌面窗口宽度逐渐增加时,才可能进入中档或大档。Demo 中的 HdsTabs 占满窗口,窗口变化会同步改变组件尺寸;局部布局里的 HdsTabs 则要按照组件自身尺寸测试。手机、平板和 2in1 不能直接映射到小、中、大三档,分屏和自由窗口一出现,这种映射就会失效。

三档的分界由 HdsTabs 宽度和高宽比共同决定。smallWidth、mediumWidth、largeWidth 都是 Length,Demo 采用数字形式设置,组件按照 vp 处理;百分比暂不支持。


字段HdsTabs 宽度条件主要观察点
smallWidth小于 440vp窗口宽度较小时三个入口是否拥挤,左右是否还保留适当空间
mediumWidth440vp~600vp;或 600vp~840vp 且 HdsTabs 高宽比小于 0.8窗口宽度开始增加以后,入口间距是否仍然连贯
largeWidth大于 840vp;或 600vp~840vp 且 HdsTabs 高宽比大于 0.8窗口宽度较大时页签横向尺寸是否过大,导航是否还像一个整体

600vp~840vp 这一段还会结合高宽比,只记录窗口宽度数字并不充分。相同宽度下,纵向空间较大的窗口与横向空间更充足的窗口可能使用不同档位。大家测试时可以同时记录窗口宽度和形态,避免尺寸变化以后误以为状态没有刷新。

不设置 barWidth 时,HdsTabs 自己也有默认尺寸规则。页签数量不超过三个且组件宽度小于 600vp 时,单个页签默认 76vp;组件宽度达到 600vp 后,单个页签默认 80vp。页签达到四个及以上时,页签栏最大宽度不超过 328vp。默认值适合快速起步,自定义宽度则适合对品牌视觉、入口文字长度或页面留白已有明确要求的项目。

页签数量会改变对宽度的判断。三个字符较少的标题排列在 240vp 中,平均每个入口约有 80vp;更换为字符较多的标题以后,文字可能已经贴近相邻入口。三个入口如果分布在四百多 vp 的页签栏中,图标之间会留下大段空白,整体感也会减弱。这属于移动端导航的常见设计经验,HdsTabs 本身没有强制限制这一选择。

Demo 把宽度设置为三组 Profile,每次点击会同时修改三个字段:


ProfilesmallWidthmediumWidthlargeWidth实验意图
紧凑216vp280vp344vp检查文字是否贴近相邻入口,左右是否还保留空间
默认240vp320vp400vp保留适中的入口距离,作为本页初始状态
宽松280vp368vp456vp增大三档差异,便于观察窗口使用的是哪一档

这些数值只是参数实验的对照,不代表项目推荐值,也不代表某类设备必须采用某个宽度。实验页面会同时显示另外两档配置,方便确认状态是否更新;要确认窗口使用的是哪一档,仍需在 HarmonyOS 7 模拟器中改变窗口宽度和形态。没有运行结果以前,不能把 mediumWidth 与 largeWidth 的预期变化表述为已经验证的现象。

调整时要结合一级入口数量和标题确定小窗口值。手机状态稳定后,将窗口调整到中档与大档,观察页签横向尺寸是否突然变化、三个入口之间是否出现过长空白。底栏边缘与屏幕的距离、图标和文字是否过于靠近,也要一起记录。每次修改一档就截取一张同位置图片,比凭印象反复点击按钮更容易比较。

如果现有工程允许横竖屏或自由窗口,还要专门检查临界区附近。窗口经过分界值时,页签不能一会儿贴边,一会儿又明显收窄。可以故意增大三档差值,确认组件使用了哪一档;确认以后,将差值缩小到项目最终采用的数值。三个宽度值同时随意修改,会增加排查工作量。

二、底部间距需要和页面底部一起观察

页签与底边距离过小时,外缘可能贴到导航指示器附近;位置过高,又会遮挡更多滚动内容。barBottomMargin 决定悬浮页签与 HdsTabs 底部之间的距离,类型是 Length,默认值为 0vp。这个参数没有统一的最小值或最大值,Demo 使用的 16、28、40 都按照 vp 处理,只是为了放大差异,方便比较。

这个参数减小时,页签会更靠近页面底部,也更容易和系统导航指示器、圆角屏幕边缘或底部手势区域发生空间冲突。数值增大以后,页签向上抬升,底边会保留更多空间,同时遮挡更多内容,滚动卡片从底栏后方经过的路径也会变长。它只改变悬浮栏的位置,安全区域仍然要由页面布局处理。

底部系统区域会受设备、窗口模式和页面是否扩展安全区域影响。同一个 margin 在两台设备上呈现不同距离很常见。测试时要同时检查页签是否贴住导航指示器、最下方入口能否稳定点击,以及悬浮栏与屏幕底边之间是否留下过大的空白。遇到沉浸式窗口或特殊安全区域配置,还要把页面布局带到真机上一起检查,barBottomMargin 不能替代安全区域适配。

barOverlap=true 后,内容可以延伸到 TabBar 后方,barBottomMargin 和页面底部可操作空间需要一起检查。把数值调整到 40vp 后,页签位置会向上移动,但不会自动为列表最后一项腾出可点击空间。如果最后一张卡片停在页签下面,用户只能浏览局部内容又无法点击,就要检查内容区的底部留白和滚动终点。

Demo 在滚动 Column 底部保留 170vp,目的是让最后一张实验卡片能够完整滚出悬浮页签覆盖区。这个 170vp 不属于 barBottomMargin + TabBar 高度 的固定公式。正式页面还要根据最后一个元素决定空间:纯背景可以进入页签后方,说明文字最好能够完整读完,按钮、输入框和列表操作项则需要滚动到页签上方后停住。

比较三个档位时,保持滚动位置不变。16vp 主要用于暴露页签是否贴近底部系统区域;调整为 28vp 后,记录页签外缘和滚动内容之间的距离;调整为 40vp 后,确认页签上移会遮挡多少正文。滚动位置变化会干扰判断,确定数值以后还要切换三个 TabContent,确认不同页面的底部留白保持一致。

页面底部如果还有“提交”“继续”一类固定操作,需要同时确认按钮与页签不会互相覆盖、滚动内容能够越过两者。遇到这种页面,往往需要重新安排底部结构;继续增大 barBottomMargin 只会让悬浮栏占据更多内容空间。参数可以调整间距,按钮能否完整显示和点击仍由页面布局负责。

三、gradientMask 用于降低内容对导航图标的干扰

彩色卡片或文字滚到页签后面时,背景细节可能贴近导航图标,图标和文字的辨识度会降低。gradientMask 通过一段渐变把内容和悬浮栏分开,它接收 HdsTabsBackgroundStyle:maskColor 是 ResourceColor,决定遮罩颜色和透明度;maskHeight 是 number,决定遮罩覆盖高度。遮罩高度不能为 0,因此 Demo 关闭遮罩时直接省略 gradientMask,不会把 maskHeight 设置成 0。

遮罩存在时,内容进入 TabBar 后方会经过一段颜色过渡,导航图标不会直接叠在彩色卡片、图片纹理或密集文字上。遮罩强度过低,文字仍会透到图标附近;不透明度过高,页面底部会形成一块明显色带,正文还没靠近页签就开始消失。

maskColor 需要同时观察色值与透明度。#66F1F3F5 里的 66 带来透明度,后面的颜色与 Demo 的浅灰背景接近,彩色卡片靠近页签时会逐渐回到页面底色。直接迁移到深色页面或大面积图片上,底部可能出现一条发白的色带。深色模式要单独检查,不能直接沿用浅色遮罩。

maskHeight 控制的是过渡范围。数值较小时,变化集中在页签附近,彩色内容可能接近页签时才开始淡化;数值较大时,过渡更早出现,正文可读区域会减少。Demo 提供 72、92、112 三档,初始选择 92vp。切换时应将滚动内容停在同一位置,观察卡片标题、正文和背景色分别从哪里开始减弱。

gradientMask 的默认值目前有两种写法。HdsTabsFloatingStyle.gradientMask 字段说明采用浅色 #CCF1F3F5、深色 #99000000,高度为页签栏默认高度增加 16vp;同页的 HdsTabsBackgroundStyle 说明采用 #CCFFFFFF,高度为包含底部 TabBar 的组件高度增加 32vp。本地 API 26 SDK 能确认字段类型,maskHeight 注释延续后一种高度说明,却没有给出一组独立且完整的 gradientMask 默认值。开发指南示例显式设置 #66F1F3F5 和 92vp,这些信息能确认参数写法,还不足以确定组件最终采用哪组默认值。Demo 因此显式设置 maskColor 与 maskHeight,不依赖尚未统一的默认配置。

是否启用遮罩,还要回到内容本身。简单纯色背景没有必要为了使用 API 强行增加遮罩;页签轮廓和文字已经能够辨认时,省略 gradientMask 还可以避免底部多出一段色带。信息流图片、彩色卡片和高密度列表经过悬浮栏后方时,更容易干扰导航图标,因此更值得测试;静态空白页可能完全不需要。

有图片的页面不能仅凭一张浅色图片判断。至少选择浅色图片、深色图片和细节密集的图片依次检查。普通列表则要关注文字基线经过遮罩时是否还能辨认,以及最后一个可操作项能否滚出覆盖区。遮罩只负责视觉过渡,内容底部留白仍由页面布局处理;两者同时调整,常会出现颜色已经合适、按钮却仍被页签遮挡的情况。

调整时关闭遮罩,用于分辨问题来自内容覆盖还是遮罩本身。确定问题来源后,固定一种接近页面背景的颜色并比较三档高度,最后微调透明度。每次只改变一个变量,截图才能说明是哪项设置带来了变化。页面包含浅色与深色两套主题时,两套颜色各检查一遍,避免让一份色值勉强覆盖全部场景。

完整 Demo 的开关采用两份 HdsTabsFloatingStyle 返回值:开启时包含 gradientMask,关闭时保留 barWidth 和 barBottomMargin,仅省略遮罩对象。这样的实验页能让宽度、底部位置和遮罩彼此独立,切换任何一项都不会连带修改另外两项。页面发生变化时,也更容易知道变化来自哪项设置。

总结

项目里可以确定一级入口数量和标题,依次调整三个窗口宽度档位,观察页签横向尺寸何时增加、图标之间是否突然出现大段空白。宽度稳定以后,调整页签离底边的距离,并把列表滚到底,确认最后一个按钮或列表项能够完整露出。

遮罩安排在最后。只有内容经过页签后方、背景细节开始干扰导航图标时才需要开启;固定滚动位置以后,比较颜色、高度和透明度。三个变量由此分开:底部间距只改变页签位置,遮罩只处理视觉过渡,它们都不会自动补足内容区留白。

我目前手里还没有可以测试 HarmonyOS 7 的真机,所以现在只能先在模拟器里验证,最终效果还是要以真机实际运行结果为准。

完整代码

/** * HarmonyOS 7 悬浮页签深度实战 03 * */​import {  HdsTabs,  HdsTabsController,  HdsTabsFloatingStyle} from '@kit.UIDesignKit';​@Entry@Componentstruct Main {  private tabsController: HdsTabsController = new HdsTabsController();​  // 0:紧凑,1:默认,2:宽松。  @State private widthProfile: number = 1;  @State private bottomMargin: number = 28;  @State private maskEnabled: boolean = true;  @State private maskHeight: number = 92;​  private getSmallWidth(): number {    if (this.widthProfile === 0) {      return 216;    }    if (this.widthProfile === 2) {      return 280;    }    return 240;  }​  private getMediumWidth(): number {    if (this.widthProfile === 0) {      return 280;    }    if (this.widthProfile === 2) {      return 368;    }    return 320;  }​  private getLargeWidth(): number {    if (this.widthProfile === 0) {      return 344;    }    if (this.widthProfile === 2) {      return 456;    }    return 400;  }​  private getFloatingStyle(): HdsTabsFloatingStyle {    if (this.maskEnabled) {      return {        barWidth: {          smallWidth: this.getSmallWidth(),          mediumWidth: this.getMediumWidth(),          largeWidth: this.getLargeWidth()        },        barBottomMargin: this.bottomMargin,        gradientMask: {          maskColor: '#66F1F3F5',          maskHeight: this.maskHeight        }      };    }​    return {      barWidth: {        smallWidth: this.getSmallWidth(),        mediumWidth: this.getMediumWidth(),        largeWidth: this.getLargeWidth()      },      barBottomMargin: this.bottomMargin    };  }​  @Builder  private sectionHeader(title: string, description: string) {    Column({ space: 4 }) {      Text(title)        .fontSize(14)        .fontWeight(FontWeight.Medium)        .fontColor('#18213A')        .width('100%')​      Text(description)        .fontSize(12)        .fontColor('#68708A')        .lineHeight(18)        .width('100%')    }    .width('100%')    .alignItems(HorizontalAlign.Start)  }​  @Builder  private widthControls() {    Column({ space: 10 }) {      this.sectionHeader(        'Width Profile',        '每个档位都会同时改写 small / medium / large 三个宽度。'      )​      Row({ space: 8 }) {        Button('紧凑')          .layoutWeight(1)          .height(38)          .fontSize(13)          .fontColor(this.widthProfile === 0 ? Color.White : '#5065E8')          .backgroundColor(this.widthProfile === 0 ? '#5065E8' : '#EEF1FF')          .onClick(() => {            this.widthProfile = 0;          })​        Button('默认')          .layoutWeight(1)          .height(38)          .fontSize(13)          .fontColor(this.widthProfile === 1 ? Color.White : '#5065E8')          .backgroundColor(this.widthProfile === 1 ? '#5065E8' : '#EEF1FF')          .onClick(() => {            this.widthProfile = 1;          })​        Button('宽松')          .layoutWeight(1)          .height(38)          .fontSize(13)          .fontColor(this.widthProfile === 2 ? Color.White : '#5065E8')          .backgroundColor(this.widthProfile === 2 ? '#5065E8' : '#EEF1FF')          .onClick(() => {            this.widthProfile = 2;          })      }      .width('100%')​      Text(        `当前三档:${this.getSmallWidth()} / ` +        `${this.getMediumWidth()} / ${this.getLargeWidth()} vp`      )        .fontSize(12)        .fontColor('#5065E8')        .width('100%')    }    .width('100%')    .alignItems(HorizontalAlign.Start)  }​  @Builder  private bottomMarginControls() {    Column({ space: 10 }) {      this.sectionHeader(        'Bottom Margin',        '16 / 28 / 40 vp 是本页实验值,用于观察页签离底部的距离。'      )​      Row({ space: 8 }) {        Button('16 vp')          .layoutWeight(1)          .height(38)          .fontSize(13)          .fontColor(this.bottomMargin === 16 ? Color.White : '#5065E8')          .backgroundColor(this.bottomMargin === 16 ? '#5065E8' : '#EEF1FF')          .onClick(() => {            this.bottomMargin = 16;          })​        Button('28 vp')          .layoutWeight(1)          .height(38)          .fontSize(13)          .fontColor(this.bottomMargin === 28 ? Color.White : '#5065E8')          .backgroundColor(this.bottomMargin === 28 ? '#5065E8' : '#EEF1FF')          .onClick(() => {            this.bottomMargin = 28;          })​        Button('40 vp')          .layoutWeight(1)          .height(38)          .fontSize(13)          .fontColor(this.bottomMargin === 40 ? Color.White : '#5065E8')          .backgroundColor(this.bottomMargin === 40 ? '#5065E8' : '#EEF1FF')          .onClick(() => {            this.bottomMargin = 40;          })      }      .width('100%')    }    .width('100%')    .alignItems(HorizontalAlign.Start)  }​  @Builder  private maskControls() {    Column({ space: 10 }) {      this.sectionHeader(        'Gradient Mask',        '先开关遮罩,再比较 72 / 92 / 112 vp 三种过渡高度。'      )​      Row({ space: 8 }) {        Button('关闭')          .layoutWeight(1)          .height(38)          .fontSize(13)          .fontColor(this.maskEnabled ? '#5065E8' : Color.White)          .backgroundColor(this.maskEnabled ? '#EEF1FF' : '#5065E8')          .onClick(() => {            this.maskEnabled = false;          })​        Button('开启')          .layoutWeight(1)          .height(38)          .fontSize(13)          .fontColor(this.maskEnabled ? Color.White : '#5065E8')          .backgroundColor(this.maskEnabled ? '#5065E8' : '#EEF1FF')          .onClick(() => {            this.maskEnabled = true;          })      }      .width('100%')​      Row({ space: 8 }) {        Button('72 vp')          .layoutWeight(1)          .height(36)          .fontSize(12)          .fontColor(this.maskHeight === 72 ? Color.White : '#596078')          .backgroundColor(this.maskHeight === 72 ? '#7483E8' : '#F2F3F7')          .onClick(() => {            this.maskHeight = 72;          })​        Button('92 vp')          .layoutWeight(1)          .height(36)          .fontSize(12)          .fontColor(this.maskHeight === 92 ? Color.White : '#596078')          .backgroundColor(this.maskHeight === 92 ? '#7483E8' : '#F2F3F7')          .onClick(() => {            this.maskHeight = 92;          })​        Button('112 vp')          .layoutWeight(1)          .height(36)          .fontSize(12)          .fontColor(this.maskHeight === 112 ? Color.White : '#596078')          .backgroundColor(this.maskHeight === 112 ? '#7483E8' : '#F2F3F7')          .onClick(() => {            this.maskHeight = 112;          })      }      .width('100%')    }    .width('100%')    .alignItems(HorizontalAlign.Start)  }​  @Builder  private parameterPanel() {    Column({ space: 18 }) {      this.widthControls()      Divider().color('#E8EAF0')      this.bottomMarginControls()      Divider().color('#E8EAF0')      this.maskControls()    }    .width('100%')    .padding(16)    .backgroundColor(Color.White)    .borderRadius(20)    .alignItems(HorizontalAlign.Start)  }​  @Builder  private infoCard(    index: string,    title: string,    description: string,    background: string  ) {    Column({ space: 8 }) {      Text(index)        .fontSize(11)        .fontWeight(FontWeight.Medium)        .fontColor('#5065E8')        .width('100%')​      Text(title)        .fontSize(18)        .fontWeight(FontWeight.Bold)        .fontColor('#17203A')        .width('100%')​      Text(description)        .fontSize(13)        .fontColor('#596078')        .lineHeight(20)        .width('100%')    }    .width('100%')    .height(132)    .padding(16)    .justifyContent(FlexAlign.Center)    .alignItems(HorizontalAlign.Start)    .backgroundColor(background)    .borderRadius(20)  }​  @Builder  private experimentPage(title: string, description: string) {    Scroll() {      Column({ space: 12 }) {        Column({ space: 6 }) {          Text(title)            .fontSize(27)            .fontWeight(FontWeight.Bold)            .fontColor('#11182C')            .width('100%')​          Text(description)            .fontSize(14)            .fontColor('#68708A')            .lineHeight(21)            .width('100%')        }        .width('100%')        .alignItems(HorizontalAlign.Start)​        this.parameterPanel()​        this.infoCard(          '01 · BLUE',          '窗口宽度实验',          '调整窗口或旋转设备,再切换 Width Profile,观察当前命中的宽度档位。',          '#E9F2FF'        )​        this.infoCard(          '02 · ORANGE',          '底部间距实验',          '依次选择 16、28、40 vp,观察页签与页面底边之间的距离。',          '#FFF0DF'        )​        this.infoCard(          '03 · GREEN',          '遮罩开关实验',          '滚动到彩色卡片经过页签后方的位置,再比较遮罩开启与关闭。',          '#E9F8F1'        )​        this.infoCard(          '04 · PURPLE',          '遮罩高度实验',          '遮罩开启时切换三档高度,观察内容淡出的起点是否过早。',          '#F3ECFF'        )​        this.infoCard(          '05 · RED',          '底部内容检查',          '继续上滑,确认最后一张卡片能够完整滚出悬浮页签覆盖区域。',          '#FFECEF'        )      }      .width('100%')      .padding({        left: 20,        right: 20,        top: 18,        bottom: 170      })    }    .width('100%')    .height('100%')    .scrollBar(BarState.Off)    .backgroundColor('#F4F6FB')  }​  build() {    Column() {      HdsTabs({        controller: this.tabsController      }) {        TabContent() {          this.experimentPage(            '悬浮页签参数实验',            '首页:集中观察 barWidth、barBottomMargin 和 gradientMask。'          )        }        .tabBar(          new BottomTabBarStyle(            $r('sys.media.ohos_ic_public_clock'),            '首页'          )        )​        TabContent() {          this.experimentPage(            '任务页',            '参数状态跨页签保留,便于检查三个页面的一致性。'          )        }        .tabBar(          new BottomTabBarStyle(            $r('sys.media.ohos_ic_public_clock'),            '任务'          )        )​        TabContent() {          this.experimentPage(            '我的页面',            '继续滚动彩色卡片,观察内容进入遮罩区域后的变化。'          )        }        .tabBar(          new BottomTabBarStyle(            $r('sys.media.ohos_ic_public_clock'),            '我的'          )        )      }      .barPosition(BarPosition.End)      .vertical(false)      .barOverlap(true)      .barFloatingStyle(this.getFloatingStyle())      .width('100%')      .height('100%')    }    .width('100%')    .height('100%')  }}


©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS

暂无评论数据

加载中...

发布

地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255