前言

一个已经运行了一段时间的 HarmonyOS 项目,底部 Tabs 通常早就超出了页面切换的范围。当前索引、列表筛选、表单草稿、播放进度、路由入口和埋点,都可能围绕这组导航组织起来。

此时准备接入 HdsTabs,工作量经常会被低估。

代码里把 Tabs 替换成 HdsTabs 并不复杂,真正麻烦的是迁移过程中同时修改了导航容器、控制器、悬浮布局和业务页面。页面出现状态丢失、底部遮挡或者重复请求以后,很难马上确认问题来自哪一层。

假设用户已经进入任务页,填写了一半表单。页面切换成悬浮 HdsTabs 后,任务页突然回到首页,表单内容也恢复了初始值。这个现象表面上发生在导航切换时,原因却可能分布在多个位置。当前索引可能保存在旧的 TabsController 里,表单状态可能位于即将销毁的 TabContent 中,悬浮栏又可能覆盖了底部操作按钮。

我处理这类迁移时,通常会先保留一份能够正常运行的 ArkUI Tabs,再把业务状态移动到导航容器之外。随后依次验证普通 HdsTabs、悬浮 HdsTabs 和其他可选能力。每次只改变一层,出现异常时也能够退回上一种已经确认的状态。

这种处理方式看起来多了几次比较,后面反而会省下不少排查时间。尤其是已经上线的项目,导航调整不应该牵着业务页面一起重新编写。

HdsTabs 的子组件仍然是 TabContentHdsTabsOptions 继承自 TabsOptions。现有业务页面具备继续复用的基础,真正需要替换的是外层容器和控制器。HdsTabsController 继承自 TabsController,但一个控制器不能同时管理多个 HdsTabs,因此普通模式和悬浮模式需要分别准备控制器。

我目前手里还没有可以测试 HarmonyOS 7 的真机,所以现在主要通过 HarmonyOS 7 模拟器检查导航切换、状态保存和页面布局。系统能力、设备差异以及最终交互体验,仍然要以真机实际运行结果为准。

一、先把业务状态稳定

现有项目里的 Tabs 往往同时管理两类内容。一类属于导航,例如当前选中的页签索引、主动跳转和滑动切换。另一类属于业务,例如表单草稿、筛选条件、未完成任务、播放进度和操作次数。

两类状态混在同一个组件内部时,导航容器被替换,业务数据也容易跟着重新初始化。

这也是为什么迁移以前要先保留一份基线。这里的基线不能只有一张页面截图,它需要真实记录当前导航如何工作,用户操作以后哪些状态会变化,窗口调整以后页面是否仍然可用。

可以先整理下面几类内容。

需要保留的内容迁移前需要确认什么
导航状态启动页签、点击切换、滑动切换、业务按钮跳转、返回页面后的选中项
业务状态表单草稿、列表筛选、任务进度、播放状态、操作计数
页面布局底部安全区、最后一个列表项、固定按钮、横竖屏、分屏与自由窗口
工程环境当前 SDK、目标设备、构建配置以及能够正常安装的版本

这张表的用途是帮助项目确认迁移以后到底有没有退化。例如页面切换到 HdsTabs 后,首页、任务和我的三个入口都能点击,这只能说明基础导航还能工作。如果任务页原来的草稿消失了,或者列表最后一个按钮被悬浮栏挡住,迁移依然没有完成。

TabContent 可以复用,业务状态需要重新检查归属

HdsTabs 继续使用 TabContent 作为子组件,HdsTabsOptions 也继承了 TabsOptions。因此,已经存在的首页、任务页和个人页面通常可以继续使用,不需要为了更换导航容器重新复制一套业务界面。

更需要检查的是这些页面把状态保存在哪里。

下面这种状态可以放到导航容器外层:

@State private currentIndex: number = 0;
@State private draftText: string = '';
@State private actionCount: number = 0;

ArkUI Tabs、普通 HdsTabs 和悬浮 HdsTabs 都读取同一份 currentIndex。用户点击页签以后,onChange 再把实际索引保存回来。表单草稿和操作计数同样位于外层,因此更换导航容器时仍然能够保留。

