前言

沉浸光感放进一个新页面时,接入过程通常比较轻。页面结构简单,组件数量有限,需要材质的区域创建 ImmersiveMaterial,随后通过 systemMaterial 应用到组件,基本就能完成第一轮实验。

已有项目里的情况会复杂许多。搜索框可能已经使用了半年,底部工具栏有自己的背景和阴影,Menu 分散在几个页面中,Sheet 也有现成的主题配置。这个阶段继续在每一个页面分别创建 ImmersiveMaterial,开始时改动不大,随着接入范围扩大,材质配置也会逐渐分散。

同样是搜索区域,有的页面使用 THIN,有的页面改成了 REGULAR;部分组件增加了 colorInvert,另外一些位置又单独设置了 materialColor;设备能力判断、普通背景回退和 HDS 配置也可能慢慢进入各个业务页面。

代码能够运行,后续维护却越来越难。我现在处理这类改造时,会先把材质相关的判断收拢起来。页面继续决定哪些位置需要沉浸光感,业务组件继续负责自己的内容和交互,材质样式、应用状态、HDS 能力查询以及普通背景回退,则交给单独的策略层处理。

普通 ArkUI 容器再增加一层比较轻的 ImmersiveSurface。搜索框、悬浮工具栏这类区域,只需要告诉 Surface 当前属于什么场景,具体使用哪一种材质、材质关闭以后用什么背景,都可以留在统一策略里面。

这样改造以后,后续调整会轻松不少。搜索区域需要更换材质样式,只需要修改策略;某些环境不适合继续使用材质,Surface 可以回到普通背景;业务页面原来的搜索、按钮、状态和事件都不用重新组织。

目前我的测试设备还没有 HarmonyOS 7 实机测试权限,因此这套封装先在模拟器中检查接口、组件结构、回退分支和页面调用。材质细节、自动反色、HDS 能力以及不同设备上的最终表现,仍然需要在具备权限的真机上重新确认。

一、已有页面先梳理接入位置

面对已经运行一段时间的项目,我通常不会先创建通用组件。我更习惯把现有页面过一遍,看看材质真正会出现在哪些地方。

原因在于,沉浸光感并不存在完全统一的接入入口。普通 ArkUI 容器、Popup、Menu、Sheet 和 HDS 框架组件都有自己的配置方式。为了追求表面上的统一,把这些入口全部塞进一个组件,最终得到的往往是一层职责越来越多的封装。

所以,第一步更适合先把页面分开。

现有位置 接入方式 封装方式
搜索框、普通卡片 ImmersiveMaterial + systemMaterial 适合使用 ImmersiveSurface
悬浮工具栏、操作区域 ImmersiveMaterial + systemMaterial 适合使用 ImmersiveSurface
Popup、Menu、Sheet 各自配置中的 systemMaterial 复用 MaterialPolicy
HdsNavigation、HdsTabs、MiniBar systemMaterialEffect 保留 HDS 自己的材质配置
普通内容区域 普通背景 延续原来的页面结构

这样划分以后,ImmersiveSurface 的职责就比较清楚了。它只负责普通 ArkUI 容器。

Menu 和 Sheet 已经有自己的材质入口,只需要从策略层取得材质对象;HDS 继续使用自己的 MaterialTypeMaterialLevel。策略层可以提供能力查询和判断依据,但不会把几套组件体系重新包成一种接口。

先确认工程是否已经满足 API 条件

现有项目开始改页面之前,我会先检查工程环境。我们目前围绕 API 26 的沉浸光感能力展开,因此至少需要确认:

HarmonyOS SDK API 26
↓
targetAPIVersion >= 26.0.0
↓
entry module 中配置 UIMaterial.state

工程本身还没有进入对应 API 版本时,先完成工程升级和基础验证会更加稳妥。否则页面已经改了很多,后面发现目标 API、Module 配置或者材质状态没有准备好,排查范围会明显扩大。这一步完成以后,再开始决定业务场景和材质之间的对应关系。

样式留在策略层

搜索框、工具栏、Menu 和 Sheet 可以先定义成几个稳定场景:

export enum MaterialScene {
  SEARCH = 0,
  TOOLBAR = 1,
  MENU = 2,
  SHEET = 3
}

策略层内部再维护当前项目采用的映射:

SEARCH
→ THIN

TOOLBAR
→ THIN

MENU
→ THICK

SHEET
→ ULTRA_THICK

这组映射属于示例中的项目选择,不需要理解成 API 固定要求。它的作用主要是把技术参数从业务页面里收回来。

搜索页面只需要知道当前位置属于 SEARCH,工具栏只需要知道自己属于 TOOLBAR。以后页面设计发生变化,需要调整某个场景的材质样式,修改策略层就可以覆盖所有使用位置。

业务页面里也不会到处出现:

ImmersiveStyle.THIN
ImmersiveStyle.THICK
ImmersiveStyle.ULTRA_THICK

阅读页面代码时,注意力仍然可以留在业务本身。

MaterialState 也交给策略层判断

