本文记录 compose-ripple-indication 接入 OpenHarmony 的完整过程,覆盖 Compose 涟漪库盘点、鸿蒙 Compose 制品线选型、上游整库真编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

compose-ripple-indication 是 Composable Horizons 出品的 Kotlin Multiplatform 涟漪效果库:rememberRippleIndication() 把 Material 的 createRippleModifierNode 包装成一行可用的 IndicationNodeFactory,让任意 Compose 设计体系(不止 Material)都能用上 Material 涟漪,并提供 color / bounded / radius / RippleAlpha 四个维度的定制。本次适配与社区早期"零依赖内核提取"路线(qrose / haze / PreCompose / voyager)有本质区别:社区 Maven 上的 Compose 鸿蒙制品线 1.12.0-0.1.0-07 已经发布了 material-ripple 的 ohosArm64 / ohosX64 klib,因此上游 commonMain 整文件一行不改、原样编译成 OpenHarmony KLIB 并链接进动态库——rememberRippleIndication、RippleAlpha、Material3DefaultRipple、MaterialRippleNodeFactory 全部是上游原件,ArkUI 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/compose-ripple-indication.git

上游仓库: composablehorizons/compose-ripple-indication

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

compose-ripple-indication 为 Compose Multiplatform 应用解决"触觉反馈最后一层"的问题:系统默认的 Indication 往往是朴素的变暗高亮,而这个库让任何自建设计体系一行接入 Material 涟漪——CompositionLocalProvider(LocalIndication provides rememberRippleIndication()) 即全局生效,或作为 defaultIndication 挂进 Compose Unstyled 主题。涟漪的颜色、是否有界(bounded)、半径(radius)、四态透明度(RippleAlpha 的 pressed / dragged / hovered / focused)全部可调,默认值取 Material 3 规范(0.16 / 0.10 / 0.08 / 0.10)。KMP 项目把这些能力放在 commonMain,由 Android、桌面、Web、iOS 共享。OpenHarmony 应用同样需要这套反馈语义,但上游构建(Kotlin 2.2.20 / Compose 1.9.1 / Gradle 8.14.3)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共库代码在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证库本体、Compose 制品线、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
依赖矩阵库依赖 compose.foundation + org.jetbrains.compose.material:material-ripple,后者必须存在 ohos 变体才能整库编译——这是本次适配的成败手。
工具链不一致上游 Kotlin 2.2.20 / Gradle 8.14.3,社区旧钉法 Kotlin 2.2.21-1.0.0 读不了 2.4 元数据,Compose 1.12 线要求 Kotlin 2.4.20-0.1.0-07 与 Gradle 9.x。
无组合宿主OHOS 目前没有 Compose composition 宿主,rememberRippleIndication 返回的 Modifier 节点无法在无组合环境驱动,涟漪动画语义跨不过语言边界。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:上游库文件的原样 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。库的参数模型与 Material3 默认值完全由上游原版代码提供,涟漪时序由 Demo 状态机按 Android RippleDrawable 行为复刻,ArkTS 只负责调用 Native、解析 JSON 和绘制视觉。

1.2 库提供的能力