这里还要区分业务状态和界面瞬时状态。表单内容、当前任务和筛选条件通常需要继续保存。滚动位置、输入焦点和局部动画状态不会自动跟着业务数据迁移。项目希望保留滚动位置时,需要使用独立 Scroller 或其他状态机制;希望恢复输入焦点时,也要另外处理焦点交接。

我们的 Demo 主要验证当前页签、草稿和操作计数。它并不能凭空保证所有滚动位置和输入焦点都自动保存。这个边界说明白以后,读者也不会误以为把状态放到父组件就解决了所有页面恢复问题。

可运行的旧方案需要一直留到迁移结束

导航迁移期间,原有 ArkUI Tabs 最好继续保持可构建状态。项目出现 HDS 导入失败、目标设备不支持、悬浮布局遮挡或者发布时间收紧时,团队需要能够快速返回原来的普通导航。如果旧方案已经在迁移初期被删除,回退就会变成重新开发。

我更倾向于让旧方案和新方案共用业务页面,只在外层选择不同导航容器。这样既能比较迁移结果,也能验证业务状态有没有被导航容器绑死。

真实项目可以使用构建配置、页面名单或者功能开关选择导航方案。开关由谁维护、什么时候生效、准备保留多久,都应该在工程说明中写清楚。否则几个月以后再看到一个 useFloatingTabs,很难判断它是长期产品能力,还是当时没有清理的实验代码。

二、导航容器逐层替换

业务状态已经移出导航容器以后,迁移可以开始进入组件层。这时候先别一股脑把悬浮样式、MiniBar、智感握姿和宽窗口策略全部加入。任何一项出现异常,排查范围都会迅速扩大。

比较稳妥的方式,是让每一次修改都能回答一个明确问题。

普通 HdsTabs 先验证基础替换第一轮只把 ArkUI Tabs 替换成普通 HdsTabs

HdsTabs 从 6.0.0(20) 开始提供,只能用于 Stage 模型,系统能力为 SystemCapability.UIDesign.HDSComponent.Core。它在 Phone、PC/2in1 和 Tablet 上可以正常调用,在 TV 上没有效果。

这一轮保持普通底部导航,不配置 barFloatingStyle,也不打开内容重叠。需要确认的内容包括:

  • @kit.UIDesignKit 可以正常导入
  • 原有 TabContent 可以继续显示
  • 初始索引和切换事件保持正常
  • HdsTabsController 与当前 HdsTabs 一一对应
  • 表单草稿和业务数据没有重新初始化

HdsTabsController 不能同时控制多个 HdsTabs。如果页面同时保留普通和悬浮两种分支,每个分支都需要独立控制器。共享的应该是页签索引和业务数据,不是控制器实例。

普通 HdsTabs 已经稳定以后,项目至少知道 HDS 依赖、TabContent 和基础状态没有问题。后面再出现底部遮挡,原因就更可能来自悬浮布局。

悬浮配置只处理导航位置和内容覆盖

普通 HdsTabs 通过以后,再增加悬浮配置:

.barPosition(BarPosition.End)
.vertical(false)
.barOverlap(true)
.barFloatingStyle({
  // 悬浮页签配置
})

barFloatingStyle 从 6.1.0(23) 开始提供。悬浮页签要求 TabBar 位于底部、采用横向排列、开启内容重叠,并使用受支持的底部页签样式或自定义 Builder。

页面进入悬浮状态以后,内容区域可以继续延伸到 TabBar 后方。这一轮需要重新检查末尾列表项、固定按钮、输入框、安全区和系统底部区域。

如果问题只在悬浮模式中出现,普通 HdsTabs 仍然正常,就没有必要重新检查业务请求和整个页面模型。可以先回到 barOverlap、内容底部留白和 barFloatingStyle

MiniBar 和智感握姿属于可选能力

最小悬浮页签能够稳定工作以后,再评估 MiniBar 和智感握姿。MiniBar 适合展示播放、下载、录音等持续任务。项目没有持续状态时,为了视觉效果加入 MiniBar,反而会占用底部空间。

adaptToHandedness 还需要支持设备和真机条件。代码能够编译,只能说明属性写法被 SDK 接受,无法证明当前设备已经完成左右手跟随。