材质能否正常使用,还要结合当前应用状态。

这一类判断同样没有必要散落到多个页面里。

策略层可以统一提供:

static canUseArkUiMaterial(): boolean {
  const info: uiMaterial.MaterialInfo =
    uiMaterial.getMaterialInfo();

  switch (info.state) {
    case uiMaterial.MaterialState.DEFAULT:
    case uiMaterial.MaterialState.ENABLE:
      return true;

    case uiMaterial.MaterialState.DISABLE:
      return false;

    default:
      return false;
  }
}

页面最终只关心一个布尔状态:

允许使用沉浸材质
或
使用普通背景

DEFAULTENABLEDISABLE 的具体判断继续留在策略层。

这样处理以后,普通背景回退也有了统一的入口。后面应用切换到不使用材质的状态,业务页面不需要分别处理每一张卡片。

HDS 能力可以集中查询,配置入口继续保持独立

已有项目里还可能同时存在 HDS 页面。

这时候 MaterialPolicy 可以顺便集中查询:

hdsMaterial.getSystemMaterialTypes()

把当前环境返回的 HDS 材质类型整理出来。

不过,HDS 的实际材质配置仍然保留在自己的组件体系中。

HdsNavigationHdsTabs 和 MiniBar 继续使用 MaterialTypeMaterialLevelsystemMaterialEffect。普通 ArkUI 的 ImmersiveSurface 不参与这些配置。

整个关系可以整理成:

业务页面
↓
普通 ArkUI 区域
→ ImmersiveSurface
→ MaterialPolicy
→ ImmersiveMaterial

Menu / Sheet
→ MaterialPolicy
→ 各自 systemMaterial

HDS 页面
→ HDS MaterialType / MaterialLevel

材质不可用
→ 普通背景

我比较喜欢这样的分层,因为每一套组件仍然按照自己的方式工作,公共部分只处理真正重复的材质策略。

策略边界确定以后,下面再处理真正需要复用的普通 ArkUI 容器。

二、ImmersiveSurface 保持轻量

我对这类基础组件的要求比较简单:解决外围材质问题就够了。ImmersiveSurface 不需要理解搜索逻辑,不需要处理按钮事件,也没有必要接管 Menu、Sheet 或页面数据。

它只负责几项外层配置:

scene
materialEnabled
colorInvert
radius
surfaceHeight
fallbackColor
content

这里比最初版本增加了 surfaceHeight。原因来自 ArkTS 组件调用方式。Surface 使用尾随闭包接收业务内容以后,不再在调用结束后继续链式设置 .width().height(),而是把容器高度作为明确参数传入。这样调用关系会更加稳定。

业务内容通过 BuilderParam 传入

ImmersiveSurface 自己不准备默认业务内容。调用页面使用尾随闭包把自己的 Builder 传进来:

@BuilderParam
content: () => void;

Surface 只负责在外面包一层材质或普通背景。

因此,Search 仍然属于页面:

@Builder
private searchContent() {
  Search({
    value: '',
    placeholder: '搜索项目中的内容'
  })
    .width('100%')
    .height(48)
    .backgroundColor(Color.Transparent)
}

工具栏按钮也继续留在页面自己的 Builder 中。通用容器如果开始知道搜索关键词、收藏状态、分享事件或者业务数据,它很快就会从材质组件变成业务组件,后续复用范围反而越来越窄。

高度通过 Surface 参数明确传入

修正后的 Surface 增加:

surfaceHeight: number = 64;

搜索区域使用:

ImmersiveSurface({
  scene: MaterialScene.SEARCH,
  materialEnabled: this.materialEnabled,
  radius: 24,
  surfaceHeight: 64,
  fallbackColor:
    MaterialPolicy.getFallbackColor(
      MaterialScene.SEARCH
    )
}) {
  this.searchContent()
}

工具栏则传入另一组高度:

ImmersiveSurface({
  scene: MaterialScene.TOOLBAR,
  materialEnabled: this.materialEnabled,
  radius: 24,
  surfaceHeight: 72,
  fallbackColor:
    MaterialPolicy.getFallbackColor(
      MaterialScene.TOOLBAR
    )
}) {
  this.toolbarContent()
}

这样调整以后,Surface 仍然没有参与业务布局设计。页面决定当前区域需要多高,再把这个结果传进去。宽度继续由 Surface 占满当前父容器,页面外围的边距、最大宽度和多设备断点仍然由业务页面控制。

材质可用时保持透明背景

当前应用允许使用沉浸材质时,Surface 内部只保留必要的容器属性:

Column() {
  this.content()
}
.width('100%')
.height(this.surfaceHeight)
.backgroundColor(Color.Transparent)
.borderRadius(this.radius)
.clip(true)
.systemMaterial(
  MaterialPolicy.getArkUiMaterial(
    this.scene,
    this.colorInvert
  )
)

业务内容继续显示在容器内部,外围材质由 MaterialPolicy 根据场景提供。这里也继续保留一个习惯:尺寸、背景和圆角等普通属性先确定,systemMaterial 放在后面。

