【HarmonyOS 7 悬浮页签深度实战】02 从普通底栏到第一个悬浮页签

文章目录

前言

已有 HarmonyOS 项目准备把普通底部导航改成悬浮页签时,代码变化其实不算多。真正容易出问题的地方,通常是页面已经换成了悬浮样式,脑子里还沿用着普通底栏的布局习惯。普通底栏很好理解。页面主体占一块区域,底部导航再占一块区域,两边各有自己的位置。内容写到导航栏上方,基本就结束了。

悬浮页签换了一种关系。页签栏可以盖在内容上面,页面内容还能继续往下延伸。这样做以后,底部会出现更明显的前后层次,图片、列表背景也能铺到页签后面。与此同时,底部按钮、输入框、最后几个列表项也有可能跑到页签下面。

所以,悬浮页签真正需要重新检查的地方,往往在页面底部。刚开始接触 HdsTabs 时,很容易把 barFloatingStyle 当成一个开关,觉得加上以后底栏自然就会浮起来。实际使用时,还需要 barPositionverticalbarOverlap 一起配合。少了其中一项,页面表现就可能和预期不同。

这里可以先记住一个很实用的开发习惯:先保留能正常工作的普通页签,再逐项增加悬浮配置。这样做虽然多了一次对照,排查时却轻松很多。普通模式正常,悬浮模式出现问题,范围已经缩小到了底部导航和内容布局;如果普通模式本身就不正常,那就先回去检查 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')
  }
}
相关推荐
whyutianict_vv1 小时前
从零到上架6款APP:AI鸿蒙全栈智能体开发5个月实战复盘(ArkTS/DevEco/AGC全流程)
人工智能·个人开发·harmonyos
OH_TPC2 小时前
HarmonyOS APP开发---"启动秀"应用引导页App,需要用到这个库
harmonyos
见山是山-见水是水4 小时前
围绕HTTP 网络请求实战构建原生体验:设计取舍、实现与排错
网络·网络协议·http·华为·harmonyos
见山是山-见水是水4 小时前
拆解网络缓存策略设计:原生鸿蒙页面的实现路径与调试方法
http·缓存·华为·harmonyos
蓝速科技5 小时前
蓝速鸿蒙信创终端长效流畅性深度评测
arm开发·华为·harmonyos
YM52e5 小时前
鸿蒙 ArkTS 实战|万能绿茶通用词汇库:20 词六大类 + 搜索筛选 + 潜台词解析
学习·华为·harmonyos
2301_780356705 小时前
开源鸿蒙+智慧医疗:全视通如何实现门诊与住院场景的设备互联互通?
华为·开源·harmonyos
是Yu欸5 小时前
openPangu-2.0 技术报告全文翻译(1):摘要、引言与混合注意力架构
人工智能·华为·架构·aigc·交互·agent
贾伟康5 小时前
【中国方言题库|13】HarmonyOS ArkTS 应用启动链路实战:从 EntryAbility 到首屏加载保持窗口与路由稳定
harmonyos·arkts·appstorage·应用启动·uiability