【HarmonyOS 6】底部悬浮导航的迷你栏适配(API23)

往期回顾:

在前两篇中,我们已经完成了两件事:

  1. 使用 HdsTabs 实现 API23 的底部悬浮页签。
  2. 通过 systemMaterialEffect 接入官方的沉浸光感材质。

但其实从 HarmonyOS 6.1.0(23) 开始,底部悬浮页签还新增了一个比较实用的能力:迷你栏 MiniBar

这篇我们就继续基于之前的悬浮页签,做一个可以实际运行的 Demo。


一、什么是迷你栏

官方文档里对迷你栏的描述是:

迷你栏是新增的自定义区域,跟页签栏高度相等且水平对齐,支持展开和折叠两种样式。

简单理解就是:

以前底部悬浮区域只有一个 TabBar,也就是我们常见的"首页 / 发现 / 消息 / 我的"。

现在可以在页签栏旁边再加一块自定义区域,例如:

  • 音乐播放控制器。
  • 课程播放进度。
  • 当前任务状态。
  • 快捷操作入口。

这块区域就是 miniBar

它不是普通的 Row 手写悬浮层,而是 HdsTabs 官方悬浮体系的一部分,所以它可以跟页签栏一起处理:

  • 悬浮背景。
  • 渐变蒙层。
  • 沉浸光感材质。
  • 折叠/展开动效。
  • 点击触发展开或收起。

二、最终 Demo 效果

这次 Demo 做了:

  • 底部是 HdsTabs 悬浮页签。
  • 左侧是一个迷你音乐播放栏。
  • 首次进入时迷你栏处于展开态。
  • 点击底部迷你栏或页签栏区域后,迷你栏会在展开态和折叠态之间切换。



三、开发前先看约束条件

文档中对这个能力有几个约束,初学者一定要先看。

3.1 页签栏放到底部

需要设置:

typescript 复制代码
.barPosition(BarPosition.End)
.vertical(false)

其中:

  • barPosition: BarPosition.End:表示页签栏在容器底部。
  • vertical(false):表示不是纵向页签,而是横向底部页签。

3.2 页签栏允许悬浮叠加

需要设置:

typescript 复制代码
.barOverlap(true)

这个配置的作用是让 TabBar 可以悬浮在 TabContent 上方。

如果不设置,底部页签就还是普通布局,不是我们要的悬浮效果。

3.3 当前支持的页签样式有限制

文档里也提到,当前支持的样式主要是:

  • BottomTabBarStyle
  • CustomBuilder

所以这篇 Demo 里,我们直接用 BottomTabBarStyle 来做底部页签,重点放在 miniBar 上。


四、导入模块与基础状态

先看导入和基础状态。

typescript 复制代码
import { HdsTabs, HdsTabsController, hdsMaterial } from '@kit.UIDesignKit';

这里用到了 3 个内容:

名称 作用
HdsTabs HDS 页签组件
HdsTabsController 页签控制器
hdsMaterial 沉浸光感材质参数

Demo 中的状态代码如下:

typescript 复制代码
@Entry
@Component
struct Index {
  private controller: HdsTabsController = new HdsTabsController();
  @State currentTab: number = 0;
  @State miniBarStyle: string = '折叠中';
  @State tabBarStyle: string = '展开';
  @State logText: string = '';

  aboutToAppear(): void {
    this.logText = '=== 迷你栏事件日志 ===\n';
  }
}

这里主要做两件事:

  1. 使用 HdsTabsController 初始化控制器。
  2. 用几个 @State 变量记录当前页签、迷你栏状态、页签栏状态和日志内容。

五、先写迷你栏内容

miniBar 本质上是一个自定义区域。

所以我们先通过 @Builder 写一个迷你栏 UI。

Demo 中我们把它做成了一个音乐播放控制器:左侧播放按钮,中间文字,右侧暂停按钮。