这里可以把整个迁移过程整理成几个容易回退的步骤。

迁移步骤本轮只修改什么需要确认的结果出现异常时返回哪里
ArkUI Tabs 基线整理共享索引和业务状态原页面行为保持不变原有业务页面
普通 HdsTabs替换容器和控制器三个页签、索引和业务状态正常ArkUI Tabs
最小悬浮 HdsTabs增加悬浮所需属性TabBar 悬浮,末尾内容可操作普通 HdsTabs
可选能力按需加入 MiniBar 或握姿适配每项都有独立验证结果最小悬浮 HdsTabs
多窗口回归检查分屏、自由窗口和大尺寸窗口页面状态在窗口变化后仍然连续已通过的小窗口方案

三、canIUse 不能替项目选择导航形态

跨设备项目里,运行时能力判断确实很重要。HarmonyOS 提供全局 canIUse(syscap),用于查询当前系统是否具备指定系统能力。这个接口从 API 8 开始提供,返回一个布尔值。

当前 Demo 使用:

const hdsCoreAvailable: boolean = canIUse(
  'SystemCapability.UIDesign.HDSComponent.Core'
);

这个结果可以帮助页面决定是否进入 HDS 分支,但它能够回答的问题非常有限。

true 表示系统具备 HDS Core 能力。它不能说明当前页面一定适合悬浮页签,也不能证明 MiniBar、智感握姿和宽窗口表现已经完成验证。普通 HdsTabs 和悬浮 HdsTabs都标注同一个 HDS Core SysCap,因此 canIUse 无法继续告诉应用应该选择普通模式还是悬浮模式。

这两种模式的选择仍然来自项目策略,例如:

  • 当前页面是否适合底部悬浮导航
  • 悬浮区域是否已经处理内容遮挡
  • 当前版本是否完成 MiniBar 和握姿回归
  • 目标窗口形态是否已经验证
  • 发布计划能否承担对应回归范围

所以 Demo 中的三个按钮承担不同职责。canIUse 决定 HDS 分支能否使用,页面按钮决定当前希望比较 ArkUI Tabs、普通 HdsTabs 还是悬浮 HdsTabs。

运行时判断不能补救编译期缺失

还有一个经常被忽略的边界。当前 SDK 如果没有 HdsTabsbarFloatingStyle 的类型声明,源码会在编译阶段失败。此时即使代码里写了 canIUse,应用也没有机会运行到这个判断。

因此,开发环境和运行设备要分开处理。

编译 SDK 负责提供符号和类型,canIUse 负责查询当前设备的系统能力。项目准备覆盖较低系统版本或者不同设备时,需要保证整个安装包能够编译,同时给不具备对应能力的设备保留合法分支。

SysCap 的要求能力集还会影响应用分发。某项能力如果被列为应用必须具备的能力,缺少该能力的设备可能无法安装应用,运行时回退自然也不会被执行。准备使用 canIUse 完成运行时分支时,需要同时检查项目的 SysCap 配置和目标设备范围。

TV 设备需要单独处理

HdsTabs 在 TV 上没有效果,这属于组件的设备行为差异。

因此,项目需要覆盖 TV 时,不能仅凭 HDS Core SysCap 就假设 HdsTabs 一定能够正常显示。更稳妥的方式是从产品形态或者设备配置层明确保留 ArkUI Tabs 方案。当前 Demo 主要面向 Phone、Tablet 和 PC/2in1,用 canIUse 演示 HDS Core 判断。TV 目标需要在实际项目中增加对应的产品策略,这部分不能依靠一个通用布尔值自动完成。

四、出现异常后先回到普通 HdsTabs

悬浮导航已经加入以后,页面异常经常会集中出现。

有些问题来自 SDK 和导入,有些来自内容覆盖,还有一些来自状态归属。排查时如果始终停留在最复杂的悬浮方案中,多个变量会互相干扰。

我一般会先切回普通 HdsTabs。

普通 HdsTabs 同样异常,说明问题更可能位于 HDS 依赖、TabContent、控制器或共享状态。普通模式正常,悬浮模式异常,范围就缩小到了悬浮属性、内容重叠和底部布局。

