【HarmonyOS 7 沉浸光感深度实战】10 通用组件封装与现有项目接入
前言
沉浸光感放进一个新页面时,接入过程通常比较轻。页面结构简单,组件数量有限,需要材质的区域创建 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 继续使用自己的 MaterialType 和 MaterialLevel。策略层可以提供能力查询和判断依据,但不会把几套组件体系重新包成一种接口。
先确认工程是否已经满足 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;
}
}
页面最终只关心一个布尔状态:
允许使用沉浸材质
或
使用普通背景
DEFAULT、ENABLE、DISABLE 的具体判断继续留在策略层。
这样处理以后,普通背景回退也有了统一的入口。后面应用切换到不使用材质的状态,业务页面不需要分别处理每一张卡片。
HDS 能力可以集中查询,配置入口继续保持独立
已有项目里还可能同时存在 HDS 页面。
这时候 MaterialPolicy 可以顺便集中查询:
hdsMaterial.getSystemMaterialTypes()
把当前环境返回的 HDS 材质类型整理出来。
不过,HDS 的实际材质配置仍然保留在自己的组件体系中。
HdsNavigation、HdsTabs 和 MiniBar 继续使用 MaterialType、MaterialLevel 和 systemMaterialEffect。普通 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 放在后面。
以后遇到视觉异常,属性关系会更容易检查。
材质关闭以后直接进入普通背景分支
materialEnabled 为 false 时,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 接口。
页面结构没有为了统一而统一,真正重复的策略却已经被收到了同一层。


组件结构稳定以后,真正进入已有项目时,还需要控制迁移节奏。
三、现有项目更适合分批迁移
通用组件写好以后,很容易产生一种冲动:既然已经封装完成,不如把项目里能找到的卡片全部替换掉。
我现在更愿意从几个高频位置开始。顶部搜索、底部操作栏、图片工具区域,这些组件面积相对有限,层级关系也比较明确。先让这些区域运行稳定,再决定是否继续处理 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')
}
}
更多推荐




所有评论(0)