以后遇到视觉异常,属性关系会更容易检查。

材质关闭以后直接进入普通背景分支

materialEnabledfalse 时,Surface 使用普通背景:

Column() {
  this.content()
}
.width('100%')
.height(this.surfaceHeight)
.backgroundColor(this.fallbackColor)
.borderRadius(this.radius)
.clip(true)

业务内容完全不需要变化。

搜索框仍然是原来的 Search,工具栏按钮仍然保留原来的事件,页面只是少了一层沉浸材质。

这也是我觉得通用 Surface 最有价值的地方。已有项目接入新视觉能力时,回退方案能够和正常方案共用同一套业务组件,后面处理兼容性和设备差异会轻松很多。

fallbackColor 最终应该回到项目主题资源

示例里的:

'#F2FFFFFF'

只是为了让 Demo 可以独立运行。

真正接入项目以后,我更建议页面传入自己的主题资源:

fallbackColor:
  $r('app.color.search_surface_background')

随后在浅色和深色资源目录中分别维护同名资源。这样,材质与应用主题之间也不会混在一起。

可以把职责理解成:

沉浸材质可用
→ systemMaterial

沉浸材质关闭
→ fallbackColor

浅色 / 深色主题
→ 应用颜色资源

材质负责材质,主题负责主题。这两层分开以后,后续关闭沉浸光感时,页面仍然能够自然回到项目原有的颜色体系。

colorInvert 继续由调用页面决定

Surface 会保留:

colorInvert: boolean = false;

默认状态仍然关闭。

原因来自实际业务内容。自动反色是否适合某个区域,与内部使用的 Text、Search、SymbolGlyph、Image 以及对应颜色资源都有关系。通用 Surface 只能知道外层是什么场景,无法替内部所有前景内容完成条件判断。

因此,真正需要复杂背景适配的搜索框可以明确传入:

colorInvert: true

普通工具栏没有这个需求时继续保持默认值。这种处理能让通用组件保持简单,也避免所有材质区域统一打开自动反色。

Menu 和 Sheet 复用策略层

普通 ArkUI 容器通过 Surface 统一以后,Menu 和 Sheet 依然保持自己的调用方式。

Menu 可以直接取得策略层材质:

Button('打开 Menu')
  .bindMenu(
    this.menuBuilder,
    {
      systemMaterial:
        this.menuMaterial
    }
  )

这里的:

this.menuMaterial

在页面初始化时已经通过下面的方式取得。

MaterialPolicy.getArkUiMaterial(
  MaterialScene.MENU
)

Sheet 也是相同思路。它继续使用自己的 systemMaterial,普通背景回退则根据 materialEnabled 决定:

backgroundColor:
  this.materialEnabled
    ? Color.Transparent
    : MaterialPolicy.getFallbackColor(
        MaterialScene.SHEET
      )

这时候三类组件虽然接入形式不同,材质策略仍然来自同一个地方。

  • 搜索框和工具栏使用 Surface。

  • Menu 和 Sheet 使用自己的材质入口。

  • HDS 继续使用 HDS 接口。

页面结构没有为了统一而统一,真正重复的策略却已经被收到了同一层。

Screenshot_2026-08-15T104955

Screenshot_2026-08-15T105015

组件结构稳定以后,真正进入已有项目时,还需要控制迁移节奏。

三、现有项目更适合分批迁移

通用组件写好以后,很容易产生一种冲动:既然已经封装完成,不如把项目里能找到的卡片全部替换掉。

我现在更愿意从几个高频位置开始。顶部搜索、底部操作栏、图片工具区域,这些组件面积相对有限,层级关系也比较明确。先让这些区域运行稳定,再决定是否继续处理 Menu、Sheet 和其他页面。这样做的好处是,出现问题以后更容易判断它来自策略、Surface 还是某一个业务页面。

工程环境先通过基础检查

真正开始迁移以前,可以先确认:

检查项目 当前建议
HarmonyOS SDK API 26
targetAPIVersion 不低于 26.0.0
Module 类型 entry
UIMaterial metadata 确认当前状态
测试环境 区分模拟器与真机

这些条件属于整个项目的基础。

基础环境没有确认以前,不适合同时修改大量业务页面。

普通 ArkUI、系统浮层和 HDS 保持各自入口

迁移过程中,我会一直保留这三个方向:

普通容器
→ ImmersiveSurface

Menu / Sheet 等系统浮层
→ 自己的 systemMaterial

HDS Navigation / Tabs / MiniBar
→ systemMaterialEffect

MaterialPolicy 可以被它们共同使用,但不会把三套能力硬塞进一个组件。这种边界能够让后面的升级更加容易。某套 API 发生变化时,修改范围也更加集中。

策略数量不要提前扩得太多

当前示例只有:

SEARCH
TOOLBAR
MENU
SHEET

四种场景。

对一个刚开始接入的项目来说,已经够用。