页面现象优先检查位置常见处理
找不到 HdsTabs当前 SDK、导入路径和 Stage 模型确认 SDK 包含 API 20 及以上 HdsTabs 类型
找不到 barFloatingStyle当前 SDK 与 API 版本悬浮样式从 6.1.0(23) 开始提供
普通 HdsTabs 可以显示,底栏没有悬浮barPositionverticalbarOverlapbarFloatingStyle回到最小悬浮配置逐项检查
最后一个列表项被底栏遮挡内容底部留白和固定操作区让可操作内容完整滚出悬浮区域
TabBar 宽度与预期不同HdsTabs 自身宽度、高宽比和三档配置不要根据设备名称或者整窗宽度推断档位
切换页签后索引不同步indexonChange 和状态保存位置让所有导航分支共用同一个索引状态
切换导航后草稿消失业务状态是否位于条件分支内部把草稿和任务状态移动到更稳定的父层
滚动位置或输入焦点丢失是否显式保存 Scroller 和焦点状态业务状态保留并不等于界面瞬时状态自动恢复
MiniBar 出现位置异常最小悬浮模式是否已经稳定暂时移除 MiniBar,再检查 Builder 和宽度配置
智感握姿没有变化设备、系统条件和测试姿态编译通过后仍需支持设备完成真机验证
TV 上 HdsTabs 没有显示设备行为和产品分支TV 保留 ArkUI Tabs 或其他适配导航
切换模式后重复请求是否同时创建多套导航页面使用条件分支仅创建当前导航,业务任务由共享状态管理
系统图标或资源报错$r 中的资源是否真实存在使用项目资源或当前 SDK 能够确认的系统资源
@Entry 根节点报错build() 是否具有合法唯一根容器将导航分支放到同一个根容器内部

排查时每次只修改一个变量。

例如普通 HdsTabs 已经正常,加入悬浮配置以后最后一个按钮被遮挡,这时候应该先处理内容底部空间。继续更换控制器或者重建 TabContent,只会把原本已经确认的部分重新带入排查范围。

五、回退方案要在发布以前真正走一遍

准备了三个导航分支,并不等于项目已经具备可靠回退能力。

应急方案长期没有编译和测试,几周以后就可能因为页面参数、资源名称或者状态模型发生变化而失效。到了线上真正需要回退时,才发现普通 HdsTabs 无法构建,这条分支就失去了意义。

当前迁移可以保留三种方案。

悬浮 HdsTabs 适合已经完成以下检查的页面:

  • 项目 SDK 和系统范围覆盖悬浮能力
  • HDS Core 可以使用
  • 一级入口数量适合底部导航
  • 内容遮挡和安全区域已经处理
  • 多窗口和目标设备已经完成回归

MiniBar 和智感握姿没有完成验证时,可以先关闭可选能力,保留最小悬浮导航。

普通 HdsTabs 适合 HDS 基础导航已经稳定,但悬浮区域、MiniBar、握姿或者宽窗口表现暂时没有足够证据的项目。

回到普通 HdsTabs 时,页面可以移除 barFloatingStyle,关闭 barOverlap,继续复用现有 TabContent、业务状态和 HdsTabsController。视觉范围缩小以后,后续版本仍然可以重新评估悬浮方案。

ArkUI Tabs 适合这些情况:

  • 当前开发环境暂时无法引入 UI Design Kit
  • 目标设备不具备 HDS Core 能力
  • 产品需要覆盖 TV
  • 当前发布时间不足以完成 HDS 回归
  • 现有普通导航已经满足产品需求

这三种方案之间可以按照下面的关系理解。

当前条件可以保留的方案需要暂时关闭什么
HDS、悬浮布局和目标设备全部完成验证悬浮 HdsTabs未验证的 MiniBar 或握姿能力
HDS 基础导航稳定,悬浮表现仍需补测普通 HdsTabsbarFloatingStyle 与内容重叠
HDS 环境或目标设备条件不满足ArkUI TabsHDS 相关组件与配置

验证回退时,不要停留在首页空白状态。