Demo 提供一个真实的涟漪会话,对应桥接的全部入口:

  • 默认值(defaults):查询库的 Material3 默认 RippleAlpha(dragged 0.16 / focused 0.10 / hovered 0.08 / pressed 0.10)、默认 bounded=true、颜色与半径未指定——数值来自编译进 .so 的 Material3DefaultRipple / Material3RippleAlpha() 运行时调用,而非静态文字;
  • 配置(config):color(#RRGGBB / #AARRGGBB,空 = 未指定)、bounded、radiusDp(≤0 = 自动)、density、四态 alpha 逐项覆盖,负数或缺省回退库默认,响应回显归一化结果;
  • 几何(geometry):给定触点与容器尺寸,返回目标半径(最远角距离或 radiusDp×density)、最远角方位与 dx/dy,纯查询不动状态;
  • 涟漪会话(down / frame / up / cancel):按下开始涟漪,ArkTS 以 16ms 轮询 frame 取 phase / radius / alpha,抬起进入 375ms 淡出,移出以 150ms 快速取消。

frame 响应字段:

字段作用
phasepress / release / cancel / idle,状态机当前相位。
elapsedMs自按下起经过的毫秒数。
radiusPx / alpha当前帧涟漪半径与透明度,ArkTS 直接用于绘制。
centerX / centerY / targetRadiusPx触点中心与目标半径。
runningfalse 表示本轮涟漪结束,ArkTS 停止轮询并清空。

库侧完整能力(rememberRippleIndication 的 remember 语义、MaterialRippleNodeFactory 的相等性、RippleAlpha 不可变模型)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 compileKotlinJvm 编译级交叉验证,证明 ohos .so 里编译的库代码没有漂移。

1.3 实现适配

维度要求
代码复用ripple-indication 的全部 commonMain 文件原样编译,不提取、不重写;全仓零新增库源码文件(连 expect/actual 都没有——这是与 KStore 适配最大的不同)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作编译级交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 demo(AGP + 热重载 + wasm)与 dist/(web 产物)留盘不进构建。
桥接稳定2 个 C ABI + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与上游完全一致(composeJvm 1.9.1 即上游自己的线),只把 ohos 侧仓库换到社区 Compose 制品线(换线不换版原则的 per-target 拆分版)。
UI 完整单页三 Tab 覆盖全部桥接入口:涟漪演示(配置 + 触摸 + 逐帧渲染 + 日志)、引擎(握手 + 默认值 + 几何探针 + 时序)、关于(链路 + 社区)。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit,全文不出现任何其他代码托管平台字样(上游 GitHub 链接仅保留在文档的"上游"字段中)。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现——OHOS 尚无 Compose composition 宿主,涟漪视觉由 ArkUI 复刻,动画相位/半径/透明度逐帧来自 Kotlin 侧状态机;这是 qrose / eungabi / Vico / Reorderable / compose-shimmer 一脉已过审的社区路线。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、验证 material-ripple ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、双线 Compose 依赖拆分
第 3 阶段:桥协议建模     ── 7 op 扁平 JSON 契约、真实库对象作数据源、涟漪时序状态机设计
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 版本握手、defaults 加载、16ms 轮询渲染、三 Tab 与错误边界
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
ripple-indication/                  上游库(进 ohos 构建,jvm + ohosArm64 + ohosX64,源码零改动)
example/nativeApp/                  Kotlin/Native 动态库(Bridge.kt:@CName × 2 + MiniJson)
example/harmonyApp/                 DevEco Stage、CMake N-API 和 ArkTS 页面
demo/ dist/                         上游 web/桌面 Demo 与构建产物,留盘不进 settings
README.OpenHarmony.md / _CN.md      鸿蒙适配说明(中英双份)
README.upstream.md                  上游原文保存
*.upstream                          上游构建文件备份(settings / build / properties / catalog / 库构建)

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :ripple-indication 与 :example:nativeApp,rootProject.name = "ohos_compose_ripple_indication"。上游的 nexus 发布插件、AGP classpath、compose-hotreload、wasm 实验仓库全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
Compose(ohos 线)社区 org.jetbrains.compose.* 1.12.0-0.1.0-07(ohos klib)
Compose(jvm 验证线)1.9.1(Maven Central,上游 1.2.0 自己构建所用的线)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作编译级交叉验证)
Gradle9.6.1(上游 wrapper 8.14.3 升级;2.4.20 线支持 Gradle 9.x)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.ripple
$env:JAVA_HOME = "F:/cmp/.tools/jdk-21.0.12.1+1"
java -version   # openjdk 21
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
ripple-indication1(RippleIndication.kt)compose.foundation + material-ripple,零 expect/actual整体编译(jvm + 双 ohos)
demo3 平台源集AGP + hot-reload + wasm + lucide 图标留盘不进构建