以后真的出现稳定的新场景,例如播放器控制区或者固定顶部操作区,再增加对应策略。没有必要刚开始就创建十几种预设,把每一种微小差异都变成一个枚举。

策略数量少,后面更容易回答一个很重要的问题:为什么这里使用这种材质?

自动反色继续由具体页面验证

Surface 已经支持 colorInvert,迁移时仍然不能统一打开。

真正使用之前,需要继续检查:

材质厚度
↓
前景组件属性
↓
颜色资源
↓
设备能力
↓
实际复杂背景

这些条件都和具体页面有关。通用策略负责提供能力,业务页面负责决定是否使用,这种分工会更加稳妥。

普通背景回退要真正跑一遍

我觉得这一项非常重要。页面接入完成以后,可以专门测试一次材质关闭状态。

调整应用级材质状态并重新构建安装以后,逐项检查:

  • 搜索框是否回到普通背景
  • 工具栏文字和按钮是否仍然清楚
  • Menu 是否还能正常打开
  • Sheet 是否仍然具有可读背景
  • 页面布局是否发生明显变化
  • 原来的业务事件是否继续执行

通用组件在材质开启时表现正常,只完成了一半。材质关闭以后,页面结构仍然稳定,业务操作仍然完整,回退策略才真正具有价值。

长列表不要因为有 Surface 就全部套一遍

通用组件存在以后,还有一个比较容易出现的问题:哪里都可以套,所以哪里都开始套。

长列表尤其需要克制。

例如:

顶部搜索
→ ImmersiveSurface

悬浮筛选
→ ImmersiveSurface

普通 ListItem
→ 原有背景

底部操作栏
→ ImmersiveSurface

这种结构已经能够建立材质层级。

普通内容项继续保持原来的背景,可以减少重复材质节点,也能让悬浮区域更加容易辨认。列表自身的数据加载、组件复用和性能优化,则继续按照原来的 ArkUI 长列表方案处理。

多设备布局继续由业务页面控制

ImmersiveSurface 增加了 surfaceHeight,并不代表它应该开始负责响应式布局。

手机上的搜索区域可能占据整行,平板上可能限制最大宽度,折叠屏展开以后也可能重新安排页面结构。这些仍然属于业务页面。

Surface 只保留:

材质场景
材质状态
圆角
高度
回退背景
业务内容

至于:

最大宽度
左右边距
断点
单双栏
横竖屏布局

继续放在页面这一层处理。我希望这个组件以后能够出现在不同页面里,所以不会把某一种设备布局规则提前写进它内部。

HDS 查询主要用于诊断和设备策略

MaterialPolicy 里保留 HDS 能力查询以后,页面可以显示当前环境返回的材质类型,也可以把结果用于测试记录。

不过,普通 ArkUI Surface 不会因为 HDS 查询结果改变自己的工作方式。

HDS 能力查询更多服务于:

确认当前测试环境
记录设备结果
排查 HDS 材质差异
决定 HDS 是否需要更保守的等级

普通组件继续使用自己的 ArkUI 材质策略。这样能够避免两套体系互相牵连。

最后再回到真机和不同窗口形态

模拟器可以帮助我们确认:

MaterialPolicy 是否能够正常工作
ImmersiveSurface 是否能够编译运行
BuilderParam 是否能够正常传递业务内容
MaterialState 回退分支是否正确
Menu 和 Sheet 是否可以继续使用自己的材质

到了正式项目阶段,还需要在目标设备上继续检查:

验证内容 主要观察内容
材质细节 通透度、阴影、背景层次
自动反色 复杂背景中的前景可读性
交互反馈 按压和光感
长列表 滚动稳定性
动态页面 动画手感和帧率
HDS 材质类型与等级
手机 小窗口布局
折叠屏和平板 宽窗口布局
回退状态 普通背景是否完整可用

当前这套三文件结构只能完成模拟器阶段的第一轮验证。真机权限具备以后,还需要重新覆盖材质效果、设备差异和性能结果。

总结

已有项目接入沉浸光感以后,我越来越倾向于把通用封装控制得小一些。

MaterialPolicy 管材质场景、MaterialState、HDS 能力查询和普通背景策略。

ImmersiveSurface 管普通 ArkUI 容器的材质显示和回退。

业务页面继续管理 Search、工具栏、Menu、Sheet 以及原来的业务状态。

这样的结构不会为了沉浸光感重新组织整个项目。修正后的 ImmersiveSurface 还增加了 surfaceHeight,业务内容通过必传的 @BuilderParam 尾随闭包传入。这样既避开了 ArkTS 中不合适的链式组件调用,也让 Surface 的尺寸职责更加明确。

我也会继续控制材质策略的数量。

搜索框、工具栏、Menu 和 Sheet 已经覆盖了一批常见场景。后续出现真正稳定的新需求,再往策略层增加新的场景。材质对象保持提前创建和复用,避免进入滚动、动画和业务事件中反复生成。

自动反色、主题、多设备和 HDS 继续保留各自的边界。自动反色由具体页面判断,浅色和深色背景交给应用资源,多设备断点留在业务布局,HDS 继续使用自己的材质配置。

