开源鸿蒙平台 CMP 三方库「haze」适配全流程
本文记录
haze接入 OpenHarmony 的完整过程,覆盖 CMP 视觉效果库盘点、ohosArm64/ohosX64双目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建、签名和真机验收。
haze是 Compose Multiplatform 生态的毛玻璃效果库,对外 API 是Modifier.hazeSource()/Modifier.hazeEffect(...),全部实现在 ComposeModifier.Node+GraphicsLayer体系里。本次适配把它的参数模型内核(HazeBlurDefaults默认值、HazeMaterials五档材质表、HazeProgressive渐进渐变、EaseIn 缓动、亮度判定、ResolvedHazeBlurStyle合成顺序)复刻进 OpenHarmony ARM64/x64 运行时,配一条 Kotlin/Native 软件模糊管线,ArkUI 页面作为真机验收宿主——鸿蒙侧解析出的材质 alpha、渐进强度与 Android/iOS/Desktop 完全同源。
项目地址: AtomGit/oh-tpc/haze
上游仓库: chrisbanes/haze
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
haze 为 Compose 应用提供毛玻璃 / 渐进模糊 / 材质着色能力:开发者用 hazeSource 标记内容层、用 hazeBlur(以及 Haze 2.x 的 haze-glass 效果工厂)标记效果层,即可获得背景模糊、线性/径向渐进模糊、SwiftUI 风格材质预设和噪点质感。KMP 项目把这些效果的参数语义放在 commonMain,由 Android、iOS、Desktop 等宿主共享渲染。OpenHarmony 应用同样需要这套能力,但默认构建不会生成 OpenHarmony 的 Kotlin/Native 变体。
如果只把示例页面改成 ArkTS 调系统 backgroundBlurStyle,页面能糊,却无法证明公共 Kotlin 实现已经进入 OpenHarmony ARM64 设备完成编译、链接和执行,也无法保证跨端材质参数一致。适配需要同时验证 KMP 模型内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB 与动态库。 |
| 工具链不一致 | 上游 Compose 1.12.1 / Kotlin 2.4.21 / AGP 9 / Gradle 9.8.1;社区 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21-1.0.0。两条插件线不能共存于同一个 Gradle 构建。 |
| 源码面 100% Compose | 通读 haze / haze-blur / haze-materials 的 commonMain:完全无 Compose 依赖的源文件只有 4 个(约 128 行),其余全部锚定 androidx.compose.ui。 |
| 渲染原语不同 | 上游模糊走平台 RenderEffect / Skia;Kotlin/Native 共享库里没有 Skia,也没有 RenderEffect。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在 KMP 目标配置、参数模型内核和 C ABI/N-API 桥接层。效果语义仍由公共 Kotlin 实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
真机 Demo 提供三个可观察场景(Tab):
- 毛玻璃材质:
HazeMaterials五档预设(ultraThin / thin / regular / thick / ultraThick)× 明暗容器切换,默认半径 24dp 或手动 0–80dp,noiseFactor 0–1; - 渐进模糊:
HazeProgressive纵向线性 / 横向线性 / 径向三种模式,双端强度(start/end 或 center/radius)实时可调,EaseIn 缓动; - 自定义参数:blurEnabled 开关、整体 alpha、纯色 tint(
HazeColorEffect.tint语义)、噪点、蒙版形状(圆角矩形 / 圆形)与面板位置尺寸。
Native 侧返回的 RenderResult 包含:
| 字段 | 作用 |
|---|---|
ok / error | 调用结果;Kotlin 异常永不跨语言边界,统一转 JSON 错误。 |
width / height / bytes | 画布尺寸与 PNG 字节数,供状态行对账。 |
blurRadius / material / progressive | 解析后的生效参数(材质 → 24dp、默认 → 20dp 等规则可验证)。 |
tint / tintAlpha | 生效 tint 颜色与解析后浮点 alpha(材质按容器亮度选 light/dark 档)。 |
luminance | 容器色 W3C 相对亮度,验证选档依据。 |
pngBase64 | 完整渲染帧(内容 + 玻璃面板)PNG。 |
共享层还提供三类交叉验证:模型钉值(默认值、材质十档 alpha、亮度、缓动)、管线性质(常数图不变、脉冲对称、PNG 结构)、协议回显(alpha=0 时输出与原始场景逐字节一致)。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | Haze 2.x 参数模型以纯 Kotlin 复刻进 ohosArm64/ohosX64,每个值注明上游出处并由 JVM 测试钉住。 |
| 平台目标 | example/nativeApp 独立消费工程启用 ohosArm64 + ohosX64,生成双 ABI 动态库。 |
| 桥接稳定 | 两个 C ABI 符号 + 扁平 UTF-8 JSON,不传递 Kotlin 对象地址,返回字符串由 N-API 侧即刻释放。 |
| CMP 兼容 | 上游 haze 模块源码零改动;社区 CMP 工具链对齐后可直接原生编 ohos,本内核退役为对照实现。 |
| UI 完整 | 三 Tab 覆盖桥全部入口字段,每个字段至少一个可见控件与状态行回显。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。haze 的
hazeSource/hazeBlur修饰符仍在仓库commonMain,继续由 Android/iOS/Desktop/Web 消费;鸿蒙 Demo 验证的是同一份参数模型内核的 OpenHarmony Native 链路。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点源码集、效果框架边界和 OpenHarmony 交付物
第 2 阶段:目标与依赖打通 ── example/ 独立 Gradle 工程、ohosArm64/ohosX64、双 ABI so
第 3 阶段:参数模型建模 ── HazeModel 复刻 HazeBlurDefaults/Materials/Progressive + 钉值测试
第 4 阶段:原生桥接 ── C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 三 Tab 玻璃工作台、状态行回显、错误边界
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
haze/ haze-blur/ haze-glass/ haze-materials/ 上游库模块,源码零改动
example/settings.gradle.kts 独立 OHOS 构建 rootProject "ohos_haze"
example/gradle/ + gradlew Gradle 8.14.1 独立 wrapper
example/nativeApp/src/commonMain/ HazeModel / BlurKernel / SceneRenderer / HazeEngine / PngEncoder / MiniJson
example/nativeApp/src/ohosMain/ Bridge.kt(@CName C ABI)
example/nativeApp/src/jvmTest/ 模型钉值 + 管线 + 协议共 23 项测试
example/harmonyApp/ DevEco Stage:CMake NAPI + ArkTS 三 Tab
example 是独立 Gradle 工程:上游根构建(build-logic 复合构建、发布流、截图/基准测试)一字不动,Gradle 与 Hvigor 分开执行,互不影响。这是 miuix 先例确立、本仓沿用的边界——上游构建复杂时,替换式改写会砸坏整条流水线。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | OpenHarmony 构建使用 HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| Gradle | 8.14.1(example/ 独立 wrapper;上游根构建 9.8.1 保留) |
| JDK | 21(Temurin;org.gradle.java.home 写入 example/gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64 |
| DevEco product | 5.1.0(18),runtimeOS: HarmonyOS |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.haze |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 页面边界
标题区 haze · OpenHarmony / 版本握手行(桥 2.0.1-ohos.1 · 上游 2.0.1 · Kotlin 2.2.21-1.0.0)
Tab 区 毛玻璃材质 / 渐进模糊 / 自定义参数
场景区 湖景 / 卡片 / 渐变(程序化内容,替代 hazeSource 捕获的真实 UI)
参数区 材质五档、明暗容器、半径/噪点/强度滑条、蒙版形状与位置
结果区 Image(PixelMap) 全宽展示 + 状态行(尺寸 / png 字节 / tintα)
页面启动时先做 hazeVersion() 版本握手并展示,再自动渲染默认帧:湖景 + regular 材质 + 浅色容器。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 / ohosX64
example/settings.gradle.kts 的 pluginManagement 与 dependencyResolutionManagement 都把两个 eazytec Maven URL 排在最前,否则 ohosArm64() 在官方 Kotlin 插件里不存在:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_haze"
include(":nativeApp")
nativeApp 只声明三个目标,sharedLib 只放这里,避免每个模块各吐一份带运行时的 so:
kotlin {
applyDefaultHierarchyTemplate()
jvm() // JVM 交叉验证目标
ohosArm64 { binaries.sharedLib { baseName = "ohoshaze" } }
ohosX64 { binaries.sharedLib { baseName = "ohoshaze" } }
}
2.2 构建核心库和 Native 产物
cd example
./gradlew :nativeApp:jvmTest \
:nativeApp:linkDebugSharedOhosArm64 \
:nativeApp:linkDebugSharedOhosX64
copyKnSoArm64 / copyKnSoX64 用 finalizedBy 挂在链接任务上,准备:
example/harmonyApp/entry/libs/arm64-v8a/libohoshaze.so ≈8.51 MB
example/harmonyApp/entry/libs/arm64-v8a/libc++_shared.so ≈1.85 MB(同 triple)
example/harmonyApp/entry/libs/x86_64/libohoshaze.so ≈7.71 MB
example/harmonyApp/entry/libs/x86_64/libc++_shared.so ≈1.73 MB
nm -gU libohoshaze.so 可见 HazeRender / HazeVersion 均为全局 T 符号。
2.3 独立消费工程
example 不依赖上游任何 Gradle 模块,内核(模型 + 管线)就在 nativeApp/src/commonMain 里,同时被 jvm() 与两个 ohos 目标编译——同一份源码,JVM 上跑测试钉值,ohos 上跑真机。
第 3 阶段:参数模型建模(技术难点)
3.1 JSON 契约
Index.ets generate()
↓ HazeRequest(扁平 JSON)
N-API hazeRender(string)
↓ const char*
@CName("HazeRender")
↓ HazeEngine.render
场景渲染 → 盒式模糊 → 材质/tint/噪点 → alpha×progressive×蒙版
↓ PNG Base64
ArkTS Image(PixelMap)
页面不复制任何材质判定或强度插值逻辑,每个回显字段都由公共 Kotlin 代码生成。
3.2 复刻的数据模型
HazeModel.kt 把四个上游源文件的取值语义 1:1 镜像成纯 Kotlin,文件头逐条注明出处:
object HazeModel {
const val VERSION = "2.0.1-ohos.1" // <上游>-ohos.1 约定
const val UPSTREAM_VERSION = "2.0.1"
object Defaults { // HazeBlurDefaults.kt
const val BLUR_RADIUS_DP = 20f // blurRadius = 20.dp
const val NOISE_FACTOR = 0.15f // noiseFactor = 0.15f
}
object Materials { // HazeMaterials.kt
const val BLUR_RADIUS_DP = 24f // 材质统一写 blurRadius(24.dp)
const val LUMINANCE_THRESHOLD = 0.5f // luminance >= 0.5 → lightAlpha
val ultraThin = Material("ultraThin", 0.35f, 0.55f)
val thin = Material("thin", 0.60f, 0.65f)
val regular = Material("regular", 0.73f, 0.80f)
val thick = Material("thick", 0.83f, 0.90f)
val ultraThick = Material("ultraThick", 0.92f, 0.97f)
}
fun easeIn(t: Float): Float = cubicBezier(0.42f, 0f, 1f, 1f, t) // EaseIn
fun luminance(argb: Int): Float // W3C sRGB 相对亮度(0.04045 阈值)
}
3.3 渲染逻辑
管线严格按上游 ResolvedHazeBlurStyle 的合成顺序:
// HazeEngine.render 主循环(节选)
val blurred = BlurKernel.blur(content, width, height, blurRadiusDp.toInt())
// 三趟盒式模糊逼近高斯,边缘 clamp 对应 BlurredEdgeTreatment.Rectangle
for (pixel in panelMask) {
val intensity = alpha * progressive(x, y) // EaseIn 插值的双端强度
var effect = blurred[idx]
effect = srcOver(effect, backgroundColor) // 材质写 backgroundColor(container)
effect = srcOver(effect, tint) // HazeColorEffect.tint(container.copy(alpha))
effect = jitter(effect, grain(noiseFactor)) // 确定性噪点
out[idx] = lerp(content[idx], effect, mask * intensity)
}
// PNG(stored zlib)→ Base64 → JSON 回显
渐进强度是 HazeProgressive.asBrush 的连续形式:lerp(startIntensity, endIntensity, easing.transform(x)),径向模式的半径取「能放进绘制区域的最大半径」(面板对角线一半),与上游默认一致。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ 扁平 JSON string
▼
C++ N-API entry(libhaze_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoshaze.so)
│ HazeEngine
▼
UTF-8 JSON + 显式 DisposeString
HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务函数挂到 ArkTS 模块,所以 ArkTS 永远只 import NAPI 包装层,不 import KN so。
4.2 Kotlin/Native 导出函数
@CName("HazeVersion")
fun hazeVersion(): String = HazeEngine.versionJson()
@CName("HazeRender")
fun hazeRender(request: String): String = try {
HazeEngine.render(request)
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
Kotlin 异常永不跨语言边界——跨过去是无信息崩溃,统一转 ok:false。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"hazeRender", nullptr, NapiHazeRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"hazeVersion", nullptr, NapiHazeVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
API 26 的 NAPI 头把 napi_get_value_string_utf8 缓冲区标成非 const,std::string::data() 是 const char* 直接传会编不过:先量长、std::vector<char> 拷贝、再构造 std::string。KN 返回的字符串先拷进 JS 字符串,再调 libohoshaze_symbols()->DisposeString(raw) 释放,不 free,不让 ArkTS 管。
4.4 CMake 和类型声明
add_library(ohoshaze SHARED IMPORTED)
set_target_properties(ohoshaze PROPERTIES
IMPORTED_LOCATION ${KN_SO_DIR}/libohoshaze.so
IMPORTED_NO_SONAME TRUE)
add_library(haze_napi SHARED napi_init.cpp)
target_link_options(haze_napi PRIVATE "-Wl,--no-undefined" "-Wl,-rpath,\$ORIGIN")
target_link_libraries(haze_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohoshaze)
add_custom_command(TARGET haze_napi POST_BUILD # KN so + libc++ 保持在 NAPI so 旁边
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${KN_SO} $<TARGET_FILE_DIR:haze_napi>
...)
export const hazeRender: (request: string) => string;
export const hazeVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 版本握手和默认帧
import { hazeRender, hazeVersion } from 'libhaze_napi.so';
const v = JSON.parse(hazeVersion()) as VersionOk;
this.versionLine = `桥 ${v.version} · 上游 ${v.upstream} · Kotlin ${v.kotlin} · ${v.engine}`;
this.generate(); // aboutToAppear 自动渲染默认帧
5.2 页面交互
毛玻璃材质 Tab 提供五档材质、明暗容器开关、「材质默认半径 24dp」开关(打开时请求省略 blurRadius,由 Native 按材质规则解析为 24)、噪点滑条;渐进模糊 Tab 提供三种模式切换与双端强度滑条(线性显示 start/end,径向显示 center/radius);自定义参数 Tab 提供 blurEnabled、整体 alpha、六色 tint、蒙版形状(圆角矩形 / 圆形)、panelRadius 与面板位置尺寸。滑条拖动中只刷新数值标签,松手(SliderChangeMode.End/Click)才进 Native 重渲染。
5.3 状态回显和错误边界
状态行随每次渲染更新:生成成功 360x640 png=…B tintα=…,其中 tintAlpha 是 Native 解析后的浮点值(材质按亮度选档 / 显式值),与上游表格逐位对账。ok:false 时显示 失败: ${error},HAP 不崩溃。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── settings.gradle.kts / build.gradle.kts / gradle.properties / gradlew
├── nativeApp/
│ ├── build.gradle.kts # jvm + ohos 双 ABI + copyKnSo*
│ └── src/
│ ├── commonMain/kotlin/example/nativeapp/ # HazeModel / BlurKernel / SceneRenderer / HazeEngine / PngEncoder / MiniJson
│ ├── ohosMain/kotlin/example/nativeapp/Bridge.kt
│ └── jvmTest/kotlin/example/nativeapp/ # 23 项交叉验证
└── harmonyApp/
├── AppScope/app.json5 # bundle org.terminator.ohos.haze
└── entry/src/main/
├── cpp/{CMakeLists.txt, napi_init.cpp, include/, types/libhaze_napi/}
├── ets/{entryability/EntryAbility.ets, pages/Index.ets}
└── module.json5 / resources/
6.2 构建、签名和安装
export JAVA_HOME="/path/to/jdk-21"
cd example
./gradlew :nativeApp:jvmTest \
:nativeApp:linkDebugSharedOhosArm64 \
:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp(不是仓库根目录!)
# File → Project Structure → Signing Configs 手动自动签名(证书不入库)
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc list targets -v
hdc -t <设备序列号> install -r \
example/harmonyApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.haze -m entry
四、完整代码对照
4.1 整体架构
HazeModel.kt / BlurKernel.kt / SceneRenderer.kt
│ Haze 2.x 参数模型 + 软件模糊 + 程序化场景
▼
HazeEngine.kt
│ ResolvedHazeBlurStyle 合成顺序 → PNG Base64 JSON
▼
Bridge.kt
│ HazeRender / HazeVersion(@CName C ABI)
▼
libhaze_napi.so
│ hazeRender / hazeVersion
▼
Index.ets
│ JSON parse + PixelMap 刷新
▼
ArkUI 真机页面
4.2 文件清单
| 文件 | 职责 |
|---|---|
example/settings.gradle.kts | 独立 OHOS 构建、eazytec 插件仓库、ohos_haze |
example/gradle.properties | JDK 21、native 三开关、version=2.0.1-ohos.1 |
example/nativeApp/build.gradle.kts | jvm + ohosArm64/ohosX64 sharedLib + copyKnSo* |
nativeApp/.../HazeModel.kt | Haze 2.x 参数模型复刻(出处注释 + 钉值) |
nativeApp/.../BlurKernel.kt | 三趟盒式模糊 + 确定性噪点 |
nativeApp/.../SceneRenderer.kt | 湖景 / 卡片 / 渐变三个程序化场景 |
nativeApp/.../HazeEngine.kt | JSON 协议解析 + 渲染管线 + 回显 |
nativeApp/.../PngEncoder.kt / MiniJson.kt | 零依赖 PNG(stored zlib)/ 扁平 JSON 解析 |
nativeApp/.../Bridge.kt | @CName C ABI,catch-all JSON 错误 |
nativeApp/src/jvmTest/ | 23 项:钉值 / 管线 / 协议 |
harmonyApp/.../napi_init.cpp | N-API 导出、字符串转换与释放 |
harmonyApp/.../Index.ets | ArkUI 三 Tab 玻璃工作台 |
README.OpenHarmony.md / _CN.md | 适配说明(中文版为主文档) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| KMP | HazeModel.Defaults | HazeBlurDefaults:20dp / 0.15 / 默认开启 |
| KMP | HazeModel.Materials | 五档材质 alpha 表 + 24dp + 亮度选档 |
| KMP | HazeModel.easeIn | CubicBezierEasing(0.42, 0, 1, 1)(Compose EaseIn) |
| KMP | HazeEngine.render | 按 ResolvedHazeBlurStyle 顺序渲染整帧 |
| Native | HazeRender | JSON 请求 → JSON(含 pngBase64)响应 |
| Native | HazeVersion | fork / 上游 / Kotlin / 引擎握手 |
| N-API | hazeRender / hazeVersion | 向 ArkTS 暴露的两个函数 |
| ArkTS | generate | 组装请求、调桥、刷新 PixelMap 与状态行 |
4.4 ArkTS 与 Kotlin 的边界
const raw = hazeRender(JSON.stringify({
scene: 'landscape', material: 'regular', containerColor: '#F7F2FA',
progressive: 'linear-v', startIntensity: 0, endIntensity: 1,
}));
const res = JSON.parse(raw) as RenderOk;
val tintAlpha = material.tintAlpha(containerColor) // luminance >= 0.5 → 0.73f
val tint = Colors.withAlpha(containerColor, tintAlpha)
两者之间只传输扁平 JSON,不传输 Kotlin 对象或未校验的动态结构;材质选档、强度插值、亮度计算全部在 Kotlin 侧,ArkTS 不重写任何判定。
五、关键决策说明
决策 1:独立 example 构建,而不是整仓切换鸿蒙 Kotlin
上游根构建包含 build-logic 复合构建、发布流、截图与基准测试;替换式改写(简单上游可接受)会砸坏整条流水线。独立构建零侵入、与上游可并行维护。
决策 2:复刻参数模型内核,而不是硬接 Compose 修饰符
haze 的 commonMain 没有可整体搬走的「纯 Kotlin 算法文件」(仅 4 个约 128 行的枚举/开关),有价值的正是参数语义。1:1 复刻 + 上游出处注释 + JVM 钉值,是对「代码复用」的诚实实现;硬接 Modifier.Node/GraphicsLayer 等于再打一套 Compose 发行版。
决策 3:软件模糊管线,而不是等平台 RenderEffect
共享库里没有 Skia/RenderEffect。三趟盒式模糊(boxes-for-gaussians)自包含、可测、语义可对齐;实时毛玻璃场景交给系统 backgroundBlurStyle,本桥定位是跨端一致的材质规范源与静态渲染。
决策 4:JSON 作为跨语言数据契约
扁平键值对 + 手写 MiniJson,不引 kotlinx.serialization / coroutines:少一个需要与 HarmonyOS Kotlin 对齐的 KN 依赖,C 签名 const char* → const char* 永不改变。
决策 5:ArkUI 作为验收宿主,参数同源是底线
回显字段(tintAlpha、blurRadius、progressive)必须与上游表格逐位一致;「设备上看起来对」不构成验收,jvmTest 钉值 + 状态行对账才构成。
六、测试与验证
6.1 测试环境
本次验证使用 macOS、JDK 21(Temurin)、HarmonyOS Kotlin 2.2.21-1.0.0、Gradle 8.14.1、DevEco Studio 5.1.0 Release 与鸿蒙真机(arm64-v8a,签名安装)。
6.2 共享样例测试
cd example && ./gradlew :nativeApp:jvmTest
23 项测试分三类全部通过:
- 模型钉值(8 项):默认 20dp/0.15、材质十档 alpha、24dp 材质半径、亮度白 1.0/黑 0.0/中灰 0.2159、EaseIn 端点与单调、版本约定;
- 管线性质(5 项):常数图模糊不变、脉冲扩散对称、零半径原样拷贝、PNG 签名与 IHDR 尺寸、噪点确定性;
- 协议回显(10 项):默认请求回显 20.0/0.15/none、材质回显 24.0 与亮度选档、显式 tint 优先、
alpha=0输出与原始场景 PNG 逐字节一致、画布钳制、非法色值转ok:false。
6.3 HAP 验证
CompileArkTS: finished
PackageHap: finished
SignHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
6.4 功能用例
- 启动后版本握手行显示
桥 2.0.1-ohos.1 · 上游 2.0.1 · Kotlin 2.2.21-1.0.0 · haze-kn-rasterizer,并自动渲染默认帧; - 毛玻璃材质 Tab:五档材质 tintα 依次 0.35/0.60/0.73/0.83/0.92,开「深色容器」跳到 0.55/0.65/0.80/0.90/0.97;
- 渐进模糊 Tab:纵向线性顶部清晰底部最糊(EaseIn 中段偏清晰),径向中心/边缘强度对调后效果反转;
- 自定义参数 Tab:关 blurEnabled 模糊消失 tint 仍生效;alpha 拉到 0 整个效果淡出;蒙版切圆形变椭圆窗;
- 三个场景(湖景/卡片/渐变)在每个 Tab 切换一遍,卡片场景高频边缘最能暴露模糊质量;
- 任意参数组合下状态行不出现
失败:,Native 异常显示在错误区域。
6.5 验证结论
Native 动态库(双 ABI + libc++)、C ABI、N-API、ArkUI 页面和签名 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实 Native 渲染的毛玻璃材质、渐进模糊与自定义参数效果,状态行回显与上游参数表逐位一致。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/haze.git
cd haze
export JAVA_HOME="/path/to/jdk-21"
cd example
./gradlew :nativeApp:jvmTest \
:nativeApp:linkDebugSharedOhosArm64 \
:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,自动签名后:
hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon
hdc -t <设备序列号> install -r \
example/harmonyApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.haze -m entry
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须真正编译
ohosArm64/ohosX64动态库; - N-API 不负责业务判断,C++ 只做类型检查、字符串转换和释放;
- Native 返回值交给 N-API 后必须调用
DisposeString; - 回显 alpha 要用解析后的浮点值,不能取 8-bit 量化值,否则和上游表格对不上账;
- 签名工程必须与源码分离,证书不能进入 AtomGit;
- 模拟器是 x86_64,只编 arm64 会报安装错误
9568347,双 ABI 都要出。
8.2 已知问题
- 软件模糊是高斯的逼近,大半径下与 Skia/RenderEffect 存在可见但不影响语义演示的差异,性能非 GPU 量级;
- 共享库拿不到 ArkUI 真实内容,
hazeSource的「捕获任意 UI」在 ohos 桥里暂无对应物,用程序化场景替代; HazeProgressive.Brush(位图蒙版)与HazeColorEffect.colorFilter未透出,扁平 JSON 暂无合理表达;- 噪点为确定性哈希抖动的逼近实现,不做逐平台一致性承诺;
- HAP 安装必须使用与 bundle name 匹配的签名。
8.3 未来优化方向
- 增加
pixelData字段接入真实图片(相册 PixelMap → N-API 指针),协议加字段不动 C 签名; - 透出
HazeProgressive.Brush与 colorFilter; - 等社区 CMP 1.12+ /
compose.uiohos 制品出现,评估用上游haze模块直接编 ohos,内核退役为对照实现; - 在 CI 中加入 Native 链接、ELF 检查和 HAP 构建;
- 补充不同设备 ABI、API 版本与性能模式的验收矩阵。
九、总结
9.1 核心难点回顾
Haze 2.x 参数模型(commonMain 语义)
→ 纯 Kotlin 复刻 + JVM 钉值
→ ohosArm64/ohosX64 Kotlin/Native
→ C ABI(HazeRender/HazeVersion)
→ C++ N-API(libhaze_napi.so)
→ ArkTS JSON 解析 + PixelMap
→ ArkUI 真机毛玻璃工作台
9.2 封装层次
- KMP 层:提供 Haze 2.x 默认值、材质表、渐进渐变、缓动与亮度的同源语义;
- Native 层:生成双 ABI 动态库并通过两个 C ABI 符号输出 JSON + PNG;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理三 Tab 状态、请求组装、结果展示和错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先让参数模型在 JVM 与 ohos 双侧编译通过并钉值,再接 ArkUI;回显字段与上游表格逐位一致是验收底线;
- Compose 重度库的可复用资产往往是「取值语义」而不是某个文件——复刻 + 出处注释 + 测试钉值,好过硬接工具链;
- 用扁平 JSON 和少量 C ABI 代替跨语言对象传递,并明确字符串释放责任;分别记录自动测试、Native 链接、HAP 构建和真机运行,避免把编译成功误认为设备链路可用。
9.4 适配成果
当前适配已完成:
example/独立 OpenHarmony Gradle 构建(上游根构建零改动);- Haze 2.x 参数模型内核的双 ABI Kotlin/Native 编译(
libohoshaze.soarm64/x64 + 匹配 libc++); - 两个 C ABI 符号和 C++ N-API 包装(
libhaze_napi.so); - DevEco Stage ArkUI 三 Tab 验收页面;
- 23 项 JVM 交叉验证测试全部通过;
- HAP 构建、签名、真机安装与运行验证;
- AtomGit 项目文档(
README.OpenHarmony.md/README.OpenHarmony_CN.md)与真机效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐




所有评论(0)