上游全库只有 1 个源文件,expect/actual 数量为 0——比 KStore(1 处 expect,补 8 行 actual)的适配面还小。唯一的问题是这个文件整体站在 Compose API 上,因此成败手从"补 actual"前移到"material-ripple 有没有 ohos 制品"。

1.4 页面边界
标题区          RIPPLE INDICATION · OPENHARMONY / 版本握手行(来自真实 Native 调用)
Tab 切换        涟漪 / 引擎 / 关于
涟漪 Tab        颜色预设色块(默认灰/紫/青/琥珀)+ 自定义 hex 输入、bounded 开关、
                radiusDp 滑块(0–48,0 显示"自动")、四态 alpha 滑块(初始取 defaults 真实值)、
                大卡片 + 按钮行两个触摸目标、操作日志(最新在前,上限 50 条)
引擎 Tab        defaults 面板、config 回显、几何探针(点按显示目标半径/最远角/dx/dy)、时序说明
关于 Tab        库简介、五层架构链路、社区与仓库链接、MIT

页面启动时先调 rippleVersion() 做握手,再取 defaults 填充四个 alpha 滑块初值,并通过 display.getDefaultDisplaySync().densityPixels 实测屏幕密度(本机 3.50)注入 config。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上 Compose 鸿蒙线有没有 material-ripple。用 .module 元数据直接验证:

org.jetbrains.compose.material:material-ripple:1.12.0-0.1.0-07
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published /
            desktopApiElements-published / metadataApiElements / ...
  ohos 变体依赖闭包: animation / collection / foundation / runtime / ui-util / kotlin-stdlib

两个关键结论:其一,material-ripple 在 1.12.0-0.1.0-07 线上发布了 ohosArm64 与 ohosX64 双 klib,依赖闭包全部是 Compose 标准模块(由 foundation 传递解析)——整库编译的最大障碍不存在;其二,这条 Compose 线不发布 *-desktop 子制品,jvm 交叉验证目标不能从这条线解析,必须按目标拆分依赖(同 compose-shimmer / eungabi 的拆法):ohos 走社区线,jvm 走 Maven Central 上游线 1.9.1,“换线不换版”。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。但 Compose 1.12.0-0.1.0-07 制品基于 Kotlin 2.4.20 构建,旧钉法读不了 2.4 元数据。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。额外好处:compose 编译器插件与 Kotlin 同版本号(org.jetbrains.kotlin.plugin.compose),一行 catalog 别名即可。

Gradle 方面,上游 wrapper 是 8.14.3;2.4.20 线按社区先例(compose-shimmer 同机同线验证过)升到 9.6.1,wrapper 的 gradle-wrapper.jar 与 gradle-wrapper.properties 一起同步升级。

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_compose_ripple_indication"

include(":ripple-indication")
include(":example:nativeApp")
2.3 编译真实引擎

ripple-indication/build.gradle.kts 收敛为纯 KMP 模块,Compose 依赖按目标线声明:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlin.compose.compiler)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvm()                    // 交叉验证用(上游线 1.9.1)
    ohosArm64()
    ohosX64()

    sourceSets {
        ohosMain.dependencies {
            api(libs.compose.runtime)            // 1.12.0-0.1.0-07
            api(libs.compose.foundation)
            api(libs.compose.material.ripple)
        }
        jvmMain.dependencies {
            api(libs.compose.runtime.jvm)        // 1.9.1(Maven Central)
            api(libs.compose.foundation.jvm)
            api(libs.compose.material.ripple.jvm)
        }
    }
}

实测(Windows / JDK 21,全绿):

./gradlew :ripple-indication:compileKotlinJvm          # BUILD SUCCESSFUL
./gradlew :ripple-indication:compileKotlinOhosArm64 :ripple-indication:compileKotlinOhosX64
                                                       # BUILD SUCCESSFUL,整文件零改动

RippleIndication.kt 一个字节没动:rememberRippleIndication、RippleAlpha、Material3DefaultRipple、Material3RippleAlpha()、MaterialRippleNodeFactory 全部原样进入 ohos KLIB。