普通背景回退也应该一直保留。对已有项目来说,我觉得这层保障很重要。材质关闭以后,搜索、工具栏、Menu 和 Sheet 仍然能够正常工作,页面布局也没有受到影响,这套封装才真正适合进入长期维护。

当前示例可以先在 HarmonyOS 7 模拟器中检查策略、Surface、页面调用和回退分支。正式进入项目以后,仍然需要在具备权限的真机上复核材质、自动反色、HDS、长列表性能以及不同窗口形态下的实际页面表现。

完整代码

MaterialPolicy.ets

/**
 * HarmonyOS 7 沉浸光感通用策略
 *
 * 负责:
 * 1. 普通 ArkUI 材质场景映射。
 * 2. MaterialState 判断。
 * 3. HDS 材质能力查询。
 * 4. 普通背景回退颜色。
 */
import { uiMaterial } from '@kit.ArkUI';
import { hdsMaterial } from '@kit.UIDesignKit';
import { BusinessError } from '@kit.BasicServicesKit';

export enum MaterialScene {
  SEARCH = 0,
  TOOLBAR = 1,
  MENU = 2,
  SHEET = 3
}

export interface HdsMaterialCapability {
  typesText: string;
  supportsImmersive: boolean;
  queryFailed: boolean;
}

export class MaterialPolicy {
  /**
   * 固定材质对象提前创建。
   * 页面滚动和动画期间直接复用。
   */
  private static readonly searchMaterial:
    uiMaterial.Material =
    new uiMaterial.ImmersiveMaterial({
      style: uiMaterial.ImmersiveStyle.THIN
    });

  private static readonly searchInvertMaterial:
    uiMaterial.Material =
    new uiMaterial.ImmersiveMaterial({
      style: uiMaterial.ImmersiveStyle.THIN,
      colorInvert: true
    });

  private static readonly toolbarMaterial:
    uiMaterial.Material =
    new uiMaterial.ImmersiveMaterial({
      style: uiMaterial.ImmersiveStyle.THIN
    });

  private static readonly toolbarInvertMaterial:
    uiMaterial.Material =
    new uiMaterial.ImmersiveMaterial({
      style: uiMaterial.ImmersiveStyle.THIN,
      colorInvert: true
    });

  private static readonly menuMaterial:
    uiMaterial.Material =
    new uiMaterial.ImmersiveMaterial({
      style: uiMaterial.ImmersiveStyle.THICK
    });

  private static readonly sheetMaterial:
    uiMaterial.Material =
    new uiMaterial.ImmersiveMaterial({
      style: uiMaterial.ImmersiveStyle.ULTRA_THICK
    });

  /**
   * 判断当前应用状态是否允许普通组件主动使用材质。
   */
  static canUseArkUiMaterial(): boolean {
    const info: uiMaterial.MaterialInfo =
      uiMaterial.getMaterialInfo();

    switch (info.state) {
      case uiMaterial.MaterialState.DEFAULT:
      case uiMaterial.MaterialState.ENABLE:
        return true;

      case uiMaterial.MaterialState.DISABLE:
        return false;

      default:
        return false;
    }
  }

  /**
   * 页面显示当前 MaterialState。
   */
  static getArkUiStateText(): string {
    const info: uiMaterial.MaterialInfo =
      uiMaterial.getMaterialInfo();

    switch (info.state) {
      case uiMaterial.MaterialState.DEFAULT:
        return 'DEFAULT';

      case uiMaterial.MaterialState.ENABLE:
        return 'ENABLE';

      case uiMaterial.MaterialState.DISABLE:
        return 'DISABLE';

      default:
        return `UNKNOWN(${info.state})`;
    }
  }

  /**
   * 根据业务场景返回已经创建好的材质对象。
   *
   * 静态方法中直接使用类名访问静态成员,
   * 避免 standalone-this 检查错误。
   */
  static getArkUiMaterial(
    scene: MaterialScene,
    colorInvert: boolean = false
  ): uiMaterial.Material {
    switch (scene) {
      case MaterialScene.SEARCH:
        return colorInvert
          ? MaterialPolicy.searchInvertMaterial
          : MaterialPolicy.searchMaterial;

      case MaterialScene.TOOLBAR:
        return colorInvert
          ? MaterialPolicy.toolbarInvertMaterial
          : MaterialPolicy.toolbarMaterial;

      case MaterialScene.MENU:
        return MaterialPolicy.menuMaterial;

      case MaterialScene.SHEET:
        return MaterialPolicy.sheetMaterial;

      default:
        return MaterialPolicy.searchMaterial;
    }
  }

  /**
   * Demo 使用的普通背景回退颜色。
   *
   * 正式项目可以替换成 base / dark 主题资源。
   */
  static getFallbackColor(
    scene: MaterialScene
  ): ResourceColor {
    switch (scene) {
      case MaterialScene.SEARCH:
        return '#F2FFFFFF';

      case MaterialScene.TOOLBAR:
        return '#EEFFFFFF';

      case MaterialScene.MENU:
        return '#F7FFFFFF';

      case MaterialScene.SHEET:
        return '#F7FFFFFF';

      default:
        return '#F2FFFFFF';
    }
  }