更有价值的方式是先进入任务页,修改表单,滚动列表,再完成一次业务操作。随后依次切换普通 HdsTabs 和 ArkUI Tabs,检查当前页签、草稿、计数和可操作区域。

验收动作应该保留的结果失败以后检查什么
悬浮模式完成一段业务操作状态写入共享模型,底部操作区可以点击状态归属、内容留白和可选能力
切换到普通 HdsTabs当前索引、草稿和业务计数继续保留构造参数、onChange 和父层状态
切换到 ArkUI Tabs页面继续可操作,各控制器没有混用独立控制器和共享索引
小窗口调整到宽窗口后返回导航方案符合项目规则,任务没有中断窗口策略和业务状态保存
应用退到后台再返回当前路由和可恢复数据符合产品约定持久化范围和开关读取时机

这里还要明确功能开关什么时候生效。

应用冷启动时读取配置,导航可以在页面创建以前确定。配置需要热更新时,正在输入或者播放的页面突然更换导航容器,可能会丢失焦点和局部状态。项目可以让新配置在下次进入页面时生效,也可以专门完成状态交接。

哪一种处理方式都可以,重点是提前写清楚,不能让一个远端布尔值在任意时刻重新创建整张页面。

长期只验证默认的悬浮方案,普通 HdsTabs 和 ArkUI Tabs 会慢慢失去维护。发布候选版本中最好分别构建三种导航,至少完成一遍主要业务流程。产品已经决定永久删除某个方案时,也要同时删除开关、控制器和测试项,避免留下看起来可以回退、实际已经失效的分支。

页面上线以后,现象和处理方式也应该提前对应起来。

底部出现遮挡,可以暂时关闭悬浮样式;HDS 基础组件在目标设备上不可用,需要回到 ArkUI Tabs;业务数据发生错误,则要检查共享状态和页面模型,更换底栏无法修复数据问题。这种对应关系提前准备好以后,真正出现问题时,团队才能选择影响范围较小的方案。

总结

现有 Tabs 迁移到悬浮 HdsTabs,最需要保护的有两部分。一部分是始终可以构建和使用的普通导航,另一部分是独立于导航容器的业务状态。

首页、任务页和个人页面继续使用原来的 TabContent,当前索引、表单草稿和业务数据放到稳定的父层。导航容器按照 ArkUI Tabs、普通 HdsTabs、悬浮 HdsTabs 的顺序逐层替换,每次只增加一组新的变量。出现异常以后,也能够返回上一种已经确认的方案。

canIUse 可以查询 HDS Core SysCap,帮助应用判断当前系统是否具备对应能力。普通 HdsTabs 和悬浮 HdsTabs 的选择仍然需要结合页面结构、目标设备和回归结果。编译环境缺少 HDS API 时,运行时判断无法补救;应用还要覆盖 TV 时,也需要保留独立的导航策略。

回退能力最终要通过真实操作确认。进入任务页、修改草稿、增加业务计数,再切换三种导航,能够比首页空白截图暴露更多问题。滚动位置、输入焦点和未完成任务也要根据项目要求分别处理,不能把它们都归到一个 currentIndex 里。

我目前手里还没有可以测试 HarmonyOS 7 的真机,所以现在只能先通过 HarmonyOS 7 模拟器检查导航切换、状态保存和页面布局。系统能力、设备差异、多窗口体验以及最终交互效果,仍然要以真机实际运行结果为准。

完整代码

Main.ets

/**
 * HarmonyOS 7 悬浮页签深度实战 08
 *
 */

import {
  HdsTabs,
  HdsTabsController,
  HdsTabsFloatingStyle
} from '@kit.UIDesignKit';

enum NavigationMode {
  ARKUI_TABS = 0,
  HDS_TABS = 1,
  FLOATING_HDS = 2
}

@Entry
@Component
struct Main {
  private arkTabsController: TabsController = new TabsController();
  private plainHdsController: HdsTabsController =
    new HdsTabsController();
  private floatingHdsController: HdsTabsController =
    new HdsTabsController();

  private tabNames: string[] = ['首页', '任务', '我的'];

  @State private preferredMode: NavigationMode =
    NavigationMode.FLOATING_HDS;
  @State private hdsCoreAvailable: boolean = false;
  @State private currentIndex: number = 0;
  @State private actionCount: number = 0;
  @State private draftText: string = '迁移前保留下来的业务草稿';

