开源鸿蒙平台 CMP 三方库「liquid」适配全流程
本文记录
liquid接入 OpenHarmony 的完整过程,覆盖 Compose 液态玻璃库的引擎边界盘点、根构建替换、ohosArm64/ohosX64目标、Kotlin/Native 动态库、SkSL 数学到 CPU 光栅化的逐段移植、C ABI/N-API 桥接、ArkUI 调参台页面、HAP 构建和真机验收。
liquid是基于 RuntimeShader/RuntimeEffect 与 ModifierNodeElement 的 Compose Multiplatform 液态玻璃效果库:Modifier.liquefiable(liquidState)把节点标记为内容层,Modifier.liquid(liquidState)在效果层上对捕获到的背景像素做球面折射、色散、毛玻璃模糊、tint、饱和度/对比度与边缘光照,一个LiquidScope九参数的 DSL 管住全部视觉。本次适配与社区前两条路线(qrose 的"提取零依赖内核"、KStore 的"真编译整库")本质不同:liquid 的公共 API 全部是 Compose 代码,唯一不碰 Compose 的是那份 116 行的 SkSL 着色器引擎常量——因此适配走的是第三条路:引擎源码原样编译 + 着色器数学逐段 CPU 移植 + ArkUI 宿主验收,一份与上游逐字一致的液态玻璃渲染引擎运行在真机上。
项目地址: AtomGit/oh-tpc/liquid.git
上游仓库: FletchMcKee/liquid
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
liquid 为 Compose Multiplatform 应用提供"液态玻璃"这一件事的完整答案:rememberLiquidState() 一行建状态,liquefiable 标记背景、liquid 施加效果,LiquidScope 里 refraction / curve 控制球面透镜的折射深度与曲率,dispersion 做红蓝反向的立方衰减色散,frost 叠加毛玻璃模糊,tint / saturation / contrast / edge 完成调色与边缘光照,shape 支持任意圆角。KMP 项目把这些能力放在 commonMain,由 Android(API 33+ RuntimeShader,低版本降级)、iOS/macOS/JVM/wasm(org.jetbrains.skia RuntimeEffect)共享同一份 SkSL。OpenHarmony 应用同样需要这套视觉效果,但上游构建(Kotlin 2.4.21 / Compose 1.12.1 / Gradle 9.8.1 / AGP 9.4.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示滑杆,却无法证明液态玻璃的渲染引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证 SkSL 引擎、CPU 光栅化数学、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.21 / Compose 1.12.1 / Gradle 9.8.1 与鸿蒙工具链无法共存于一个 Gradle 构建;社区 CMP 是 1.9.2 / HarmonyOS Kotlin 2.2.21 线,Painter 与 ModifierNode 的 androidx.compose 依赖在 ohos 上无制品。 |
| 依赖矩阵 | 引擎本体(SkSL 常量)零外部依赖是红利;但 Modifier.liquid / Modifier.liquefiable / LiquidScope 全部站在 androidx.compose.ui 上,Compose 层整体进不了 ohos 编译。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 渲染语义跨不了桥 | SkSL 的 uniform shader content + content.eval() 是 GPU 运行时纹理采样,宿主链路(GraphicsLayer 捕获 → blur 链 → RuntimeShader)是 Compose 内部管线,ArkTS 侧必须重做宿主。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:SkSL 引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。渲染数学完全由上游那份着色器定义,CPU 光栅化器逐段忠实移植,ArkTS 只负责调参、调用 Native 和展示 PNG。
1.2 库提供的能力
Demo 提供一个真实的液态玻璃渲染会话,对应桥的全部入口:
- 版本握手(version):
liquidVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字; - 引擎握手(shader source):
liquidShaderSource()返回上游LiquidShader常量原文(约 4.6 KB / 116 行),页面显示其字节数——证明.so链接的就是:liquid模块的编译产物; - 渲染(render):
liquidRender(json)一口进——程序化背景、玻璃透镜位置/尺寸/圆角、九参数视觉模型,返回 PNG Base64; - 拖动交互:PanGesture 移动玻璃中心,
onActionEnd重渲,对应上游 Drag Demo 的语义。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
width / height | 实际渲染画布尺寸(非法输入被 clamp)。 |
bytes / ms | PNG 字节数与 Native 侧渲染用时。 |
pngBase64 | 渲染结果 PNG 的 Base64,ArkTS 解码为 PixelMap。 |
引擎侧完整能力(SDF 圆角矩形、sdgBox 解析梯度、球面折射、色散三采样、饱和度/对比度、tint、边缘光照、AA)全部随那份 SkSL 常量与 CPU 光栅化器进入 .so,JVM 侧保留 10 项渲染器单元测试做交叉验证,证明 ohos .so 里执行的玻璃数学没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | LiquidShader.kt 原样编译进 ohos(唯一改动:internal → public 一行,供桥跨模块导出);上游 7 个 Compose 文件 git mv 进 src/composeMain 留盘不编,内容零改动。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);benchmark / core:testing / samples:* / build-logic(上游 Android/Roborazzi 生态)留盘不进构建。 |
| 桥接稳定 | 使用 3 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 引擎零外部依赖,不触碰 kotlinx ohos 制品线;版本主体与上游一致(1.1.1),fork 后缀 -ohos.1。 |
| UI 完整 | 单页覆盖全部桥接入口:双握手、可拖动画布、四预设、三背景、tint 四选、八参数滑杆。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。liquid 的 Compose 层(Modifier API)留在
composeMain,等社区 CMP 工具链与 Compose 1.12 对齐后原样接回;ArkTS Demo 把桥暴露的渲染闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与源码依赖边界、确认引擎面、确定交付物
第 2 阶段:目标与工具链打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、Gradle 9.6.1
第 3 阶段:桥协议建模 ── 15 键请求契约、PNG Base64 载荷、错误通道
第 4 阶段:光栅化移植 ── SkSL 数学逐段 CPU 端口、frost 高斯、程序化背景
第 5 阶段:ArkUI 宿主 ── 双握手、拖动画布、预设/背景/tint/八滑杆
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
liquid/ 上游液态玻璃库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
liquid/src/commonMain/ SkSL 引擎常量 LiquidShader.kt(全库唯一非 Compose 文件)
liquid/src/composeMain/ 上游 Compose 实现(Modifier API/LiquidScope/渲染管线,留盘不编)
liquid/src/androidMain/ 上游 Android RuntimeShader 路径,留盘不进构建
liquid/src/skikoMain/ 上游 skiko RuntimeEffect 路径,留盘不进构建
liquid/src/commonTest/ 上游 Roborazzi 截图测试(依赖 Compose),留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + LiquidRenderer + PngEncoder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
benchmark/ samples/ core/ build-logic/ 上游 Android/示例生态,留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :liquid 与 :example:nativeApp,rootProject.name = "ohos_liquid"。上游的 build-logic 约定插件、AGP classpath、vanniktech 发布、dokka、spotless、Roborazzi 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(Eclipse Temurin,org.gradle.java.home 写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 9.6.1(上游 wrapper 9.8.1 降级;KGP 2.4.20 线在 9.6.1 上有 KStore 先例) |
| 外部依赖 | 引擎零依赖——不需要 kotlinx ohos 制品线,唯一外部需求是 Kotlin 编译器本身 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.liquid |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 文件(commonMain) | 依赖形态 | 处置 |
|---|---|---|
internal/LiquidShader.kt | 零 import,纯 Kotlin 字符串常量 | 留在 commonMain 编译(唯一 internal→public 一行改动) |
Liquefiable.kt / Liquid.kt / LiquidScope.kt | androidx.compose Modifier/Composable/Dp/Shape/Color | git mv 进 composeMain,留盘不编 |
internal/AbstractLiquidElement.kt | expect/actual 渲染管线主体,26 个 Compose import | git mv 进 composeMain |
internal/InternalLiquidScope.kt | Compose 状态 + 参数派生(含 frost→sigma 数学) | git mv 进 composeMain |
internal/LiquefiableElement.kt / drawUtil.kt | Modifier.Node / GraphicsLayer / 圆角归一化 | git mv 进 composeMain |
androidMain / skikoMain / commonTest | Android RuntimeShader / Skia RuntimeEffect / Roborazzi | 留盘不进构建 |
盘点结论是本次适配的成败手:全库只有 1 个文件不碰 Compose,而它恰好是引擎本体。着色器字符串承载了全部视觉数学(SDF、折射、色散、调色、光照、AA),CPU 侧标量数学只有圆角归一化(RoundedCornerShape(percent) → 归一化四元组,cap 0.5)和 frost 换算(sigma = 0.57735f * frost + 0.5f,上游 InternalLiquidScope.kt 第 226 行)两处,都可以在光栅化器里忠实复刻。过去挡住纯 UI 库适配的是"没有可编译的引擎",liquid 证明这个障碍对"数学在 shader 字符串里"的库不成立。
1.4 页面边界
标题区 LIQUID · OPENHARMONY / 副标题(CMP 液态玻璃库 · Kotlin/Native ohosArm64/ohosX64)
握手行 版本 1.1.1-ohos.1 · 引擎着色器 N 字节(两个真实 Native 调用)
画布区 Image(PixelMap) 360×520 + PanGesture 拖动玻璃 + 提示"拖动画布可移动玻璃"
预设区 玻璃按钮 / 清透镜 / 棱镜 / 深毛玻璃(对照上游 Demo 参数)
背景区 极光 aurora / 点阵 grid / 彩条 stripes(程序化生成)
tint 区 无 tint / 白 25% / 蓝 30% / 暖 25%
滑杆区 frost 0-30 / refraction 0-0.5 / curve 0-1 / edge 0-0.25 / saturation 0-2 / dispersion 0-1 / contrast 0-2 / cornerPercent 0-50
状态行 渲染完成 N ms / 错误原文 / 参数摘要
页面启动时先做双握手(版本 + 着色器字节数),再按默认参数渲染第一帧。
第 2 阶段:目标与工具链打通
2.1 依赖边界验证(零依赖红利)
与 KStore 适配的第一步不同,liquid 不需要逐个验证 kotlinx ohos 制品线——引擎常量不依赖 coroutines / serialization / kotlinx-io 中的任何一个。适配前只需确认两件事:其一,eazytec 社区 Maven(https://maven.eazytec-cloud.com)上有 HarmonyOS Kotlin 2.4.20-0.1.0-07 的 KMP 插件与 ohosArm64 / ohosX64 目标支持;其二,JVM 交叉验证用的 kotlin-test 随插件自带。依赖矩阵这一栏从"四件套逐个验证"退化为"零依赖直接通过"——这是选 liquid 做纯 UI 库适配样板的直接原因。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07 + Gradle 9.6.1)
上游源码是 Kotlin 2.4.21 编的,社区钉法 2.2.21(qrose 线)与之差了两个代际,而且 liquid 没有外部依赖把版本"锚"在旧线上——所以工具链直接选与上游同代的 2.4.20-0.1.0-07,源码级兼容成本为零。Gradle 方面,上游 wrapper 是 9.8.1,KGP 2.4.20 线已验证的组合是 9.6.1(KStore 先例),本仓把 gradle-wrapper.properties 的 distributionUrl 降到 9.6.1,只动这一行。
settings.gradle.kts 把 eazytec 仓库写在最前:
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_liquid"
include(":liquid")
include(":example:nativeApp")
2.3 编译真实引擎
liquid/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
}
}
注意上游这个模块原本挂着 compose / AGP-KMP / vanniktech / dokka 四个插件和 explicitApi() + abiValidation()——直接往它加 ohos target 会牵连 ABI dump 与发布变体,所以整体随根构建替换。实测:
.\gradlew.bat :liquid:compileKotlinJvm :liquid:compileKotlinOhosArm64 :liquid:compileKotlinOhosX64 # BUILD SUCCESSFUL
引擎常量一个字没动(除那一行可见性),Compose 实现全部隔离在 composeMain,ohos 编译器看不到 androidx.compose。
2.4 测试依赖的处置:Roborazzi 截图测试不跨工具链
上游 commonTest 是 Roborazzi 截图测试(依赖 compose.ui-test 与 Skia 渲染),在 ohos 构建里既无制品也无宿主。处置是两条纪律的组合:测试改为在 example/nativeApp 新写纯 Kotlin 渲染器单测(jvmTest),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
上游测试文件零改动留盘;新的渲染器测试与进 .so 的是同一份代码,交叉验证的有效性由此成立。
第 3 阶段:桥协议建模
3.1 JSON 契约
io.github.fletchmckee.liquid.internal.LiquidShader(上游引擎常量,原样编译)
↓ :liquid KLIB
LiquidRenderer.kt(CPU 光栅化,逐段对照 shader 数学)
↓ LiquidRequest / PNG
Bridge.kt(@CName × 3,扁平 JSON 进、PNG Base64 出)
↓ UTF-8 JSON
N-API liquidRender() / liquidShaderSource() / liquidVersion()
↓
ArkUI Index.ets(液态玻璃调参台)
请求与响应:
请求 : {"width":360,"height":520,"bg":"aurora","lensX":0.5,"lensY":0.32,
"lensW":0.66,"lensH":0.30,"cornerPercent":25,"frost":15,
"refraction":0.08,"curve":0.15,"edge":0.02,"saturation":1.0,
"dispersion":0.0,"contrast":1.0,"tint":"none"}
响应 : {"ok":true,"width":360,"height":520,"bytes":N,"ms":T,"pngBase64":"..."}
{"ok":false,"error":"..."}
两个设计取向:载荷是 PNG Base64 而非 JsonElement——玻璃渲染的产物是位图,ArkTS 侧 Base64Helper + image.createImageSource 一次解码成 PixelMap,不做逐像素 JSON;lensX/lensY/lensW/lensH 用 0-1 相对坐标——画布尺寸与玻璃位置解耦,拖动手势只做除法换算。
3.2 视觉参数为什么逐字对齐上游
桥的 15 个键全部来自上游 LiquidScope 与 Demo 参数模型,范围一一对齐 LiquidControls 滑杆:frost 0-30、refraction 0-0.5、curve 0-1、edge 0-0.25、saturation 0-2、dispersion 0-1、contrast 0-2、cornerPercent 0-50。四个预设锚定上游 Demo 常用值:玻璃按钮(frost=15, refraction=0.08, curve=0.15, edge=0.02)、清透镜(frost=0, refraction=0.25, curve=0.25)、棱镜(dispersion=0.6, contrast=1.15)、深毛玻璃(frost=28, saturation=1.4)。Native 侧 LiquidRequest 构造即 clamp,非法/NaN 输入回退默认值,永不抛异常。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("LiquidRender")
fun liquidRender(request: String): String = try {
/* 解析 → clamp → 渲染 → PNG Base64 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、越界参数、不支持的字段值,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。
第 4 阶段:光栅化移植(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libliquid_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosliquid.so)
│ LiquidRender / LiquidShaderSource / LiquidVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出字符串又会让页面复制渲染数学,所以选择 C ABI + JSON + 桥内完成全部渲染。
4.2 Kotlin/Native 导出函数
@CName("LiquidRender")
fun liquidRender(request: String): String // 15 键渲染请求一口进
@CName("LiquidShaderSource")
fun liquidShaderSource(): String = LiquidShader // 上游 SkSL 原文(import 引用,证明链接 :liquid 产物)
@CName("LiquidVersion")
fun liquidVersion(): String = "1.1.1-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
LiquidRender | liquidRender(req) | 背景生成 + frost 高斯 + SDF/折射/色散/调色/光照/AA + PNG |
LiquidShaderSource | liquidShaderSource() | 引擎握手(上游 SkSL 原文) |
LiquidVersion | liquidVersion() | fork 版本握手(真机握手行的数据来源) |
4.3 SkSL 数学逐段 CPU 端口
LiquidRenderer.kt 的渲染主循环逐段对照着色器行号实现,注释即对照表:
// shader line 65-66:
// lensDepth = 1.0 - saturate(-shapeSdf / refraction);
// distortion = 1.0 - sqrt(1.0 - lensDepth * lensDepth);
// normalDisplacement = distortion * -curve * minDimension;
val lensDepth = 1f - saturate(-shapeSdf / request.refraction)
val distortion = 1f - sqrt(1f - lensDepth * lensDepth)
val displacement = distortion * -request.curve * minDim
// shader line 83: aberration = dispersion * distFromCenter^3 * minDimension;
dispersionAberration(request.dispersion, distX, distY, minDim, aberration)
val coordRx = baseX - aberration[0] // R 通道反向偏移
val coordBx = baseX + aberration[0] // B 通道正向偏移
移植纪律的四条要点:归一化空间一致——shapeCoord=(p-center)/minDimension、aaWidth=1.5/minDimension 与 shader 逐字对应;易错点照抄——色散的 distFromCenter 除以 size 分量而非 minDimension、出界回退 G 通道坐标、validR/validB 用 shapeVr(非 lensVr)判界;frost 用上游换算——sigma = 0.57735f * frost + 0.5f(注释标明 InternalLiquidScope.kt:226 出处),可分离高斯 + 边缘复制对照 Skia FilterTileMode.CLAMP;性能裁剪不越界——只模糊透镜包围盒外扩 64px 的区域(色散与位移的采样范围有限),采样 clamp 到该区域。合成采用直行 RGBA + src-over:a = aaAlpha,玻璃外保持背景,AA 半透明带自然过渡。
4.4 程序化背景
上游 Demo 的背景是照片与时钟,PNG 解码器不在最小交付面里。三种确定性背景覆盖三种验收语义:aurora(深靛蓝→紫→青绿渐变 + 三个径向发光 + 半透明条带)验证 frost 与折射、grid(米白底 + 32px 间距六色环圆点)对照上游 Grid Demo 且点阵形变最可辨、stripes(45° 七色彩条 + 白色圆环)让色散的红蓝撕裂一目了然。
4.5 CMake 和类型声明
add_library(ohosliquid SHARED IMPORTED)
set_target_properties(ohosliquid PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(liquid_napi SHARED napi_init.cpp)
target_link_libraries(liquid_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosliquid)
OHOS_ARCH 为空或 libohosliquid.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。KN 返回的字符串必须经 libohosliquid_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。
export const liquidRender: (request: string) => string;
export const liquidShaderSource: () => string;
export const liquidVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 双握手和触发渲染
aboutToAppear(): void {
this.version = liquidVersion(); // 版本握手:真实 Native 调用
this.shaderBytes = liquidShaderSource().length; // 引擎握手:上游 SkSL 字节数
this.generate();
}
private generate(): void {
const body: string = JSON.stringify({ width: CANVAS_W, height: CANVAS_H, bg: this.bg,
lensX: this.lensX, lensY: this.lensY, lensW: LENS_W, lensH: LENS_H,
cornerPercent: this.cornerPercent, frost: this.frost, refraction: this.refraction,
curve: this.curve, edge: this.edge, saturation: this.saturation,
dispersion: this.dispersion, contrast: this.contrastValue, tint: this.tintValue });
const parsed: RenderResponse = JSON.parse(liquidRender(body)) as RenderResponse;
if (!parsed.ok || parsed.pngBase64 === undefined) { /* 状态行报错 */ return; }
this.lastMs = parsed.ms ?? 0;
this.decodePng(parsed.pngBase64); // Base64 → ImageSource → PixelMap → @State
}
5.2 页面交互
画布区是 PanGesture 宿主:onActionUpdate 按位移/显示尺寸换算 lensX/lensY(clamp 0.1-0.9),拖动中不重渲,onActionEnd 才触发 generate——对应上游 Drag Demo 的"玻璃跟随手指"语义。预设/背景/tint 三组 chips 点击即套用整体参数并重渲;八个滑杆 onChange 更新状态后经 120ms setTimeout 节流重渲,aboutToDisappear 清理定时器。四预设锚点见 3.2 节。
5.3 对比度和错误边界
预设选中态深色高亮、tint 色板带 #AARRGGBB 原文;ok:false 时错误原文进状态行,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行与 hilog。滑杆全部内联在 @Builder ParamsPanel() 中,规避按值传参子 Builder 的刷新陷阱。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosliquid)+ cxxShared + copyKnSo*
│ └── src/
│ ├── commonMain/.../LiquidRenderer.kt CPU 光栅化(逐段对照 shader 行号)
│ ├── commonMain/.../PngEncoder.kt 最小 PNG 编码(zlib stored)
│ ├── commonTest/.../LiquidRendererTest.kt JVM 交叉验证 10 用例
│ └── ohosMain/.../Bridge.kt @CName × 3 + MiniJson
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.liquid、versionName 1.1.1-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohosliquid_api.h
├── src/main/cpp/types/libliquid_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 液态玻璃调参台
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录),构建 HAP:
# hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc list targets -v
hdc -t <设备序列号> install -r `
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start `
-a EntryAbility -b org.terminator.ohos.liquid -m entry
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。
四、完整代码对照
4.1 整体架构
io.github.fletchmckee.liquid.internal.LiquidShader(上游引擎常量,原样编译)
│ SDF / 折射 / 色散 / 调色 / 光照 / AA 全部视觉数学
LiquidRenderer + PngEncoder
│ CPU 光栅化(逐段对照)+ PNG 编码
Bridge.kt + MiniJson
│ LiquidRender / LiquidShaderSource / LiquidVersion
libliquid_napi.so
│ liquidRender / liquidShaderSource / liquidVersion
Index.ets
│ JSON parse + Base64 → PixelMap + @State
▼
ArkUI 真机调参台(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_liquid、samples 等移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 单条钉版(引擎零依赖,无 kotlinx 条目) |
build.gradle.kts(根) | 全局禁用 Ohos*Test* 任务 |
liquid/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64 |
liquid/src/commonMain/.../LiquidShader.kt | 上游引擎常量(唯一改动:internal→public) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../LiquidRenderer.kt | CPU 光栅化、逐段 shader 对照、frost 高斯、程序化背景 |
nativeApp/.../Bridge.kt | 3 个 C ABI、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 调参台(双握手/拖动/预设/八滑杆) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(20 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | LiquidShader 常量 | SkSL 视觉数学本体(uniform:size/cornerRadii/refraction/curve/edge/tint/saturation/dispersion/contrast) |
| 上游 Compose | Modifier.liquid / Modifier.liquefiable | 效果层/内容层标记(留盘 composeMain,ohos 不编) |
| 上游 Compose | LiquidScope 九参数 | refraction/curve/edge/tint/saturation/dispersion/contrast/frost/shape |
| 渲染器 | LiquidRenderer.render(LiquidRequest) | 背景生成 + frost 高斯 + 全数学 + PNG |
| 渲染器 | frostToSigma(frost) | 0.57735f * frost + 0.5f(上游同款换算) |
| Native | LiquidRender / LiquidShaderSource / LiquidVersion | C ABI JSON 导出 |
| N-API | liquidRender(req) 等 | 向 ArkTS 暴露能力 |
| ArkTS | generate() / PanGesture | 触发渲染并刷新 UI / 拖动玻璃 |
4.4 ArkTS 与 Kotlin 的边界
const raw = liquidRender(JSON.stringify({
width: 360, height: 520, bg: 'aurora',
lensX: 0.5, lensY: 0.32, lensW: 0.66, lensH: 0.30, cornerPercent: 25,
frost: 15, refraction: 0.08, curve: 0.15, edge: 0.02,
saturation: 1.0, dispersion: 0.0, contrast: 1.0, tint: 'none'
}));
const res = JSON.parse(raw);
// res.ok === true;res.pngBase64 → Base64Helper 解码 → PixelMap 上屏
val req = LiquidRequest(width = 360, height = 520, frost = 15f, /* … */)
val result = LiquidRenderer.render(req) // 同一份代码跑 JVM 测试、进 ohos .so
两者之间只传输扁平 JSON 字符串与 PNG Base64,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:引擎/宿主分离,而不是真编译整库或提取内核
qrose 抽编码器内核是因为 Painter 不可编;KStore 真编译整库是因为 kotlinx ohos 制品齐了。liquid 是第三种情形:公共 API 全是 Compose,但视觉引擎是一份纯 Kotlin 字符串。真编译整库不可行(Compose 层过不了 ohos 编译器),提取内核无必要(引擎本来就是自包含的)——正确动作是最小引擎面:常量原样编译 + 数学 CPU 移植 + 宿主 ArkUI 重做。
决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 降到 9.6.1
上游源码是 Kotlin 2.4.21,同代工具链源码级兼容成本为零;引擎零依赖意味着没有 stdlib 元数据反推约束,选新线纯粹为了贴近上游。Gradle 方面 KGP 2.4.20 线的已验证组合是 9.6.1(KStore 先例),从上游 9.8.1 降一行 wrapper 即可,两条工具链线的经验继续不能互相套用。
决策 3:根构建替换(cmp_qrose 模式),上游测试生态整体留盘
上游根构建(build-logic 约定插件 / AGP 9.4.1 / Roborazzi / spotless / vanniktech)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。上游 :liquid 模块挂着 explicitApi() + abiValidation(),直接加 ohos target 会牵连 ABI dump 与发布变体——所以重写模块构建而不是在上游文件上打补丁。上游 Roborazzi 截图测试依赖 Compose 宿主,留盘不编,交叉验证由新的纯 Kotlin 渲染器测试承担。
决策 4:CPU 光栅化先行,GPU 留作演进
SkSL 的 content.eval() 是 GPU 运行时采样,直通方案是 EGL/OpenGL ES + GLSL 移植(sampler2D + texture() 替换、half* → float*、saturate → clamp)。但 GPU 链路引入 EGL 上下文、shader 编译与纹理生命周期三重新复杂度,且难以在 JVM 侧交叉验证。CPU 光栅化让渲染数学与上游着色器逐行可对照、可单测、可在 debugShared 未 strip 的 .so 里直接验收——先证明"数学没漂",GPU 化是纯性能优化。
决策 5:双握手设计
只握手版本号,无法排除"页面写死字符串"的质疑。LiquidShaderSource 把上游 SkSL 原文返回给 ArkTS,页面显示其字节数——着色器内容随 :liquid 编译进 .so,伪造它等于把整份 SkSL 抄进 ArkTS,握手行因此是"引擎在包里"的最硬证据。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Eclipse Temurin)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :example:nativeApp:jvmTest
10 项用例全部通过(渲染器与进 .so 的是同一份代码):
| 覆盖面 | 用例钉住什么 |
|---|---|
| SDF 符号 | 透镜中心为负、远外为正、50% 圆角时角点落在圆盘内 |
| 参数 clamp | cornerPercent>50 收敛到 0.5;非法/NaN 输入回退默认 |
| frost 换算 | frost=15 → sigma≈9.160(0.57735*15+0.5) |
| 色散对称 | aberration 奇对称;dispersion=0.6 渲染结果产生实际差异 |
| 形变生效 | refraction/curve>0 时 grid 背景下大范围像素差异 |
| 渲染冒烟 | 小画布渲染成功,PNG magic bytes 正确 |
6.3 HAP 验证
:liquid:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
llvm-nm -D: LiquidRender / LiquidShaderSource / LiquidVersion 均为全局 T 符号
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully
libohosliquid.so debugShared 未 strip 约 3.8 MB(arm64)/ 3.4 MB(x86_64),含 Kotlin/Native 运行时;HAP 内双 ABI 各三件套(libliquid_napi.so / libohosliquid.so / libc++_shared.so)。
6.4 功能用例
- 启动后握手行出现
liquidVersion() → 1.1.1-ohos.1与liquidShaderSource()的着色器字节数(两个真实 Native 调用,非静态文字); - 默认参数(aurora 背景 + 玻璃按钮预设)自动渲染第一帧,状态行显示 Native 侧渲染用时;
- 拖动画布,玻璃中心跟随手指移动,松手后按新位置重渲;
- 切"清透镜"预设,frost 归零、折射拉满,彩条背景出现明显球面形变;
- 切"棱镜"预设,色散 0.6 时条纹边缘出现红蓝撕裂;
- 拉 frost 滑杆至高位,玻璃内背景整体雾化(高斯模糊生效);
- tint 四选切换,玻璃叠加对应色调;饱和度/对比度滑杆改变玻璃内色彩;
- 圆角滑杆 0-50 连续变化,玻璃四角从直角过渡到胶囊。
6.5 验证结论
SkSL 引擎、CPU 光栅化数学、frost 高斯、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的液态玻璃渲染会话:双握手、拖动玻璃、四预设与八参数实时调参(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/liquid.git
cd liquid
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :liquid:compileKotlinOhosArm64 :liquid:compileKotlinOhosX64 # klib
.\gradlew.bat :example:nativeApp:jvmTest # 10 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp 构建 HAP 后:
hdc install -r entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.liquid -m entry
hdc shell hilog | grep Liquid
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:liquid的引擎真正编译成ohosArm64/ohosX64产物——纯 UI 库的"引擎"可能是 shader 字符串,但编译验证一步不能省; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Compose 文件必须先拆进
composeMain再加 ohos target——留在commonMain会让 ohos 编译器直接碰androidx.composeimport,报错位置全在别人的代码里; - 上游
:liquid挂着explicitApi()+abiValidation()+ 发布插件,往原构建上加 target 会牵连 ABI dump,根构建替换比打补丁便宜得多; LiquidShader原本是internal,跨模块桥接拿不到——一行public化是全仓唯一源码改动,必须在文档"改动清单"里如实记录;- ArkTS 组件的
@State不能与内置通用属性方法重名:contrast、tint都会报 “not assignable to the same property in base type ‘CustomComponent’”,改名contrastValue/tintValue(JSON 键名不受影响); TextAlign枚举是大写TextAlign.RIGHT,写成.Right编译器只提示 Did you mean——一处一处改;- 拖动手势
onActionUpdate里直接重渲会卡成幻灯片,onActionEnd才触发、滑杆用 120ms 节流,CPU 光栅化的帧率预算要靠交互侧纪律兜底; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - DevEco 打开的是
example/harmonyApp,不是仓库根目录——根目录会被识别成 Gradle 工程,两侧构建文件互相干扰;DevEco 生成的.idea/、.hvigor/、entry/libs/要靠根.gitignore的路径条目挡住。
8.2 已知问题
- Compose 宿主不可用:
Modifier.liquid/Modifier.liquefiable/LiquidScopeDSL 留在composeMain,等社区compose.uiohos 制品与 CMP 1.12 对齐后原样接回; - CPU 光栅化不是 GPU:单帧 360×520 在真机上为百毫秒量级,适合出图、静态展示与参数预览,不适合连续动画;
- 背景是程序化生成(极光/点阵/彩条),不是照片——PNG 解码不在最小交付面,真实业务背景等 GPU 化后走纹理输入;
- PNG 用 zlib stored 未压缩,360×520 一帧约数百 KB,Base64 后 JSON 偏大;
- 上游 frost 在 Android 是 radius 制、skiko 是 sigma 制,CPU 版采用 skiko 换算线(
0.57735f * frost + 0.5f),两条线的视觉在低模糊段略有差异; updates类状态推送不跨桥:桥是同步一次一调用,参数变化由 ArkTS 主动重渲;- android / ios / wasm 目标剥离(源文件留在树里),发布、dokka、spotless 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.liquid匹配的签名。
8.3 未来优化方向
- GPU 化:EGL/OpenGL ES 离屏渲染,SkSL → GLSL 移植(
content.eval→texture()、half*→float*、saturate→clamp、layout(color)删除),纹理输入替代程序化背景,帧率进入动画区间; - 接回 Compose:社区 CMP 工具链与 Compose 1.12 / Kotlin 2.4 对齐后,
composeMain原样参与编译,CPU 光栅化降级为无 GPU 场景的 fallback; - 背景图输入通道:N-API 传像素缓冲(或 ArkTS 侧解码后传 BGRA 数组),替代程序化背景;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 liquid(Compose 液态玻璃库)
→ 盘点源码依赖度:引擎 = 一份零依赖 SkSL 常量,其余全是 Compose
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(上游 2.4.21 同代)/ Gradle 9.6.1
→ 根构建替换(cmp_qrose 模式)+ composeMain 源码集隔离
→ ohosArm64 / ohosX64 KLIB + 唯一一处 internal→public
→ CPU 光栅化逐段移植 shader 数学(frost 用上游 sigma 换算)
→ C ABI(@CName × 3)+ 双握手设计
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON + Base64 → PixelMap
→ ArkUI 真机调参台验收(拖动 / 预设 / 八参数)
9.2 封装层次
- 引擎层:上游原版
LiquidShaderSkSL 常量(SDF、折射、色散、调色、光照、AA 全部视觉数学); - 渲染层:CPU 光栅化器逐段对照实现,frost 高斯与圆角归一化复刻上游 CPU 侧数学,PNG 编码输出;
- Native 层:生成双 ABI 动态库并通过 3 个 C ABI 输出 JSON/PNG;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理双握手、调参状态、手势拖动、渲染节流与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 纯 UI 库的适配面要看"数学住在哪":liquid 的全部视觉数学住在一份 shader 字符串里,公共 API 反而全是 Compose——引擎/宿主分离的边界由此自然得出,路线选择基于源码依赖度盘点,而不是库的类型标签;
- 零依赖是纯 UI 库适配的最大红利:KStore 要先验证四件套 kotlinx ohos 制品线,liquid 引擎一行外部依赖都没有,工具链选型只剩"与上游同代"一条约束——依赖矩阵的复杂度决定适配的前置成本;
- 交叉验证跟着引擎走:引擎是字符串 + 数学,测试就钉数学本身——SDF 符号、clamp 边界、sigma 换算、色散对称、形变生效,10 项 JVM 单测与
.so里的是同一份代码,"数学没漂"才是纯 UI 库版"内核没漂"的证明。
9.4 适配成果
当前适配已完成:
- liquid
1.1.1SkSL 引擎的双 ABI Kotlin/Native 编译(引擎常量与上游逐字一致,唯一改动internal→public一行); - 上游 Compose 实现完整保留(7 文件 git mv 进
composeMain,100% rename 记录,内容零改动); - 3 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - CPU 光栅化器:SkSL 数学逐段对照移植 + frost 高斯(上游换算)+ 三种程序化背景 + 最小 PNG 编码;
- DevEco Stage ArkUI 调参台(双握手、可拖动玻璃、四预设、三背景、tint 四选、八参数滑杆、120ms 节流重渲);
- 10 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。
适配过程 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)