  /**
   * 查询当前环境支持的 HDS 材质类型。
   */
  static queryHdsCapability():
    HdsMaterialCapability {
    try {
      const types:
        Array<hdsMaterial.MaterialType> =
        hdsMaterial.getSystemMaterialTypes();

      if (types.length === 0) {
        return {
          typesText: '当前环境未返回 HDS 材质类型',
          supportsImmersive: false,
          queryFailed: false
        };
      }

      const names: Array<string> = [];
      let supportsImmersive: boolean = false;

      for (
        let index: number = 0;
        index < types.length;
        index++
      ) {
        const type: hdsMaterial.MaterialType =
          types[index];

        switch (type) {
          case hdsMaterial.MaterialType.NONE:
            names.push('NONE');
            break;

          case hdsMaterial.MaterialType.ADAPTIVE:
            names.push('ADAPTIVE');
            break;

          case hdsMaterial.MaterialType.IMMERSIVE:
            names.push('IMMERSIVE');
            supportsImmersive = true;
            break;

          default:
            names.push(`UNKNOWN(${type})`);
            break;
        }
      }

      return {
        typesText: names.join('、'),
        supportsImmersive: supportsImmersive,
        queryFailed: false
      };
    } catch (error) {
      const businessError =
        error as BusinessError;

      return {
        typesText:
        `查询失败 ${businessError.code} `
          + `${businessError.message}`,
        supportsImmersive: false,
        queryFailed: true
      };
    }
  }
}

ImmersiveSurface.ets

/**
 * HarmonyOS 7 通用沉浸材质容器
 *
 * 负责:
 * 1. 普通 ArkUI 区域的 systemMaterial 接入。
 * 2. MaterialState 关闭后的普通背景回退。
 * 3. 圆角和容器高度。
 * 4. 外部业务内容插入。
 */

import {
  MaterialPolicy,
  MaterialScene
} from './MaterialPolicy';

@Component
export struct ImmersiveSurface {
  /**
   * 当前业务场景。
   */
  scene: MaterialScene =
    MaterialScene.SEARCH;

  /**
   * 当前是否允许显示沉浸材质。
   */
  materialEnabled: boolean = true;

  /**
   * 自动反色默认关闭。
   */
  colorInvert: boolean = false;

  /**
   * Surface 圆角。
   */
  radius: number = 24;

  /**
   * 高度由调用页面传入。
   *
   * Surface 自身始终占满父容器宽度,
   * 避免在尾随闭包外继续调用 width / height。
   */
  surfaceHeight: number = 64;

  /**
   * 材质关闭以后使用的普通背景。
   */
  fallbackColor: ResourceColor =
    '#F2FFFFFF';

  /**
   * 外部业务内容。
   *
   * 当前组件通过尾随闭包初始化,
   * 因此不再设置引用 this 的默认 Builder。
   */
  @BuilderParam
  content: () => void;

  build() {
    if (this.materialEnabled) {
      Column() {
        this.content()
      }
      .width('100%')
      .height(this.surfaceHeight)
      .backgroundColor(Color.Transparent)
      .borderRadius(this.radius)
      .clip(true)
      .systemMaterial(
        MaterialPolicy.getArkUiMaterial(
          this.scene,
          this.colorInvert
        )
      )
    } else {
      Column() {
        this.content()
      }
      .width('100%')
      .height(this.surfaceHeight)
      .backgroundColor(this.fallbackColor)
      .borderRadius(this.radius)
      .clip(true)
    }
  }
}

Main.ets

/**
 * HarmonyOS 7 沉浸光感深度实战 10
 *
 * 验证环境:
 * HarmonyOS SDK API 26
 * HarmonyOS 7 模拟器
 */

import {
  MaterialPolicy,
  MaterialScene,
  HdsMaterialCapability
} from './MaterialPolicy';

import {
  ImmersiveSurface
} from './ImmersiveSurface';

@Entry
@Component
struct Main {
  @State private materialEnabled:
    boolean = false;

  @State private materialStateText:
    string = '尚未读取';

  @State private hdsTypesText:
    string = '尚未查询';

  @State private hdsSupportText:
    string = '未确认';

  @State private showSheet:
    boolean = false;

  @State private lastActionText:
    string = '尚未执行操作';

  /**
   * Menu 和 Sheet 自己已经提供 systemMaterial,
   * 因此只从策略层取得对应材质。
   */
  private readonly menuMaterial =
    MaterialPolicy.getArkUiMaterial(
      MaterialScene.MENU
    );

  private readonly sheetMaterial =
    MaterialPolicy.getArkUiMaterial(
      MaterialScene.SHEET
    );

  aboutToAppear(): void {
    this.loadMaterialEnvironment();
  }