  aboutToAppear(): void {
    this.hdsCoreAvailable = canIUse(
      'SystemCapability.UIDesign.HDSComponent.Core'
    );
  }

  private effectiveMode(): NavigationMode {
    if (!this.hdsCoreAvailable) {
      return NavigationMode.ARKUI_TABS;
    }
    return this.preferredMode;
  }

  private modeName(mode: NavigationMode): string {
    if (mode === NavigationMode.HDS_TABS) {
      return '普通 HdsTabs';
    }
    if (mode === NavigationMode.FLOATING_HDS) {
      return '悬浮 HdsTabs';
    }
    return 'ArkUI Tabs';
  }

  private tabDescription(index: number): string {
    if (index === 1) {
      return '切换导航容器后,任务状态和操作次数继续保留。';
    }
    if (index === 2) {
      return '回退只改变导航外壳,不复制个人页面的数据。';
    }
    return '用同一组业务页面比较三档导航策略。';
  }

  private pageBottomPadding(): number {
    if (this.effectiveMode() === NavigationMode.FLOATING_HDS) {
      return 132;
    }
    return 28;
  }

  private floatingStyle(): HdsTabsFloatingStyle {
    return {
      barWidth: {
        smallWidth: 228,
        mediumWidth: 300,
        largeWidth: 328
      },
      barBottomMargin: 20,
      gradientMask: {
        maskColor: '#66F4F6FB',
        maskHeight: 112
      }
    };
  }

  private requestMode(mode: NavigationMode): void {
    if (mode !== NavigationMode.ARKUI_TABS &&
      !this.hdsCoreAvailable) {
      return;
    }
    this.preferredMode = mode;
  }

  @Builder
  private modeButton(label: string, mode: NavigationMode) {
    Button(label, { type: ButtonType.Capsule })
      .fontSize(12)
      .fontColor(
        this.preferredMode === mode ? Color.White : '#43506A'
      )
      .backgroundColor(
        this.preferredMode === mode ? '#5065E8' : '#E9EDF5'
      )
      .enabled(
        mode === NavigationMode.ARKUI_TABS ||
          this.hdsCoreAvailable
      )
      .onClick(() => {
        this.requestMode(mode);
      })
  }

  @Builder
  private migrationPanel() {
    Column({ space: 12 }) {
      Column({ space: 4 }) {
        Text('导航迁移演练')
          .fontSize(25)
          .fontWeight(FontWeight.Bold)
          .fontColor('#11182C')
          .width('100%')

        Text('三档导航共用当前 Tab、表单草稿和业务计数')
          .fontSize(13)
          .fontColor('#68708A')
          .width('100%')
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
        this.modeButton('ArkUI Tabs', NavigationMode.ARKUI_TABS)
        this.modeButton('普通 HdsTabs', NavigationMode.HDS_TABS)
        this.modeButton('悬浮 HdsTabs', NavigationMode.FLOATING_HDS)
      }
      .width('100%')

      Column({ space: 8 }) {
        Row() {
          Text('HDS Core SysCap')
            .fontSize(12)
            .fontColor('#68708A')

          Blank()

          Text(this.hdsCoreAvailable ? '可调用' : '不可调用')
            .fontSize(12)
            .fontWeight(FontWeight.Medium)
            .fontColor(
              this.hdsCoreAvailable ? '#2D7A48' : '#B24545'
            )
        }
        .width('100%')

        Row() {
          Text('实际导航')
            .fontSize(12)
            .fontColor('#68708A')

          Blank()

          Text(this.modeName(this.effectiveMode()))
            .fontSize(12)
            .fontWeight(FontWeight.Medium)
            .fontColor('#5065E8')
        }
        .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor(Color.White)
      .borderRadius(16)
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 18 })
    .alignItems(HorizontalAlign.Start)
  }

