开源鸿蒙平台 CMP 三方库「JellyFab」适配全流程
本文记录
JellyFab接入 OpenHarmony 的完整过程,覆盖 Compose Multiplatform 动效库盘点、工具链钉版选型、无状态内核提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI Canvas 真机页面、HAP 构建和真机验收。
JellyFab是基于 Compose Multiplatform 的胶体悬浮菜单库:点一下主 FAB,一排 mini-FAB 会从一个软软鼓起的 blob 里"挤"出来,伴随双弹跳、高斯鼓包形变、阴影联动和 scrim。上游把这套果冻质感全部写在 Compose 里——弧角布局(JellyArcLayout)、36 点 blob 轮廓采样 + 高斯鼓包(JellyBlob)、交错时序(JellyPhysics)。本次适配与真实引擎整库编译路线(KStore 的"原样编译进 KLIB")不同:JellyFab 的可移植价值不在 Compose 组件而在动画数学,因此走的是 cmp_qrose / ComposePhysicsLayout 的"无状态内核"路线——把分散在 Compose 文件里的几何、缓动、时序抽成零 Compose、零协程的纯 Kotlin 内核编进 OpenHarmony KLIB,上游 Compose UI 原样隔离在src/composeMain作为 API 对照,ArkUI Canvas 逐帧渲染内核算出的几何作为验收宿主。全文不出现任何第三方镜像站链接,仓库地址只保留 AtomGit。
项目地址: AtomGit/oh-tpc/JellyFab.git
上游仓库: iprashantpanwar/JellyFab
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
JellyFab 为 Compose Multiplatform 应用提供"果冻悬浮菜单"这一件事的完整答案:一个 JellyFab 可组合函数覆盖主 FAB、主层 mini-FAB 弧形排布、次级链式弹出、blob 果冻形变、阴影与 scrim;JellyFabConfig 暴露交错时长、弹跳系数、阴影参数等全部动效旋钮;JellyFabState 管理展开状态机。上游把这些能力放在 commonMain,由 Android、iOS、桌面、Web、Wasm 共享。OpenHarmony 应用同样需要这套动效能力,但上游构建(Kotlin 2.2.20 / Compose 1.9.1 / AGP)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS 画几个圆圈,页面可以显示按钮,却无法证明上游的果冻几何、双弹跳时序和交错缓动在 OpenHarmony ARM64 设备上完成了编译、链接和逐帧执行。适配需要同时验证动画数学、缓动引擎、时序状态机、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.2.20 / Compose Multiplatform 1.9.1 / AGP 与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| Compose 边界 | JellyFab 依赖 graphicsLayer、Modifier、ImageVector、createBlurPaint expect/actual——社区 CMP 工具链版本对不齐,强行编译等于再打一套 Compose 发行版。 |
| 数学埋在 UI 里 | 果冻几何、缓动、时序分散在 JellyArcLayout / JellyBlob / JellyPhysics 等 Compose 文件中,必须手工抽离。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 动画语义跨不了桥 | 上游动画由 Animatable 驱动,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:无状态内核的 ohos 编译、Compose UI 的 composeMain 隔离(cmp_qrose 模式)和 C ABI/N-API 桥接层。动画语义由"无状态帧函数"解决——上游 Animatable 白拿的"从哪个状态动画过来"语义,用一对显式布尔让宿主传入。
1.2 库提供的能力
Demo 提供一个真实的逐帧动画会话,对应桥接的全部入口:
- 版本握手:
JellyFabVersion()返回 fork 版本、引擎标识与轮廓采样数,来自真实 Native 调用而非静态文字; - 舞台初始化:
JellyFabInit(request)接收画布物理像素、密度、primary/secondary 数量与全部动效参数覆盖,返回主 FAB 中心、层距、blob 基础半径、两层目标点位与四组时长; - 逐帧求值:
JellyFabFrame(request)对任意tMs返回完整一帧——blob 的 36 点轮廓、阴影度量、主 FAB 位置、两层 mini-FAB 的中心/缩放/透明度、相位与旋转角; - 内核自检:
JellyFabSelfTest()在真机上跑完整不变量清单(弧角归一、极坐标偏移、链式 lerp、轮廓半径、缓动单调性、时序交错),逐项报告通过情况; - 无状态设计:内核不持有动画时钟,
tMs = 1e9表示 snap(首帧绘制或 resize 后定格),ArkTS 用自己的帧循环驱动动画——这正是 JVM 测试和真机结果逐位一致的原因。
帧响应包含:
| 字段 | 作用 |
|---|---|
phase | collapsed / expanding / expanded / collapsing。 |
blob.points | 36 点轮廓(扁平 [x0,y0,x1,y1,...]),含高斯鼓包形变。 |
shadow | 阴影圆心、半径、透明度与模糊 sigma,随鼓包联动。 |
primary[] / secondary[] | 每层 mini-FAB 的中心、缩放、进度与透明度。 |
rotation / scrim | 主 FAB 图标旋转角与 scrim 透明度。 |
内核侧完整能力(弧角几何、极坐标偏移、链式 lerp、双弹跳时序、cubic-bezier 与阻尼弹簧解析解)全部在 commonMain 里随编译进入 .so,JVM 侧保留内核测试交叉验证,证明 ohos .so 里执行的动画逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 数学复用 | 公式、常数(0.45 半径因子、0.6 高斯 sigma、1.1/0.08/0.1 阴影因子)与符号约定全部保持上游,不"改进"。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 composeApp Demo 留盘不进构建。 |
| Compose 隔离 | 上游 Compose UI 全部迁到 src/composeMain(含平台源码集),不是源集、不参与 ohos 编译,留作 API 对照。 |
| 桥接稳定 | 4 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 内核零第三方依赖——零 Compose、零协程、零序列化,JSON 桥用手写 MiniJson。 |
| UI 完整 | 单页覆盖全部桥接入口:Canvas 逐帧渲染、主层/次级点击命中、参数面板、内核自检。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游 Compose UI 完整保留在
composeMain,等社区compose.uiohos 制品出现后可以接回;当前 Canvas 渲染的是内核算出的同一份几何模型。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认数学埋点、确定交付物
第 2 阶段:构建替换 ── eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1、JDK 21
第 3 阶段:内核提取 ── 几何/缓动/时序/场景四个纯 Kotlin 文件 + 自检,JVM 测试锁定
第 4 阶段:原生桥接 ── @CName × 4 C ABI、MiniJson、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── Canvas 逐帧渲染、点击命中、参数面板、内核自检页
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
jellyfab/src/commonMain/ 新增:无状态内核(几何/缓动/时序/场景/自检,零 Compose 零协程)
jellyfab/src/composeMain/ 上游 Compose UI 原样迁入(含平台源码集,ohos 不编)
jellyfab/src/commonTest/ 内核 JVM 交叉验证
jellyfab/build.gradle.kts 纯 KMP:jvm() + ohosArm64 + ohosX64
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS Canvas 页面
composeApp/ 上游 Demo,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :jellyfab 与 :example:nativeApp,rootProject.name = "ohos_jellyfab"。上游根构建的 Compose Multiplatform 插件、AGP、Android 目标、发布段全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(2.2.21 旧线必须降版,不能沿用上游 wrapper) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.jellyfab |
| 版本 | 2.0.0-ohos.1(上游 2.0.0) |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
| 动画数学 | 弧角/极坐标/链式 lerp/blob 采样/时序/缓动 | 纯 Kotlin 语义,埋在 Compose 文件里 | 手工抽离成 5 个内核文件(唯一新增源码) |
| Compose UI | JellyFab / JellyBlob / JellyFabState / JellyFabItem / JellyArcLayout / 图标 | androidx.compose.ui + expect/actual | 原样迁入 composeMain,一个字节不改 |
| 平台源码集 | androidMain / iosMain / webMain / jvmMain 等 | Compose 依赖 | 迁入 composeMain/platform/ 隔离 |
与 KStore"整库真编译"路线的本质区别在这里:JellyFab 的数学不能直接编——它和 Compose 类型(Offset、Animatable、graphicsLayer)绑在同一批文件里,拆的时候要手洗。抽离纪律是把公式、常数和符号约定原样搬走,Offset 换成自带的 JellyPoint,其余一个字符都不"改进"。
1.4 页面边界
标题区 JellyFab · OHOS / 版本握手行(2.0.0-ohos.1 · jelly-kernel-pure-kotlin)
Tab 切换 Demo / About
Canvas 区 逐帧渲染:scrim → 次级层 → 主层 → 阴影 → blob 轮廓 → 主 FAB 图标
状态行 最近一次 snap/动画结果(如"settled in 2466ms · phase expanded")
参数面板 Primary / Secondary 数量加减(0..8)+ Apply 重建舞台 + Self-test
点击命中 次级 mini-FAB → 切换次级层;主 FAB → 展开/收起
关于区 版本握手、库简介、适配路线、仓库与社区链接、自检结果
页面启动时先调 JellyFabVersion() 做握手,Canvas onReady 后用物理像素宽高与密度调 JellyFabInit 建舞台。
第 2 阶段:目标与依赖打通
2.1 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
ohosArm64() / ohosX64() 只存在于 HarmonyOS Kotlin 插件,社区此前批准的钉法是 2.2.21-1.0.0。本仓内核零第三方依赖,不存在 KStore 那种"制品线 stdlib 元数据反推工具链版本"的问题,直接沿用已审钉法。代价是 Gradle 必须降到 8.14.1——只有 2.4.20 新线才支持 9.x wrapper,两条工具链线的经验不能互相套用。
settings.gradle.kts 把 eazytec 仓库写在最前(显式命名,避免与全局 init 脚本的自动命名镜像条目冲突):
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/") { name = "eazytecPublic" }
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos") { name = "eazytecHarmonyos" }
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_jellyfab"
include(":jellyfab")
include(":example:nativeApp")
2.2 编译真实内核
jellyfab/build.gradle.kts 收敛为纯 KMP 模块:
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零依赖
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
注意 composeMain 不是 Kotlin 源集,天然不参与编译——隔离靠目录约定而不是构建脚本排除。实测:
./gradlew :jellyfab:jvmTest # 内核测试全绿
./gradlew :jellyfab:compileKotlinOhosArm64 :jellyfab:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.3 双 ABI 动态库
example/nativeApp 出 sharedLib(库模块不开 binaries,避免每模块吐一份带运行时的 so):
ohosArm64 { binaries.sharedLib { baseName = "ohosjellyfab" } }
ohosX64 { binaries.sharedLib { baseName = "ohosjellyfab" } }
copyKnSo* 任务把链接产物与匹配 triple 的 libc++_shared.so 一起拷进 example/harmonyApp/entry/libs/<abi>/——KN so 的 DT_NEEDED 包含 libc++,缺一件运行期 dlopen 就失败。
第 3 阶段:内核提取(技术难点)
3.1 无状态帧函数
上游 JellyFab() 是四块 Compose 的组合:scrim Box、主层弧形、次级链、JellyBlob + 图标。它们的布局、动画状态与 z 序全部可以从 (spec, state, fromState, elapsedMs) 恢复:
主 FAB 中心 = 底端角落,padding(24.dp) 翻转
主层中心[i] = 主中心 + radialOffset(angle[i], layerDistance, progress[i])
次级[i] = 从主层[0]的*当前*中心链式 lerp
blob 轮廓 = 36 采样 + 高斯鼓包
阴影 = 鼓包影响度驱动的 spread/alpha/lift
fromState 是让无状态帧函数仍然正确的关键:"从展开态收起"和"从未展开"最终都停在 progress 0,但只有前者会从 1 动画下来。上游靠 Animatable 白拿这个语义,内核用一对显式布尔(fromExpanded / fromSecondary)让宿主传入。
3.2 五个内核文件
| 文件 | 职责 |
|---|---|
JellyGeometry.kt | 弧角归一、极坐标偏移(主层 cos/sin,次级 -sin——两套符号约定都是上游原样)、链式 lerp、36 点轮廓采样 + 高斯鼓包、阴影度量 |
JellyEasing.kt | cubic-bezier tween(FastOutSlowIn / LinearOutSlowIn)+ 阻尼弹簧解析解 |
JellyTimeline.kt | 展开/收起/次级四条交错时序与双弹跳 |
JellyScene.kt | frame(spec, state, elapsedMs, fromState) → 整帧(JellyFrame) |
JellySelfTest.kt | 不变量自检清单,可在真机执行 |
提取纪律:Offset 换 JellyPoint,其余照抄——常数 0.45(blob 半径因子)、0.6(高斯 sigma)、1.1/0.08/0.1(阴影三因子)、36(采样数)逐字保留。符号约定也是:主层 270° 是正上方,次级 90° 是正上方,两者共存是上游原状,不是 bug。
3.3 JVM 测试锁定
内核语义用 commonTest 锁定:弧角序列(count=3 → [270°,225°,180°])、极坐标偏移的 progress 截断、链式 lerp 的依赖次序、轮廓采样数与半径正性、缓动中段超线性、展开帧位于两布局之间、收起帧从展开布局出发、settled 语义、spec 换算(dp × density)。这份测试在 JVM 上跑,同时约束 ohos .so——同一份 commonMain 代码,测试通过即"内核没漂"。
第 4 阶段:原生桥接
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libjellyfab_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosjellyfab.so)
│ JellyFabVersion / JellyFabInit / JellyFabFrame / JellyFabSelfTest
▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class)
@CName("JellyFabVersion") fun jellyFabVersion(): String
@CName("JellyFabInit") fun jellyFabInit(request: String): String
@CName("JellyFabFrame") fun jellyFabFrame(request: String): String
@CName("JellyFabSelfTest") fun jellyFabSelfTest(): String
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
JellyFabVersion | jellyFabVersion() | fork 版本握手(真机版本行数据来源) |
JellyFabInit | jellyFabInit(req) | JellySpec + JellyLayout 构造、两层目标点位与时长 |
JellyFabFrame | jellyFabFrame(req) | JellyScene.frame 整帧求值 |
JellyFabSelfTest | jellyFabSelfTest() | JellySelfTest.run 不变量清单 |
四个入口全部 String → String,整体 try/catch,Kotlin 异常永远不跨 FFI;非法 JSON、缺字段、未初始化都走同一个 {"ok":false,"error":"..."} 通道。
4.3 MiniJson 为什么不上 kotlinx.serialization
桥的出入参是扁平 JSON。为这点东西拉序列化依赖会多出一个 KN 依赖,还要和 HarmonyOS Kotlin 插件的版本对齐。手写 MiniJson 只做解析/序列化 + 索引访问,字符串里的转义走完整状态机(含 \uXXXX),所以 About 文案里的任意字符都能安全回读。C ABI 保持 const char* -> const char*,ArkTS 侧 jellyFabFrame(string): string 永远不用改。
4.4 C++ N-API 与 CMake
每个 N-API 回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosjellyfab_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。
add_library(ohosjellyfab SHARED IMPORTED)
set_target_properties(ohosjellyfab PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(jellyfab_napi SHARED napi_init.cpp)
target_link_libraries(jellyfab_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosjellyfab)
OHOS_ARCH 为空或 libohosjellyfab.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录。
第 5 阶段:ArkUI 宿主
5.1 初始化与逐帧驱动
aboutToAppear(): void {
const info = JSON.parse(jellyFabVersion()) as VersionInfo;
this.versionInfo = `${info.name} ${info.version} · ${info.engine}`;
}
private reinit(): void {
const raw = jellyFabInit(JSON.stringify({
widthPx: this.canvasW, heightPx: this.canvasH, density: this.density,
primaryCount: this.primaryCount, secondaryCount: this.secondaryCount
}));
// @State 刷新状态行 + snapFrame 定格首帧
}
private stepFrame(): void {
const tMs = Date.now() - this.animStartMs;
const frame = JSON.parse(jellyFabFrame(JSON.stringify({
tMs, expanded: this.expanded, secondaryExpanded: this.secondaryExpanded,
fromExpanded: this.fromExpanded, fromSecondary: this.fromSecondary
}))) as FrameInfo;
this.redraw(frame); // Canvas 重绘
if (frame.settled) { /* 停帧,状态行报耗时 */ }
}
ArkTS 用 setInterval(16ms) 驱动帧循环,settled === true 时停帧——内核无状态,帧循环的节奏完全由宿主控制。
5.2 Canvas 渲染次序
每帧按 z 序画六层:scrim 半透明遮罩 → 次级 mini-FAB(透明度驱动)→ 主层 mini-FAB → 阴影圆(alpha/半径来自内核)→ blob 36 点轮廓(lineTo 闭合填充 + 白描边)→ 主 FAB 十字图标(按 rotation 旋转,收起态 45° 呈 ×,展开态 0° 呈 +)。几何全部来自内核,ArkTS 不重算任何弧度或鼓包。
5.3 点击命中与自检
触摸命中测试次序与渲染 z 序一致:先次级(alpha > 0.5 才可点)再主层最后主 FAB,半径加 8px 容差。About Tab 的 Self-test 按钮调 jellyFabSelfTest(),把每条不变量的名称与明细逐项显示——这是内核在真机上自证清白的入口。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosjellyfab)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/
│ ├── Bridge.kt @CName × 4 + 错误通道
│ └── MiniJson.kt 零依赖 JSON(继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.jellyfab、versionName 2.0.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/libohosjellyfab_api.h
├── src/main/cpp/types/libjellyfab_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI Canvas 验收页面
6.2 构建、签名和安装
./gradlew :jellyfab:jvmTest # 内核交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填完整配置)。
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.jellyfab -m entry
四、完整代码对照
4.1 整体架构
jellyfab/src/composeMain/**(上游 Compose UI,ohos 不编)
│ JellyFab / JellyBlob / JellyFabState / JellyArcLayout(API 对照)
jellyfab/src/commonMain/**(无状态内核)
│ JellyGeometry / JellyEasing / JellyTimeline / JellyScene / JellySelfTest
Bridge.kt + MiniJson
│ JellyFabVersion / JellyFabInit / JellyFabFrame / JellyFabSelfTest
libjellyfab_napi.so
│ jellyFabVersion / jellyFabInit / jellyFabFrame / jellyFabSelfTest
Index.ets
│ JSON parse + setInterval 帧循环 + Canvas 分层绘制
▼
ArkUI 真机页面(Canvas 逐帧果冻动画)
4.2 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | JellyGeometry.blobOutline(...) | 36 点轮廓 + 高斯鼓包采样 |
| 内核 | JellyTimeline.expandProgress / expandBulge | 交错展开进度与鼓包(双弹跳) |
| 内核 | JellyScene.frame(spec, state, t, from) | 无状态整帧求值 |
| 内核 | JellySelfTest.run() | 不变量自检清单 |
| Native | JellyFabVersion / Init / Frame / SelfTest | C ABI JSON 导出 |
| N-API | jellyFabVersion / Init / Frame / SelfTest | 向 ArkTS 暴露能力 |
| ArkTS | stepFrame() / redraw() | 帧循环驱动 + Canvas 分层绘制 |
4.3 ArkTS 与 Kotlin 的边界
const frame = JSON.parse(jellyFabFrame(JSON.stringify({
tMs: 120, expanded: true, secondaryExpanded: false,
fromExpanded: false, fromSecondary: false
})));
// frame.blob.points → 36 点轮廓;frame.primary[] → 主层点位
val frame = JellyScene.frame(
spec = spec,
state = JellyState(expanded = true),
elapsedMs = 120.0,
fromState = JellyState(expanded = false),
)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;几何语义只在 Kotlin 侧存在一份。
五、关键决策说明
决策 1:无状态内核而不是整库编译
JellyFab 的数学和 Compose 类型绑在同一批文件里,整库编译需要一套 ohos Compose 发行版,不现实;抽内核是唯一能把上游动画语义搬进 .so 的路。纪律是把公式、常数、符号约定原样搬走,测试锁死——而不是"重写一个果冻动画"。
决策 2:工具链沿用 2.2.21-1.0.0 + Gradle 8.14.1
内核零第三方依赖,不存在"制品线反推工具链"的问题,直接沿用社区已审钉法。代价是 Gradle 必须降 8.14.1——2.2.21 旧线不支持 9.x wrapper。这条与 KStore 的 2.4.20 线经验相反,两套钉法不能互相套用。
决策 3:根构建替换(cmp_qrose 模式),composeApp 留盘
上游根构建(Compose Multiplatform 插件、AGP、Android/iOS/js/wasm 目标、发布段)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。composeApp Demo 需要 AGP + 完整 CMP 工具链,和 HarmonyOS Kotlin 不能共存于一个 Gradle 构建,留盘不进 settings.gradle.kts——它的存在方式只有这一种。
决策 4:fromState 显式传参代替 Animatable
上游动画状态由 Animatable 持有,"从哪个状态动画过来"是隐式的。C ABI 只能同步一来一回,内核改为无状态:(state, fromState, elapsedMs) 三元组完整决定一帧。这让 JVM 测试和真机逐位一致成为可能,也让 ArkTS 帧循环完全自主。
决策 5:自检进桥
JellyFabSelfTest 把内核不变量清单做成 C ABI 导出,真机上逐项执行并回传明细。几何库没有"能读就算过"的外部判据(KStore 有落盘闭环),自检就是它的验收闸门——JVM 测试与真机自检双保险,才敢说"内核没漂"。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Eclipse Temurin)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :jellyfab:jvmTest
内核测试全部通过,钉住:
| 断言组 | 钉住什么 |
|---|---|
| 弧角序列 | evenArcAngles(count=3) → [270°,225°,180°](上/左上/左) |
| 极坐标与链式 lerp | progress 截断、链式依赖次序(次级从主层[0]当前中心生长) |
| 轮廓采样 | 36 点、半径正性、鼓包只沿鼓包角方向推 |
| 缓动 | FastOutSlowIn 中段超线性、LinearOutSlowIn 滞后、阻尼弹簧过冲 |
| 时序 | 交错推进、双弹跳峰值、settled 判定 |
| 场景 | 展开帧位于两布局之间、收起帧从展开布局出发、scrim/rotation 边界 |
| spec 换算 | dp × density 全链路(fabSize / layerDistance / secondaryRadius / blobBaseRadius) |
同一份 commonMain 代码编进 ohos .so——测试通过即真机执行的动画逻辑没有漂移。
6.3 HAP 验证
:jellyfab:jvmTest: BUILD SUCCESSFUL
:jellyfab:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/
BuildNativeWithCmake: finished(双 ABI 编出 libjellyfab_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
libohosjellyfab.so arm64 约 4.0MB、x86_64 约 3.7MB(debugShared,含 Kotlin/Native 运行时),内含双 ABI 三件套;JellyFabVersion / JellyFabInit / JellyFabFrame / JellyFabSelfTest 均为全局 T 符号(llvm-nm -D 验证)。
6.4 功能用例
- 启动后版本行出现
JellyFab 2.0.0-ohos.1 · jelly-kernel-pure-kotlin(来自真实 Native 调用,非静态文字); - Canvas
onReady后自动初始化舞台,状态行显示画布物理像素、密度与两层数量; - 点主 FAB,主层 mini-FAB 交错弹出、blob 高斯鼓包形变、阴影联动、图标旋转 45°→0°,状态行报
settled in ...ms · phase expanded; - 展开态点次级 mini-FAB,次级链从主层[0]当前中心链式生长,透明度渐入;
- 参数面板把 Primary 调到 8、Secondary 调到 5,点 Apply 重建舞台,再次展开验证满配弧形排布(见效果图);
- About Tab 点 Self-test,内核不变量逐项报告通过;
- 收起后 blob 平复、scrim 消退、图标回转 45°,帧循环自动停。
6.5 验证结论
动画内核、缓动引擎、时序状态机、Native 动态库、C ABI、N-API、ArkUI Canvas 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的逐帧果冻动画:版本握手、交错展开、次级链、鼓包形变与内核自检(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/JellyFab.git
cd JellyFab
export JAVA_HOME="/path/to/jdk-21"
./gradlew :jellyfab:jvmTest # 内核交叉验证
./gradlew :jellyfab:compileKotlinOhosArm64 :jellyfab:compileKotlinOhosX64 # klib
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
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.jellyfab -m entry
hdc shell hilog | grep JellyFab
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP/CMP 适配,必须让动画数学真正编进
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前且显式命名,否则与全局 init 脚本的自动命名镜像条目冲突,配置期直接Unresolved reference: ohosArm64;@CName需要@OptIn(ExperimentalNativeApi::class),文件头@file:OptIn一次声明最省事;- Kotlin 的
Pair<String, Double>不会自动装进vararg Pair<String, MiniJson>——桥的响应构造处必须显式包MiniJson.Num(...),编译期就会拦下; composeMain隔离靠目录约定而非构建脚本:它不是源集,天然不编译;平台源码集(androidMain / iosMain 等)要一并挪进composeMain/platform/,否则 Compose 依赖从旧路径漏进配置期;- ArkTS 接口必须与桥的 JSON 字段逐一对齐:
SelfTestInfo漏声明error字段会让CompileArkTS直接失败——失败通道(ok:false+error)是四个入口共用的契约; - 全局
vp2px在新 SDK 上已弃用,改用this.getUIContext().vp2px(...),语义不变; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,提交前保护该文件,证书绝不进入 AtomGit。
8.2 已知问题
- HarmonyOS 上没有 Compose UI:
JellyFab可组合函数仍在composeMain,Canvas 渲染的是内核几何而不是 Skia 矢量; - 模糊是近似:上游
createBlurPaintexpect/actual 的高斯模糊在 Canvas shadow 上做简化,不逐像素复刻; - 图标走 Canvas 绘制:
CloseIcon/MoreHorizontal是 ComposeImageVector,鸿蒙侧用十字线代替; - 拖拽重排与手势冲突处理未桥接:上游
JellyFabState的拖拽语义留在 Kotlin 侧; - android / ios / js / wasmJs 目标剥离(源文件留在树里),发布段一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.jellyfab匹配的签名。
8.3 未来优化方向
- 把上游
CloseIcon/MoreHorizontal换成 Canvas Path 精确复刻,替代十字线近似; - 评估 Canvas
shadowBlur之外的高斯模糊实现(离屏 + 多次模糊),逼近上游createBlurPaint视觉; - 把
JellyFabConfig的更多动效旋钮(bounceFactor / shadowOpacity / stagger)补进JellyFabInit参数面板实时调节; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 JellyFab(动画数学埋在 Compose 文件里)
→ 手工抽离无状态内核(公式/常数/符号约定逐字保留)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1)
→ 根构建替换(cmp_qrose 模式,Compose UI 隔离 composeMain)
→ ohosArm64 / ohosX64 KLIB + JVM 测试锁定
→ C ABI(@CName × 4)+ MiniJson 扁平 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS setInterval 帧循环 + Canvas 分层绘制
→ ArkUI 真机验收(交错展开 / 次级链 / 鼓包形变 / 内核自检)
9.2 封装层次
- 内核层:
JellyGeometry/JellyEasing/JellyTimeline/JellyScene纯 Kotlin,零 Compose 零协程零依赖; - 隔离层:上游 Compose UI 原样保留在
composeMain,作为 API 对照与未来接回的基础; - Native 层:生成双 ABI 动态库并通过 4 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理舞台状态、帧循环、Canvas 分层渲染、点击命中与自检展示;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 动效类 CMP 库的适配路线由"数学和 UI 的耦合度"决定:数学独立(KStore)走整库编译,数学埋在 Compose 文件里(JellyFab)走无状态内核抽离——路线选择必须基于耦合度盘点,而不是历史结论;
- 无状态化是跨 FFI 动画的唯一出路:把
Animatable的隐式动画起点变成显式fromState传参,帧函数才能既无状态又正确,JVM 与真机才能逐位一致; - 几何库没有外部判据,自检就是验收闸门:把内核不变量做成 C ABI 导出在真机执行,配合 JVM 测试双保险,"内核没漂"这句话才成立。
9.4 适配成果
当前适配已完成:
- JellyFab
2.0.0(2.0.0-ohos.1)动画内核的双 ABI Kotlin/Native 编译(公式、常数、符号约定与上游逐字一致,仅载体从 Compose 类型换成JellyPoint); - 无状态内核五件套(几何 / 缓动 / 时序 / 场景 / 自检)+ JVM 交叉验证全绿;
- 4 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI Canvas 验收页面(逐帧渲染、两层点击命中、参数面板、内核自检、版本握手);
- 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)