2.4 测试的处置:上游没有测试源

上游仓库不存在 commonTest,因此交叉验证是编译级的:compileKotlinJvm 用上游自己的 Maven Central 线(compose 1.9.1 / material-ripple 1.9.1)编译同一份源码,证明"换线不换版"下源码与两条制品线都兼容。根构建同时全局禁用 ohos 测试任务(无设备也不需要):

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

第 3 阶段:桥协议建模

3.1 JSON 契约
com.composables.compose.ripple/**(上游库原样编译)
        ↓ RippleAlpha / Material3DefaultRipple / Material3RippleAlpha()
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API rippleVersion() / rippleDispatch()
        ↓
ArkUI Index.ets(三 Tab 单页)

请求与响应(7 个 op):

请求 : {"op":"defaults"}
       {"op":"config","color":"#FF6750A4","bounded":true,"radiusDp":0,"density":3.5,
        "draggedAlpha":-1,"focusedAlpha":-1,"hoveredAlpha":-1,"pressedAlpha":-1}
       {"op":"geometry","x":120,"y":48,"width":344,"height":96}
       {"op":"down","x":120,"y":48,"width":344,"height":96}
       {"op":"frame"} / {"op":"up"} / {"op":"cancel"}
响应 : {"ok":true,"op":"frame","phase":"press","elapsedMs":83,"radiusPx":104.7,
        "alpha":0.1,"centerX":120,"centerY":48,"targetRadiusPx":210.5,"running":true}
       {"ok":false,"op":"...","error":"..."}

三个不对称设计:config 的 alpha 传负数或缺省表示"用库默认",桥在 Kotlin 侧调用真实的 Material3RippleAlpha() 兜底并钳制到 0…1;geometry 是纯函数查询,与涟漪会话状态解耦,引擎 Tab 的几何探针不动任何状态;frame 是拉模式——ArkTS 每 16ms 拉一次当前帧,停轮询画面即冻结,证明每一帧都来自 Kotlin 侧。

3.2 数据源为什么是真实库对象

桥内的默认值这样取:

val d = Material3DefaultRipple      // 上游原件:0.16 / 0.10 / 0.08 / 0.10
val base = Material3RippleAlpha()   // 上游原件:带默认参数的工厂函数
config = RippleConfig(
    colorArgb = ..., bounded = ..., radiusDp = ..., density = ...,
    alphas = RippleAlpha(
        draggedAlpha = json.optAlpha("draggedAlpha", base.draggedAlpha),
        ...
    ),
)

RippleAlpha / Material3DefaultRipple / Material3RippleAlpha() 全部 import 自 com.composables.compose.ripple——也就是编译进同一个 .so 的上游代码。默认值与归一化语义在运行期由库对象给出,桥上不重抄任何数字。

涟漪时序则诚实复刻:上游把动画委托给 material-ripple 的 createRippleModifierNode,该节点需要 composition 才能运行,无组合环境驱动不了。Demo 状态机按 Android RippleDrawable 行为实现四段时序——淡入 75ms、扩散 300ms(减速缓动 1−(1−p)²)、抬起淡出 375ms、取消淡出 150ms,alpha 峰值取配置的 pressedAlpha。真实与复刻的边界在 README 与本文反复标明。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("RippleDispatch")
fun rippleDispatch(request: String): String = try {
    val json = MiniJson.parse(request)
    when (json.str("op")) { ... }
} catch (t: Throwable) {
    err("dispatch", t.message ?: t.toString())
}

非法 op、缺 x/y/width/height、width/height ≤ 0、颜色串不合法,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进操作日志。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libripple_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosripple.so)
  │ RippleDispatch / RippleVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只传静态文字又证明不了库在跑。所以选择 C ABI + JSON,数据源全部是运行期库调用。

4.2 Kotlin/Native 导出函数
@CName("RippleVersion")
fun rippleVersion(): String = "1.2.0-ohos.1"   // 版本握手

@CName("RippleDispatch")
fun rippleDispatch(request: String): String    // 7 op 一口进
C ABI 符号ArkTS 方法覆盖内核
RippleDispatchrippleDispatch(req)defaults 查询 + config 归一化 + geometry + down/frame/up/cancel 会话
RippleVersionrippleVersion()fork 版本握手(真机版本行的数据来源)

libohosripple.so 用 llvm-nm -D 验证两符号均为全局 T(arm64:RippleDispatch @ 0x10615b0、RippleVersion @ 0x1061734)。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"rippleDispatch", nullptr, NapiRippleDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"rippleVersion", nullptr, NapiRippleVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosripple_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosripple_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohosripple SHARED IMPORTED)
set_target_properties(ohosripple PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(ripple_napi SHARED napi_init.cpp)
target_link_libraries(ripple_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosripple)

OHOS_ARCH 为空或 libohosripple.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const rippleDispatch: (request: string) => string;
export const rippleVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 握手、默认值与密度注入
aboutToAppear(): void {
  this.version = rippleVersion();                     // 版本握手:真实 Native 调用
  const d = this.dispatch('{"op":"defaults"}');       // 库真实默认值 → 滑块初值
  this.pressed = d.pressedAlpha; this.dragged = d.draggedAlpha; ...
  this.density = display.getDefaultDisplaySync().densityPixels;   // 实测 3.50
  this.applyConfig();                                 // config 归一化回显
}

四个 alpha 滑块的初值即 defaults op 返回的 0.10 / 0.16 / 0.08 / 0.10——用户在真机上看到的第一眼默认值,就是编译进 .so 的上游 Material3DefaultRipple。

5.2 触摸与逐帧渲染
onTouch(event: TouchEvent) {
  const x = event.touches[0].x * this.density;        // vp → px,组件自身坐标系
  if (event.type === TouchType.Down)  this.dispatch(down(x, y, w, h)), this.startPolling();
  if (event.type === TouchType.Up)    this.dispatch('{"op":"up"}');
  if (event.type === TouchType.Cancel || 移出边界) this.dispatch('{"op":"cancel"}');
}

private startPolling(): void {
  this.timer = setInterval(() => {
    const f = this.dispatch('{"op":"frame"}');
    this.radiusPx = f.radiusPx; this.alpha = f.alpha;  // @State → Circle 重绘
    if (!f.running) { clearInterval(this.timer); this.ripple = null; }
  }, 16);
}

涟漪渲染为 Stack 内的 Circle(直径 2×radiusPx、fill 配置色、opacity alpha、位置 center±radius);bounded 时容器 .clip(true) 圆角裁剪,unbounded 不裁剪任其溢出——库的 bounded 语义在 ArkUI 侧的对照实现。

5.3 对比度和错误边界

引擎 Tab 把 defaults 四值与滑块并排展示;几何探针点按后显示目标半径、最远角方位与 dx/dy;ok:false 的错误原文进状态行与日志区。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志,深浅色主题随系统切换。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosripple)+ copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.ripple、versionName 1.2.0-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/libohosripple_api.h
        ├── src/main/cpp/types/libripple_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(三 Tab)
6.2 构建、签名和安装
./gradlew :ripple-indication:compileKotlinJvm                     # 编译级交叉验证(上游线)
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp && ohpm install
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

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.ripple -m entry

四、完整代码对照

4.1 整体架构

com.composables.compose.ripple/**(上游原样编译,零改动)
    │ rememberRippleIndication / RippleAlpha / Material3DefaultRipple
Bridge.kt + MiniJson
    │ RippleDispatch / RippleVersion
libripple_napi.so
    │ rippleDispatch / rippleVersion
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(三 Tab 单页)
    ▼
社区 Compose 1.12.0-0.1.0-07(runtime / foundation / material-ripple,ohos klib)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_compose_ripple_indication、demo 移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + Compose 双线(ohos 1.12.0-0.1.0-07 / jvm 1.9.1)钉版
build.gradle.kts(根)版本 1.2.0-ohos.1、全局禁用 Ohos*Test* 任务
ripple-indication/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,Compose 依赖按目标线声明
example/nativeApp/build.gradle.kts双 ABI sharedLib、copyKnSo* 回填 entry/libs
nativeApp/.../Bridge.kt2 个 C ABI、7 op、真实库对象数据源、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(涟漪 / 引擎 / 关于)
README.OpenHarmony_CN.md鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
库rememberRippleIndication(color, bounded, radius, rippleAlpha)一行接入涟漪 Indication(进 .so,宿主出现后直接可用)
库RippleAlpha(dragged, focused, hovered, pressed)四态透明度不可变模型(桥的配置载体)
库Material3RippleAlpha() / Material3DefaultRippleMaterial3 默认值(defaults op 与 config 兜底的数据源)
库MaterialRippleNodeFactorycreateRippleModifierNode 的包装(随编译进 .so,无组合环境不驱动)
NativeRippleDispatch / RippleVersionC ABI JSON 导出
N-APIrippleDispatch(req) / rippleVersion()向 ArkTS 暴露能力
ArkTSonTouch + 16ms 轮询触发会话并逐帧重绘

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(rippleDispatch(JSON.stringify({
  op: 'down', x: 120, y: 48, width: 344, height: 96
})));
// res.ok === true;随后每 16ms:
const f = JSON.parse(rippleDispatch('{"op":"frame"}'));
// f.phase === 'press',f.radiusPx / f.alpha 直接驱动 Circle
Engine.start(g.x, g.y, g.targetRadiusPx)          // 记录触点、目标半径与单调时钟
"frame" -> Engine.frame()                          // 相位机:press→release/cancel→idle
           // radius = target * (1-(1-p)²),alpha = pressedAlpha * 淡入系数

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:整库真编译,连内核都不用抽

早期仓库抽内核,是因为 ohos 上没有它们依赖的制品。本次适配先验证制品线(material-ripple 的 ohosArm64/ohosX64 klib 已在 1.12.0-0.1.0-07 发布),障碍已消失——再抽内核等于把一个 108 行的文件手抄一遍,反而引入漂移风险。真编译让 rememberRippleIndication 的 remember 语义、RippleAlpha 模型、Material3 默认值全部是上游原件,全仓零新增库源码文件。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 升 9.6.1

Compose 1.12.0-0.1.0-07 线基于 Kotlin 2.4.20 构建,旧钉法 2.2.21-1.0.0 读不了 2.4 元数据。Gradle 沿上游 wrapper 升到 9.6.1(同机 compose-shimmer 已验证的同款组合),gradle-wrapper.jar 与 properties 同步升级,避免"新 properties 旧 jar"的引导问题。

决策 3:根构建替换(cmp_qrose 模式),demo 与 dist 留盘

上游根构建(nexus 发布、AGP、hot-reload、wasm 实验仓库)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。demo 需要 AGP + material3 + lucide 图标 + wasm 工具链,社区线不发布这些制品,留盘不进 settings.gradle.kts——上游桌面/Web Demo 的存在方式只有留盘这一种。

决策 4:参数模型走真实库对象,时序复刻 RippleDrawable 并诚实标注

桥的默认值、归一化、几何全部来自编译进 .so 的上游代码;唯一时序(75/300/375/150ms 与减速缓动)由 Demo 状态机按 Android RippleDrawable 行为复刻——因为上游把动画委托给 material-ripple 的 Modifier 节点,该节点需要 composition 才能运行。真实与复刻的边界在 README(中英)与本文 3.2 节明确标注,不冒充"上游动画"。

决策 5:编译级 JVM 交叉验证(上游无测试源)

上游没有 commonTest,KStore 式"跑上游测试文件"的闸门在此不适用。退而求其次:compileKotlinJvm 用上游自己的 Maven Central 线(compose 1.9.1)编译同一份源码,证明"换线不换版"下源码零改动兼容两条制品线;ohos 侧测试任务全局禁用。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21、DevEco Studio 5.1.0 Release(含 OHOS SDK / hvigor / ohpm / hdc)、Gradle 9.6.1 与鸿蒙真机(arm64,打孔屏)。

6.2 JVM 交叉验证

./gradlew :ripple-indication:compileKotlinJvm

上游源文件零改动,同时通过上游线(1.9.1)与社区线(1.12.0-0.1.0-07)的编译——createRippleModifierNode、IndicationNodeFactory 等 API 在两条线上签名一致,"换线不换版"的直接证据。

6.3 HAP 验证

:ripple-indication:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填)
llvm-nm -D: RippleDispatch / RippleVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libripple_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL in 4s 683ms
install bundle successfully / start ability successfully

entry-default-unsigned.hap 约 41.3 MB,内含双 ABI 各三件套:arm64 的 libripple_napi.so(65 KB)+ libohosripple.so(strip 后约 19.2 MB,debugShared 未 strip 约 27.8 MB,含 Kotlin/Native 运行时与 Compose)+ libc++_shared.so(约 1.3 MB),x86_64 同构。

6.4 功能用例

  1. 启动后引擎/关于 Tab 出现 rippleVersion() → 1.2.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. 涟漪 Tab 四个 alpha 滑块初值显示 0.10 / 0.16 / 0.08 / 0.10,与 defaults op 返回一致——即库的 Material3DefaultRipple 真实值;
  3. 屏幕密度经 display.getDefaultDisplaySync().densityPixels 实测注入(本机 3.50),配置回显区可见;
  4. 按下大卡片:涟漪自触点淡入(75ms)并以减速缓动扩散(300ms),峰值透明度为 pressedAlpha;
  5. 抬起:涟漪继续补完扩散并按 375ms 淡出,running:false 后轮询停止、圆清空;
  6. 按住拖出卡片边界:走 cancel 路径,150ms 快速淡出;
  7. 切换 bounded 开关:有界时涟漪被圆角容器裁剪,无界时溢出边界;
  8. radiusDp 滑块从"自动"调到具体值:涟漪按 radiusDp×density 固定半径,引擎 Tab 几何探针的目标半径同步变化,最远角方位随触点移动正确翻转;
  9. 切换颜色预设(默认灰 / 紫 / 青 / 琥珀)与自定义 hex:涟漪颜色实时生效;
  10. hovered / focused / dragged 三个状态层 Chip:以对应 alpha 叠加纯色遮罩,数值来自 config 归一化响应。

6.5 验证结论

库本体、社区 Compose 制品线、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,涟漪 Tab 配置面板完整呈现,按下/抬起/取消全时序、bounded 裁剪、自定义半径与四态 alpha 均按预期工作(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-ripple-indication.git
cd compose-ripple-indication
export JAVA_HOME="/path/to/jdk-21"
./gradlew :ripple-indication:compileKotlinOhosArm64 :ripple-indication:compileKotlinOhosX64   # klib
./gradlew :ripple-indication:compileKotlinJvm                                               # 编译级交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp && ohpm install
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.ripple -m entry
hdc shell hilog | grep RippleNapi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :ripple-indication 真正编译成 ohosArm64 / ohosX64 目标——哪怕库只有一个文件;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Compose 制品线不发布 *-desktop 子制品:jvm 目标不能引用社区线版本号,必须按 ohosMain / jvmMain 拆分声明(ohos 1.12.0-0.1.0-07、jvm 1.9.1),否则 jvm 编译解析失败;
  4. Kotlin 2.4.20 线配 Gradle 9.6.1:wrapper 的 gradle-wrapper.jar 与 gradle-wrapper.properties 必须一起升级,只改 properties 会用旧 jar 引导新发行版;
  5. 命令行跑 hvigor 三个连环坑:NODE_HOME is not set(设 DevEco 自带 node);hvigorw.bat 对含空格路径(E:\DE\DevEco Studio)处理有缺陷,绕过 bat 直接 node tools/hvigor/bin/hvigorw.js 即可;PackageHap 阶段 spawn java ENOENT,把 JDK 21 的 bin 加入 PATH(POSIX 风格路径)后通过;
  6. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组且 product 引用默认签名,勾选自动签名后由 IDE 回填完整配置;
  7. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(napi so / KN so / libc++)缺一不可;
  8. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  9. 根 .gitignore 的 /build/ 是根锚定,盖不住嵌套目录——example/harmonyApp/ 的 oh_modules、.hvigor、.idea、entry/libs、entry/.cxx 要单独追加,否则混进提交;
  10. 签名材料由 DevEco 自动写入 build-profile.json5,提交前检查,证书绝不进入 AtomGit。

8.2 已知问题

  • OHOS 无 Compose composition 宿主:Demo 视觉为 ArkUI 复刻(默认值 / 归一化 / 几何真实来自 Kotlin 侧库代码,时序为复刻实现);
  • 涟漪时序(75/300/375/150ms)不在上游库代码内(上游委托给 material-ripple 节点),Demo 状态机按 Android RippleDrawable 行为实现,数值以平台文档为准;
  • 上游无测试源,JVM 交叉验证为编译级而非测试级;
  • frame 为拉模式轮询(16ms),单涟漪会话;多指并发按下时后者取消前者;
  • ArkTS 编译存在 px2vp / vp2px deprecation 与个别 API 的 SDK 26 兼容性警告(功能正常,后续可换 getUIContext() 系 API);
  • android / js / wasmJs / ios 目标剥离(源文件留在树里),发布插件一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.ripple 匹配的签名。

8.3 未来优化方向

  • Compose composition 宿主落地后,commonMain 的 rememberRippleIndication() 直接接入真实节点,桥退化为兼容层;
  • 多涟漪并发会话(down 返回会话 id,frame 按 id 查询);
  • N-API 线程安全回表推送帧数据,替代 16ms 轮询;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建。

九、总结

9.1 核心难点回顾

上游 RippleIndication.kt(唯一源文件,纯 Compose API)
    → 验证 material-ripple ohos 制品线(1.12.0-0.1.0-07,成败手)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
    → 根构建替换(cmp_qrose 模式,demo/dist 留盘)
    → ohosArm64 / ohosX64 KLIB,源码零改动、零 expect/actual
    → C ABI(@CName × 2)+ 真实库对象作数据源
    → C++ N-API(DisposeString 纪律)
    → ArkTS 16ms 轮询渲染
    → ArkUI 真机三 Tab 验收(defaults / 涟漪时序 / bounded / 半径 / 几何)

9.2 封装层次

  • 库层:上游原版 rememberRippleIndication、RippleAlpha、Material3DefaultRipple、MaterialRippleNodeFactory;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 校验、触摸分发、逐帧渲染与日志;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:material-ripple ohos klib 已存在,涟漪库的正确答案就是"整库真编译 + 无头桥"——全仓零新增源码文件;路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走:Kotlin 钉版由 Compose 制品线的元数据反推(2.4.20-0.1.0-07),Gradle 按工具链线区分(2.4.20 线用 9.x),wrapper 的 jar 与 properties 必须同步升级;
  3. 诚实标注真实与复刻的边界:参数模型、默认值、几何来自编译后的上游原件,时序是按平台行为复刻的 Demo 实现——边界写得越清楚,审核与后续维护越容易。

9.4 适配成果

当前适配已完成:

  • compose-ripple-indication 1.2.0 真实库体的双 ABI Kotlin/Native 编译(上游 commonMain 文件零重写、零 expect/actual);
  • 社区 Compose 制品线在涟漪库上的完整落地(material-ripple 1.12.0-0.1.0-07 ohos klib + runtime / foundation 传递闭包);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 三 Tab 验收页面(版本握手、Material3 默认值、涟漪全时序、bounded 裁剪、自定义半径、四态 alpha、几何探针、状态层、操作日志);
  • 编译级 JVM 交叉验证全绿(上游线 1.9.1)+ HAP 构建(双 ABI 六件套 so,约 41.3 MB)、签名、设备安装和真机验证;
  • 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

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