typescript 复制代码
// ========== 迷你栏构建器(音乐播放控制器) ==========
@Builder
miniBarBuilder() {
  Row() {
    // 折叠态主要展示这个按钮;展开态继续展示后面的文字和操作按钮。
    Column() {
      Image($r('sys.media.ohos_ic_public_play'))
        .width(26)
        .height(26)
        .fillColor('#007DFF')
    }
    .width(48)
    .height(48)
    .justifyContent(FlexAlign.Center)
    .borderRadius(24)
    .backgroundColor('#E8F0FE')
    .margin({ left: 4, right: 8 })

    Column({ space: 2 }) {
      Text('正在播放')
        .fontSize(12)
        .fontColor('#182431')
        .fontWeight(FontWeight.Medium)
      Text('HarmonyOS 入门到精通')
        .fontSize(10)
        .fontColor('#8D9299')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .width(116)
    .alignItems(HorizontalAlign.Start)

    Column() {
      Image($r('sys.media.ohos_ic_public_pause'))
        .width(22)
        .height(22)
        .fillColor('#007DFF')
    }
    .width(48)
    .height(48)
    .justifyContent(FlexAlign.Center)
    .borderRadius(24)
    .backgroundColor('#E8F0FE')
    .margin({ left: 8, right: 4 })
  }
  .height(48)
  .alignItems(VerticalAlign.Center)
}

这里有两个细节要注意。

5.1 不要给迷你栏根节点强行写 width('100%')

迷你栏的宽度应该交给 miniBarWidth 和组件内部的折叠/展开逻辑管理。

如果我们在 miniBarBuilder() 的根节点上强行写:

typescript 复制代码
.width('100%')

就可能导致折叠时视觉表现不明显,看起来像没有收回。

所以这里根节点只设置高度:

typescript 复制代码
.height(48)

5.2 折叠态主要看第一个元素

迷你栏折叠时,组件会把内容裁剪到较小宽度。

所以我们把最重要的播放按钮放在最左边:

typescript 复制代码
Image($r('sys.media.ohos_ic_public_play'))

这样折叠后,用户还能看到一个明确的入口。


六、接入 HdsTabs

有了迷你栏内容后,就可以放进 HdsTabs 了。

Demo 中一共放了 4 个页签。

typescript 复制代码
HdsTabs({ controller: this.controller }) {
  TabContent() {
    this.HomeTabContent()
  }
  .tabBar(new BottomTabBarStyle($r('sys.media.ohos_ic_public_clock'), '首页'))

  TabContent() {
    this.DiscoverTabContent()
  }
  .tabBar(new BottomTabBarStyle($r('sys.media.ohos_ic_public_clock'), '发现'))

  TabContent() {
    this.MessageTabContent()
  }
  .tabBar(new BottomTabBarStyle($r('sys.media.ohos_ic_public_clock'), '消息'))

  TabContent() {
    this.ProfileTabContent()
  }
  .tabBar(new BottomTabBarStyle($r('sys.media.ohos_ic_public_clock'), '我的'))
}

这里的页签图标都使用了同一个系统资源:

typescript 复制代码
$r('sys.media.ohos_ic_public_clock')

主要是为了保证 Demo 能稳定编译运行。不同 SDK 暴露的系统资源名可能有差异,教程 Demo 优先保证稳定性。


七、核心:barFloatingStyle 配置

迷你栏的核心配置都在 .barFloatingStyle() 里。

这是本篇最关键的代码:

typescript 复制代码
.width('100%')
.height('100%')
// 基础布局:底部、水平、悬浮叠加
.barOverlap(true)
.barPosition(BarPosition.End)
.vertical(false)
// 核心新特性:悬浮样式 + 迷你栏
.barFloatingStyle({
  // 页签栏分档宽度(响应式)
  barWidth: { smallWidth: 240, mediumWidth: 300, largeWidth: 360 },
  // 距离底部边距
  barBottomMargin: 28,
  // 渐变蒙层
  gradientMask: { maskColor: '#66F1F3F5', maskHeight: 92 },
  // 沉浸光感材质
  systemMaterialEffect: {
    materialType: hdsMaterial.MaterialType.IMMERSIVE,
    materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
  },
  // 迷你栏配置
  miniBar: {
    miniBarBuilder: () => this.miniBarBuilder(),
    // 手机宽度下首次进入先展示展开态,点击迷你栏/页签栏后会在展开态和折叠态之间切换。
    miniBarStyle: 1,
    // 迷你栏展开态分档宽度,折叠态由组件内部按内容高度收起。
    miniBarWidth: { smallWidth: 240, mediumWidth: 260, largeWidth: 320 },
    // 样式变更回调
    onBarStyleChange: (miniBarStyle: number, tabBarStyle: number,
                       _miniBarWidth: number, _tabBarWidth: number,
                       mode: number) => {
      this.miniBarStyle = miniBarStyle === 0 ? 'COLLAPSE(折叠)' : 'EXPAND(展开)';
      this.tabBarStyle = tabBarStyle === 0 ? 'COLLAPSE(折叠)' : 'EXPAND(展开)';
      const modeStr = mode === 0 ? 'NORMAL(屏幕尺寸)' :
        mode === 1 ? 'USER_CLICK(用户点击)' : 'APP_TRIGGER(应用触发)';
      this.appendLog(`onBarStyleChange → 迷你栏:${this.miniBarStyle} | 页签栏:${this.tabBarStyle} | 模式:${modeStr}`);
    },
    // 迷你栏动画开始回调
    onMiniBarAnimationStart: (style: number, width: number) => {
      const styleStr = style === 0 ? '折叠中...' : '展开中...';
      this.appendLog(`迷你栏动画开始 → ${styleStr} 目标宽度:${width}vp`);
    },
    // 迷你栏动画结束回调
    onMiniBarAnimationEnd: (style: number) => {
      const styleStr = style === 0 ? '已折叠' : '已展开';
      this.appendLog(`迷你栏动画结束 → ${styleStr}`);
    },
    // 页签栏动画开始回调
    onTabBarAnimationStart: (style: number, width: number) => {
      const styleStr = style === 0 ? '折叠中...' : '展开中...';
      this.appendLog(`页签栏动画开始 → ${styleStr} 目标宽度:${width}vp`);
    },
    // 迷你栏背景跟随页签栏
    enableMiniBarBackground: true,
    // 必须裁剪内容,否则折叠时内部文字仍可能露出,看起来就像没有收回。
    enableMiniBarClip: true
  }
})

看起来配置比较多,但初学者可以先把它分成两部分理解。


八、第一部分:页签栏悬浮样式

这一部分控制的是底部页签栏本身。

typescript 复制代码
barWidth: { smallWidth: 240, mediumWidth: 300, largeWidth: 360 },
barBottomMargin: 28,
gradientMask: { maskColor: '#66F1F3F5', maskHeight: 92 },
systemMaterialEffect: {
  materialType: hdsMaterial.MaterialType.IMMERSIVE,
  materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
}

8.1 barWidth

barWidth 是页签栏的分档宽度。

官方文档中,HdsBarWidthRangeOptions 提供了 3 个宽度档位:

属性 含义
smallWidth HdsTabs 宽度小于 440vp 时使用
mediumWidth 中等宽度场景使用
largeWidth 大屏或宽屏场景使用

Demo 中这样设置:

typescript 复制代码
barWidth: { smallWidth: 240, mediumWidth: 300, largeWidth: 360 }

这样在不同宽度设备上,底部页签栏会有不同宽度。

8.2 barBottomMargin

typescript 复制代码
barBottomMargin: 28

表示底部页签栏距离 HdsTabs 底部 28vp。

这个值不建议太小,否则悬浮感不明显;也不建议太大,否则容易遮挡内容。

8.3 gradientMask

typescript 复制代码
gradientMask: { maskColor: '#66F1F3F5', maskHeight: 92 }

这个是底部背板蒙层。

它的作用是让底部悬浮区域和页面内容之间有一个柔和过渡,不会显得特别突兀。

8.4 systemMaterialEffect

typescript 复制代码
systemMaterialEffect: {
  materialType: hdsMaterial.MaterialType.IMMERSIVE,
  materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
}

这部分就是上一篇讲过的沉浸光感材质。

这里我们使用:

  • IMMERSIVE:沉浸式材质。
  • ADAPTIVE:材质等级交给系统自适应。

九、第二部分:迷你栏配置

真正让迷你栏生效的是 miniBar

typescript 复制代码
miniBar: {
  miniBarBuilder: () => this.miniBarBuilder(),
  miniBarStyle: 1,
  miniBarWidth: { smallWidth: 240, mediumWidth: 260, largeWidth: 320 },
  enableMiniBarBackground: true,
  enableMiniBarClip: true
}

9.1 miniBarBuilder

typescript 复制代码
miniBarBuilder: () => this.miniBarBuilder()

这里把前面写好的 @Builder miniBarBuilder() 传给 HdsTabs

也就是说,迷你栏显示什么内容,完全由我们自己决定。

9.2 miniBarStyle

typescript 复制代码
miniBarStyle: 1

文档中的 HdsBarStyle 有两个值:

含义
0 COLLAPSE,折叠样式
1 EXPAND,展开样式

Demo 中设置为 1,也就是首次进入页面时先展示展开态。

这样更适合截图和教学,因为一进入页面就能看到完整迷你栏内容。

文档也说明了:miniBarStyle 仅在 HdsTabs 宽度小于 600vp 时生效。大屏场景下,首次创建时通常会按展开样式展示。

9.3 miniBarWidth

typescript 复制代码
miniBarWidth: { smallWidth: 240, mediumWidth: 260, largeWidth: 320 }

这个控制的是迷你栏展开态的宽度。

文档里也提醒了:迷你栏最大宽度不超过 328vp。

所以我们这里最大给到 320,没有超过限制。

9.4 enableMiniBarBackground

typescript 复制代码
enableMiniBarBackground: true

表示迷你栏背景跟随页签栏背景。

如果设置为 false,迷你栏背景会变成透明,视觉上就不会跟底部页签栏融在一起。

9.5 enableMiniBarClip

typescript 复制代码
enableMiniBarClip: true

这是本篇中最容易踩坑的地方。

如果设置为 false,迷你栏折叠时,miniBarBuilder 里的文字或按钮可能仍然露出来。

这样看起来就像"左侧迷你栏没有收回"。

所以如果我们希望折叠状态表现正常,一般建议开启裁剪:

typescript 复制代码
enableMiniBarClip: true

十、如何监听折叠和展开

文档中提供了几个回调,用来监听迷你栏和页签栏的状态变化。

10.1 样式变化回调

Demo 中使用的是:

typescript 复制代码
onBarStyleChange: (miniBarStyle: number, tabBarStyle: number,
                   _miniBarWidth: number, _tabBarWidth: number,
                   mode: number) => {
  this.miniBarStyle = miniBarStyle === 0 ? 'COLLAPSE(折叠)' : 'EXPAND(展开)';
  this.tabBarStyle = tabBarStyle === 0 ? 'COLLAPSE(折叠)' : 'EXPAND(展开)';
  const modeStr = mode === 0 ? 'NORMAL(屏幕尺寸)' :
    mode === 1 ? 'USER_CLICK(用户点击)' : 'APP_TRIGGER(应用触发)';
  this.appendLog(`onBarStyleChange → 迷你栏:${this.miniBarStyle} | 页签栏:${this.tabBarStyle} | 模式:${modeStr}`);
}

这里可以拿到 5 个参数:

参数 含义
miniBarStyle 当前迷你栏样式
tabBarStyle 当前页签栏样式
miniBarWidth 当前迷你栏宽度
tabBarWidth 当前页签栏宽度
mode 本次变化的触发模式

文档中 mode 对应 HdsTabsBarChangeMode

含义
0 NORMAL,屏幕尺寸变化触发
1 USER_CLICK,用户点击触发
2 APP_TRIGGER,应用触发

在我们的 Demo 里,最常见的是用户点击触发,也就是 USER_CLICK

10.2 迷你栏动画开始

typescript 复制代码
onMiniBarAnimationStart: (style: number, width: number) => {
  const styleStr = style === 0 ? '折叠中...' : '展开中...';
  this.appendLog(`迷你栏动画开始 → ${styleStr} 目标宽度:${width}vp`);
}

这个回调会在迷你栏折叠/展开动画开始时触发。

10.3 迷你栏动画结束

typescript 复制代码
onMiniBarAnimationEnd: (style: number) => {
  const styleStr = style === 0 ? '已折叠' : '已展开';
  this.appendLog(`迷你栏动画结束 → ${styleStr}`);
}

这个回调会在动画结束时触发。

10.4 页签栏动画开始

typescript 复制代码
onTabBarAnimationStart: (style: number, width: number) => {
  const styleStr = style === 0 ? '折叠中...' : '展开中...';
  this.appendLog(`页签栏动画开始 → ${styleStr} 目标宽度:${width}vp`);
}

这里监听的是底部页签栏自己的折叠/展开动画。


十一、事件日志怎么做

为了方便观察回调触发顺序,Demo 中做了一个简单的日志方法:

typescript 复制代码
private appendLog(msg: string): void {
  const time = new Date().toLocaleTimeString();
  this.logText += `[${time}] ${msg}\n`;
}

首页中直接显示这个日志:

typescript 复制代码
Text(this.logText)
  .fontSize(11)
  .fontColor('#666666')
  .width('100%')
  .maxLines(10)
  .textAlign(TextAlign.Start)
  .backgroundColor('#F5F5F5')
  .borderRadius(12)
  .padding(12)

这样在点击底部迷你栏或页签栏时,就可以看到类似:

text 复制代码
onBarStyleChange → 迷你栏:COLLAPSE(折叠) | 页签栏:EXPAND(展开) | 模式:USER_CLICK(用户点击)
迷你栏动画开始 → 折叠中... 目标宽度:xxvp
迷你栏动画结束 → 已折叠

这对初学者很友好,因为可以直接看到"点击之后到底发生了什么"。


十二、总结

这篇我们在前两篇的基础上,继续补齐了 API23 新增的迷你栏能力。

到这里,API23 的底部悬浮导航三件套基本就完整了:

  • 悬浮页签。
  • 沉浸光感。
  • 迷你栏。
相关推荐
@不误正业2 小时前
OpenHarmony-A2A协议实战-多智能体跨应用协同架构与实现
人工智能·架构·harmonyos·开源鸿蒙
空中海2 小时前
iOS 静态逆向、IPA 结构与 Mach-O 分析
ios·华为·harmonyos
李李李勃谦2 小时前
鸿蒙PC文件加密工具实战:AES-256-GCM 加密
华为·harmonyos
maaath2 小时前
【maaath】Flutter for OpenHarmony打造跨平台便签备忘录应用
flutter·华为·harmonyos
李李李勃谦3 小时前
鸿蒙PC思维导图工具实战:拖拽交互与多格式导出
华为·交互·harmonyos
xmdy58663 小时前
Flutter+开源鸿蒙实战|智联邻里Day8 Lottie动画集成+url_launcher跳转拨号+个人中心完善+全局UI统一
flutter·开源·harmonyos
SmartBrain3 小时前
从Prompt工程到Harness工程:AI Agent落地之路
人工智能·python·华为·aigc
liulian091615 小时前
Flutter for OpenHarmony 跨平台开发:BMI计算器功能实战指南
flutter·华为
xmdy586618 小时前
Flutter+开源鸿蒙实战|智安盾电商溯源平台Day1 项目搭建与整体方案拆解
flutter·开源·harmonyos