  /**
   * 读取当前应用状态和 HDS 材质能力。
   */
  private loadMaterialEnvironment(): void {
    this.materialEnabled =
      MaterialPolicy.canUseArkUiMaterial();

    this.materialStateText =
      MaterialPolicy.getArkUiStateText();

    const hdsCapability:
      HdsMaterialCapability =
      MaterialPolicy.queryHdsCapability();

    this.hdsTypesText =
      hdsCapability.typesText;

    if (hdsCapability.queryFailed) {
      this.hdsSupportText =
        '查询失败';
    } else if (
      hdsCapability.supportsImmersive
    ) {
      this.hdsSupportText =
        '支持 IMMERSIVE';
    } else {
      this.hdsSupportText =
        '未确认 IMMERSIVE';
    }
  }

  @Builder
  private sectionTitle(
    title: string,
    description: string
  ) {
    Column({ space: 4 }) {
      Text(title)
        .fontSize(21)
        .fontWeight(FontWeight.Bold)
        .fontColor('#11182C')
        .width('100%')

      Text(description)
        .fontSize(13)
        .fontColor('#68708A')
        .lineHeight(20)
        .width('100%')
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
  }

  /**
   * 当前运行环境。
   */
  @Builder
  private environmentPanel() {
    Column({ space: 10 }) {
      Row({ space: 12 }) {
        Text('MaterialState')
          .width('40%')
          .fontSize(13)
          .fontColor('#68708A')

        Text(this.materialStateText)
          .layoutWeight(1)
          .fontSize(13)
          .fontWeight(FontWeight.Medium)
          .fontColor(
            this.materialEnabled
              ? '#1A8F5D'
              : '#D06C35'
          )
          .textAlign(TextAlign.End)
      }
      .width('100%')

      Divider()
        .color('#E8EBF2')

      Row({ space: 12 }) {
        Text('HDS MaterialType')
          .width('40%')
          .fontSize(13)
          .fontColor('#68708A')

        Text(this.hdsTypesText)
          .layoutWeight(1)
          .fontSize(13)
          .fontWeight(FontWeight.Medium)
          .fontColor('#17203A')
          .textAlign(TextAlign.End)
          .maxLines(3)
      }
      .width('100%')

      Divider()
        .color('#E8EBF2')

      Row({ space: 12 }) {
        Text('HDS 沉浸材质')
          .width('40%')
          .fontSize(13)
          .fontColor('#68708A')

        Text(this.hdsSupportText)
          .layoutWeight(1)
          .fontSize(13)
          .fontWeight(FontWeight.Medium)
          .fontColor('#5065E8')
          .textAlign(TextAlign.End)
          .maxLines(2)
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(20)
  }

  /**
   * 搜索框业务内容。
   *
   * Surface 负责外围材质,
   * Search 自己继续负责搜索组件。
   */
  @Builder
  private searchContent() {
    Search({
      value: '',
      placeholder: '搜索项目中的内容'
    })
      .width('100%')
      .height(48)
      .backgroundColor(Color.Transparent)
  }

  /**
   * 工具栏业务内容。
   */
  @Builder
  private toolbarContent() {
    Row({ space: 10 }) {
      Button('收藏')
        .layoutWeight(1)
        .height(38)
        .fontSize(12)
        .fontColor('#5065E8')
        .backgroundColor('#EEF1FF')
        .onClick(() => {
          this.lastActionText =
            '已执行收藏';
        })

      Button('分享')
        .layoutWeight(1)
        .height(38)
        .fontSize(12)
        .fontColor('#5065E8')
        .backgroundColor('#EEF1FF')
        .onClick(() => {
          this.lastActionText =
            '已执行分享';
        })

      Button('稍后')
        .layoutWeight(1)
        .height(38)
        .fontSize(12)
        .fontColor('#5065E8')
        .backgroundColor('#EEF1FF')
        .onClick(() => {
          this.lastActionText =
            '已加入稍后处理';
        })
    }
    .width('100%')
    .height('100%')
    .padding({
      left: 12,
      right: 12
    })
    .alignItems(VerticalAlign.Center)
  }

  /**
   * Menu 内容。
   */
  @Builder
  private menuBuilder() {
    Menu() {
      MenuItem({
        content: '复制链接'
      })
        .onClick(() => {
          this.lastActionText =
            '已选择复制链接';
        })

      MenuItem({
        content: '加入收藏'
      })
        .onClick(() => {
          this.lastActionText =
            '已选择加入收藏';
        })

      MenuItem({
        content: '稍后处理'
      })
        .onClick(() => {
          this.lastActionText =
            '已选择稍后处理';
        })
    }
  }

  /**
   * Sheet 内容。
   */
  @Builder
  private sheetBuilder() {
    Column({ space: 16 }) {
      Text('现有项目设置')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#17203A')
        .width('100%')

      Text(
        'Sheet 继续保留自己的业务内容,'
          + '材质统一从 MaterialPolicy 获取。'
      )
        .fontSize(14)
        .fontColor('#68708A')
        .lineHeight(21)
        .width('100%')

      Column({ space: 12 }) {
        Row({ space: 10 }) {
          Text('接收更新提醒')
            .layoutWeight(1)
            .fontSize(14)
            .fontColor('#17203A')

          Toggle({
            type: ToggleType.Switch,
            isOn: true
          })
        }
        .width('100%')

        Divider()
          .color('#E8EBF2')

        Row({ space: 10 }) {
          Text('保留当前筛选')
            .layoutWeight(1)
            .fontSize(14)
            .fontColor('#17203A')

          Toggle({
            type: ToggleType.Switch,
            isOn: false
          })
        }
        .width('100%')
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#66FFFFFF')
      .borderRadius(18)

      Button('完成')
        .width('100%')
        .height(42)
        .onClick(() => {
          this.lastActionText =
            'Sheet 操作完成';

          this.showSheet =
            false;
        })
    }
    .width('100%')
    .height('100%')
    .padding({
      left: 24,
      right: 24,
      top: 18,
      bottom: 24
    })
    .alignItems(HorizontalAlign.Start)
  }

  /**
   * 普通业务内容继续使用普通背景。
   */
  @Builder
  private normalContentCard(
    index: number
  ) {
    Column({ space: 6 }) {
      Text(`普通内容区域 ${index}`)
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor('#17203A')
        .width('100%')

      Text(
        '普通内容继续使用原页面样式,'
          + '无需全部改成沉浸材质。'
      )
        .fontSize(13)
        .fontColor('#68708A')
        .lineHeight(20)
        .width('100%')
    }
    .width('100%')
    .height(104)
    .padding(16)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Start)
    .backgroundColor(Color.White)
    .borderRadius(20)
  }

  build() {
    Scroll() {
      Column({ space: 18 }) {
        Column({ space: 6 }) {
          Text('HarmonyOS 7 沉浸光感')
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#11182C')
            .width('100%')

          Text('通用组件与现有项目接入')
            .fontSize(16)
            .fontColor('#68708A')
            .width('100%')
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')

        this.sectionTitle(
          '当前环境',
          '策略层统一读取应用状态和 HDS 材质能力。'
        )

        this.environmentPanel()

        this.sectionTitle(
          '搜索区域',
          'Search 保持原业务结构,Surface 负责外围材质。'
        )

        ImmersiveSurface({
          scene: MaterialScene.SEARCH,
          materialEnabled: this.materialEnabled,
          radius: 24,
          surfaceHeight: 64,
          fallbackColor:
          MaterialPolicy.getFallbackColor(
            MaterialScene.SEARCH
          )
        }) {
          this.searchContent()
        }

        this.sectionTitle(
          '悬浮工具栏',
          '按钮和业务事件继续保留在工具栏内部。'
        )

        ImmersiveSurface({
          scene: MaterialScene.TOOLBAR,
          materialEnabled: this.materialEnabled,
          radius: 24,
          surfaceHeight: 72,
          fallbackColor:
          MaterialPolicy.getFallbackColor(
            MaterialScene.TOOLBAR
          )
        }) {
          this.toolbarContent()
        }

        this.sectionTitle(
          '普通内容',
          '页面主体没有必要全部使用沉浸材质。'
        )

        this.normalContentCard(1)
        this.normalContentCard(2)

        this.sectionTitle(
          '系统浮层',
          'Menu 和 Sheet 继续使用各自的材质入口。'
        )

        Row({ space: 12 }) {
          Button('打开 Menu')
            .layoutWeight(1)
            .height(42)
            .bindMenu(
              this.menuBuilder,
              {
                systemMaterial:
                this.menuMaterial
              }
            )

          Button(
            this.showSheet
              ? 'Sheet 已打开'
              : '打开 Sheet'
          )
            .layoutWeight(1)
            .height(42)
            .onClick(() => {
              this.showSheet = true;

              this.lastActionText =
                'Sheet 已打开';
            })
            .bindSheet(
              $$this.showSheet,
              this.sheetBuilder(),
              {
                height: 360,
                dragBar: true,

                backgroundColor:
                this.materialEnabled
                  ? Color.Transparent
                  : MaterialPolicy
                  .getFallbackColor(
                    MaterialScene.SHEET
                  ),

                systemMaterial:
                this.sheetMaterial
              }
            )
        }
        .width('100%')

        Column({ space: 6 }) {
          Text('最近一次操作')
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#17203A')
            .width('100%')

          Text(this.lastActionText)
            .fontSize(13)
            .fontColor('#68708A')
            .lineHeight(20)
            .width('100%')
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(20)

        Text(
          '模拟器用于确认策略、组件和回退分支,'
            + '最终材质与设备差异仍需真机验证。'
        )
          .fontSize(12)
          .fontColor('#747C92')
          .lineHeight(19)
          .padding({
            top: 4,
            bottom: 24
          })
          .width('100%')
      }
      .width('100%')
      .padding({
        left: 20,
        right: 20,
        top: 24,
        bottom: 24
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F4F6FB')
  }
}
Logo

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

更多推荐