  @Builder
  private businessPage(index: number) {
    Scroll() {
      Column({ space: 14 }) {
        Column({ space: 5 }) {
          Text(this.tabNames[index])
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#11182C')
            .width('100%')

          Text(this.tabDescription(index))
            .fontSize(13)
            .fontColor('#68708A')
            .lineHeight(19)
            .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)

        Column({ space: 10 }) {
          Text('共享业务草稿')
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#17203A')
            .width('100%')

          TextInput({ text: this.draftText })
            .width('100%')
            .height(44)
            .backgroundColor('#F4F6FB')
            .onChange((value: string) => {
              this.draftText = value;
            })
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(18)
        .alignItems(HorizontalAlign.Start)

        Column({ space: 10 }) {
          Row() {
            Text('业务操作次数')
              .fontSize(13)
              .fontColor('#68708A')

            Blank()

            Text(`${this.actionCount}`)
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#5065E8')
          }
          .width('100%')

          Button('记录一次业务操作', { type: ButtonType.Capsule })
            .width('100%')
            .height(42)
            .fontSize(13)
            .backgroundColor('#5065E8')
            .onClick(() => {
              this.actionCount += 1;
            })
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#E9EDFF')
        .borderRadius(18)

        Column({ space: 8 }) {
          Text('回退观察点')
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#17203A')
            .width('100%')

          Text('切换三档导航后,当前页、草稿和计数都应保持。')
            .fontSize(12)
            .fontColor('#68708A')
            .lineHeight(18)
            .width('100%')

          Text(`当前外壳:${this.modeName(this.effectiveMode())}`)
            .fontSize(12)
            .fontColor('#5065E8')
            .width('100%')
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(18)
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .padding({
        left: 16,
        right: 16,
        top: 14,
        bottom: this.pageBottomPadding()
      })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
    .backgroundColor('#F4F6FB')
  }

  @Builder
  private arkUiNavigation() {
    Tabs({
      barPosition: BarPosition.End,
      index: this.currentIndex,
      controller: this.arkTabsController
    }) {
      ForEach(
        this.tabNames,
        (item: string, index: number) => {
          TabContent() {
            this.businessPage(index)
          }
          .tabBar(
            new BottomTabBarStyle(
              $r('sys.media.ohos_ic_public_clock'),
              item
            )
          )
        },
        (item: string) => item
      )
    }
    .vertical(false)
    .scrollable(true)
    .onChange((index: number) => {
      this.currentIndex = index;
    })
    .width('100%')
    .height('100%')
  }

  @Builder
  private plainHdsNavigation() {
    HdsTabs({
      index: this.currentIndex,
      controller: this.plainHdsController
    }) {
      ForEach(
        this.tabNames,
        (item: string, index: number) => {
          TabContent() {
            this.businessPage(index)
          }
          .tabBar(
            new BottomTabBarStyle(
              $r('sys.media.ohos_ic_public_clock'),
              item
            )
          )
        },
        (item: string) => item
      )
    }
    .barPosition(BarPosition.End)
    .vertical(false)
    .scrollable(true)
    .barOverlap(false)
    .onChange((index: number) => {
      this.currentIndex = index;
    })
    .width('100%')
    .height('100%')
  }

  @Builder
  private floatingHdsNavigation() {
    HdsTabs({
      index: this.currentIndex,
      controller: this.floatingHdsController
    }) {
      ForEach(
        this.tabNames,
        (item: string, index: number) => {
          TabContent() {
            this.businessPage(index)
          }
          .tabBar(
            new BottomTabBarStyle(
              $r('sys.media.ohos_ic_public_clock'),
              item
            )
          )
        },
        (item: string) => item
      )
    }
    .barPosition(BarPosition.End)
    .vertical(false)
    .scrollable(true)
    .barOverlap(true)
    .barFloatingStyle(this.floatingStyle())
    .onChange((index: number) => {
      this.currentIndex = index;
    })
    .width('100%')
    .height('100%')
  }

  build() {
    Column({ space: 10 }) {
      this.migrationPanel()

      Column() {
        if (this.effectiveMode() === NavigationMode.ARKUI_TABS) {
          this.arkUiNavigation()
        } else if (this.effectiveMode() === NavigationMode.HDS_TABS) {
          this.plainHdsNavigation()
        } else {
          this.floatingHdsNavigation()
        }
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F4F6FB')
  }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