本文记录 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 / msPNG 字节数与 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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(Eclipse Temurin,org.gradle.java.home 写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle9.6.1(上游 wrapper 9.8.1 降级;KGP 2.4.20 线在 9.6.1 上有 KStore 先例)
外部依赖引擎零依赖——不需要 kotlinx ohos 制品线,唯一外部需求是 Kotlin 编译器本身
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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.ktandroidx.compose Modifier/Composable/Dp/Shape/Colorgit mv 进 composeMain,留盘不编
internal/AbstractLiquidElement.ktexpect/actual 渲染管线主体,26 个 Compose importgit mv 进 composeMain
internal/InternalLiquidScope.ktCompose 状态 + 参数派生(含 frost→sigma 数学)git mv 进 composeMain
internal/LiquefiableElement.kt / drawUtil.ktModifier.Node / GraphicsLayer / 圆角归一化git mv 进 composeMain
androidMain / skikoMain / commonTestAndroid 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 方法覆盖内核
LiquidRenderliquidRender(req)背景生成 + frost 高斯 + SDF/折射/色散/调色/光照/AA + PNG
LiquidShaderSourceliquidShaderSource()引擎握手(上游 SkSL 原文)
LiquidVersionliquidVersion()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.tomlKotlin 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.ktCPU 光栅化、逐段 shader 对照、frost 高斯、程序化背景
nativeApp/.../Bridge.kt3 个 C ABI、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 调参台(双握手/拖动/预设/八滑杆)
README.OpenHarmony_CN.md鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
引擎LiquidShader 常量SkSL 视觉数学本体(uniform:size/cornerRadii/refraction/curve/edge/tint/saturation/dispersion/contrast)
上游 ComposeModifier.liquid / Modifier.liquefiable效果层/内容层标记(留盘 composeMain,ohos 不编)
上游 ComposeLiquidScope 九参数refraction/curve/edge/tint/saturation/dispersion/contrast/frost/shape
渲染器LiquidRenderer.render(LiquidRequest)背景生成 + frost 高斯 + 全数学 + PNG
渲染器frostToSigma(frost)0.57735f * frost + 0.5f(上游同款换算)
NativeLiquidRender / LiquidShaderSource / LiquidVersionC ABI JSON 导出
N-APIliquidRender(req) 等向 ArkTS 暴露能力
ArkTSgenerate() / 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% 圆角时角点落在圆盘内
参数 clampcornerPercent>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 功能用例

  1. 启动后握手行出现 liquidVersion() → 1.1.1-ohos.1 与 liquidShaderSource() 的着色器字节数(两个真实 Native 调用,非静态文字);
  2. 默认参数(aurora 背景 + 玻璃按钮预设)自动渲染第一帧,状态行显示 Native 侧渲染用时;
  3. 拖动画布,玻璃中心跟随手指移动,松手后按新位置重渲;
  4. 切"清透镜"预设,frost 归零、折射拉满,彩条背景出现明显球面形变;
  5. 切"棱镜"预设,色散 0.6 时条纹边缘出现红蓝撕裂;
  6. 拉 frost 滑杆至高位,玻璃内背景整体雾化(高斯模糊生效);
  7. tint 四选切换,玻璃叠加对应色调;饱和度/对比度滑杆改变玻璃内色彩;
  8. 圆角滑杆 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :liquid 的引擎真正编译成 ohosArm64 / ohosX64 产物——纯 UI 库的"引擎"可能是 shader 字符串,但编译验证一步不能省;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Compose 文件必须先拆进 composeMain 再加 ohos target——留在 commonMain 会让 ohos 编译器直接碰 androidx.compose import,报错位置全在别人的代码里;
  4. 上游 :liquid 挂着 explicitApi() + abiValidation() + 发布插件,往原构建上加 target 会牵连 ABI dump,根构建替换比打补丁便宜得多;
  5. LiquidShader 原本是 internal,跨模块桥接拿不到——一行 public 化是全仓唯一源码改动,必须在文档"改动清单"里如实记录;
  6. ArkTS 组件的 @State 不能与内置通用属性方法重名:contrast、tint 都会报 “not assignable to the same property in base type ‘CustomComponent’”,改名 contrastValue / tintValue(JSON 键名不受影响);
  7. TextAlign 枚举是大写 TextAlign.RIGHT,写成 .Right 编译器只提示 Did you mean——一处一处改;
  8. 拖动手势 onActionUpdate 里直接重渲会卡成幻灯片,onActionEnd 才触发、滑杆用 120ms 节流,CPU 光栅化的帧率预算要靠交互侧纪律兜底;
  9. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. DevEco 打开的是 example/harmonyApp,不是仓库根目录——根目录会被识别成 Gradle 工程,两侧构建文件互相干扰;DevEco 生成的 .idea/、.hvigor/、entry/libs/ 要靠根 .gitignore 的路径条目挡住。

8.2 已知问题

  • Compose 宿主不可用:Modifier.liquid / Modifier.liquefiable / LiquidScope DSL 留在 composeMain,等社区 compose.ui ohos 制品与 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 封装层次

  • 引擎层:上游原版 LiquidShader SkSL 常量(SDF、折射、色散、调色、光照、AA 全部视觉数学);
  • 渲染层:CPU 光栅化器逐段对照实现,frost 高斯与圆角归一化复刻上游 CPU 侧数学,PNG 编码输出;
  • Native 层:生成双 ABI 动态库并通过 3 个 C ABI 输出 JSON/PNG;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理双握手、调参状态、手势拖动、渲染节流与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 纯 UI 库的适配面要看"数学住在哪":liquid 的全部视觉数学住在一份 shader 字符串里,公共 API 反而全是 Compose——引擎/宿主分离的边界由此自然得出,路线选择基于源码依赖度盘点,而不是库的类型标签;
  2. 零依赖是纯 UI 库适配的最大红利:KStore 要先验证四件套 kotlinx ohos 制品线,liquid 引擎一行外部依赖都没有,工具链选型只剩"与上游同代"一条约束——依赖矩阵的复杂度决定适配的前置成本;
  3. 交叉验证跟着引擎走:引擎是字符串 + 数学,测试就钉数学本身——SDF 符号、clamp 边界、sigma 换算、色散对称、形变生效,10 项 JVM 单测与 .so 里的是同一份代码,"数学没漂"才是纯 UI 库版"内核没漂"的证明。

9.4 适配成果

当前适配已完成:

  • liquid 1.1.1 SkSL 引擎的双 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。

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