开源鸿蒙平台 CMP 三方库「JetLime」适配全流程
本文记录
JetLime接入 OpenHarmony 的完整过程,覆盖 CMP 时间线库盘点、ohosArm64/ohosX64双目标、Kotlin/Native 动态库、drawBehind几何复刻、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建、签名和真机验收。
JetLime是 Compose Multiplatform 生态的时间线(Timeline)库,对外 API 是JetLimeColumn/JetLimeRow两个列表容器加JetLimeEvent/JetLimeExtendedEvent事件节点:EventPosition.dynamic()决定 Start / Middle / End 位置链,PointPlacement三档锚点配合pointStartFactor = 1.1的几何公式把节点和连接线画在drawBehind里,分页版本还有保持线条连续的dynamicPaginated()规则。本次适配走的是 cmp_qrose / haze / miuix 已过审的「参数模型内核复刻」路线:上游commonMain十三个源文件全部锚定androidx.compose,与社区 HarmonyOS Kotlin 2.2.21-1.0.0 工具链无法同构建,于是把取值语义与布局几何以纯 Kotlin 复刻进example/nativeApp,同一份源码在 JVM 上跑 36 项钉值测试、在 ohos 双 ABI 上跑真机渲染,ArkUI 页面作为验收宿主——鸿蒙侧解析出的位置序列、锚点坐标、描边半径与 Android/iOS/Desktop 完全同源。
项目地址: AtomGit/oh-tpc/JetLime
上游仓库: pushpalroy/JetLime
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
JetLime 为 Compose 应用提供垂直 / 水平时间线:开发者用 JetLimeColumn(itemsList, style = JetLimeDefaults.columnStyle()) 搭起容器,每个条目用 JetLimeEvent(style = JetLimeEventDefaults.eventStyle(position = position)) 渲染节点与连接线,即可获得点填充 / 空心 / 自定义图标三种节点、START / CENTER / END 三档锚点、纯色 / 渐变 / 虚线线条、RTL 镜像与内置分页。KMP 项目把这些参数语义与几何公式放在 commonMain,由 Android、iOS、Desktop、Web 宿主共享渲染。OpenHarmony 应用同样需要这套能力,但默认构建不会生成 OpenHarmony 的 Kotlin/Native 变体。
如果只在 ArkTS 里手写一条竖线加几个圆,页面能看,却无法证明公共 Kotlin 实现已经进入 OpenHarmony ARM64 设备完成编译、链接和执行,也无法保证跨端几何一致。适配需要同时验证 KMP 模型内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB 与动态库。 |
| 工具链不一致 | 上游 Kotlin 2.4.0 / Compose 1.10.3 / AGP 8.13.1;社区 HarmonyOS Kotlin 2.2.21-1.0.0 配 CMP 1.9.2。两条插件线不能共存于同一个 Gradle 构建。 |
| 源码面 100% Compose | 通读 jetlime/src/commonMain 的 13 个源文件:EventPosition / EventPointType 这些「模型」文件也挂着 @Immutable / @Stable / Color / Painter,没有可整体搬走的纯 Kotlin 文件。 |
| 几何原语不同 | 上游节点与连接线画在 Modifier.drawBehind 里(drawCircle / drawLine / Stroke);Kotlin/Native 共享库里没有 Skia,也没有 DrawScope。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在 KMP 目标配置、参数与几何模型内核和 C ABI/N-API 桥接层。取值语义与布局公式仍由公共 Kotlin 实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
真机 Demo 提供四个可观察场景(Tab):
- 垂直时间线:
VerticalAlignment左 / 右换边、PointPlacement三档锚点、EventPointType四种节点(Filled / Empty / ✓ 图标 / ⇄ 图标)、点半径与描边滑条、填充百分比滑条; - 水平时间线:
HorizontalAlignment上 / 下换边、RTL 镜像开关(时间线方向整体翻转)、内容卡片宽度滑条; - 样式实验室:
lineSolidBrush/lineGradientBrush切换、线宽、dashPathEffect虚线 on / off 双参、contentDistance/itemSpacing间距、脉冲缩放采样(1.0–1.2)、明暗主题、事件数量; - 位置规则:
dynamicPaginated分页演示——已加载条目 1–12 滑条 +isLastPage开关,实时回显EventPosition位置链。
Native 侧返回的 RenderOk 包含:
| 字段 | 作用 |
|---|---|
ok / error | 调用结果;Kotlin 异常永不跨语言边界,统一转 JSON 错误。 |
width / height / bytes / pngBase64 | 画布尺寸、PNG 字节数与完整渲染帧,供状态行对账与 PixelMap 刷新。 |
positions | EventPosition 解析序列(如 Start,Middle,Middle,End),分页规则直接可见。 |
events[i] | 每个事件的 pointX / pointY / radius / fillRadius / strokeRadius / segments,与上游 drawBehind 公式逐位对账。 |
resolved | 解析后的生效参数(16 / 8 / 4dp 默认值、填充 0.5、animFactor 等)。 |
共享层还提供三类交叉验证:模型钉值(默认值表、动画参数、pointStartFactor)、几何钉值(锚点公式、分段区间、RTL 镜像、虚线相位、渐变插值)、协议回显(空请求回显全部默认值、非法色值转 ok:false、density 换算)。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | JetLime 4.3.0 参数与几何模型以纯 Kotlin 复刻进 ohosArm64/ohosX64,每个值注明上游出处并由 JVM 测试钉住。 |
| 平台目标 | example/nativeApp 独立消费工程启用 ohosArm64 + ohosX64,生成双 ABI 动态库。 |
| 桥接稳定 | 两个 C ABI 符号 + 扁平 UTF-8 JSON,不传递 Kotlin 对象地址,返回字符串由 N-API 侧即刻释放。 |
| CMP 兼容 | 上游 jetlime 模块与 sample 源码零改动;社区 CMP 工具链对齐后可直接原生编 ohos,本内核退役为对照实现。 |
| UI 完整 | 四 Tab 覆盖桥全部入口字段,每个字段至少一个可见控件与状态行回显。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。JetLime 的
JetLimeColumn/JetLimeEvent仍在仓库commonMain,继续由 Android/iOS/Desktop/Web 消费;鸿蒙 Demo 验证的是同一份参数与几何模型内核的 OpenHarmony Native 链路。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点源码集、时间线框架边界和 OpenHarmony 交付物
第 2 阶段:目标与依赖打通 ── example/ 独立 Gradle 工程、ohosArm64/ohosX64、双 ABI so
第 3 阶段:参数模型建模 ── JetLimeModel 复刻 Defaults/EventPosition/Placement + 钉值测试
第 4 阶段:几何引擎 ── TimelineEngine 平移 drawBehind 公式 + 软件光栅化管线
第 5 阶段:原生桥接 ── C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 四 Tab 时间线工作台、状态行回显、HAP、签名和真机验收
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
jetlime/ 上游库模块,源码零改动
sample/ 上游 Compose 示例,零改动
example/settings.gradle.kts 独立 OHOS 构建 rootProject "ohos_jetlime"
example/gradle/ + gradlew Gradle 8.14.1 独立 wrapper
example/nativeApp/src/commonMain/ JetLimeModel / TimelineEngine / Rasterizer / PngEncoder / MiniJson
example/nativeApp/src/ohosMain/ Bridge.kt(@CName C ABI)
example/nativeApp/src/jvmTest/ 模型钉值 + 几何 + 协议共 36 项测试
example/harmonyApp/ DevEco Stage:CMake NAPI + ArkTS 四 Tab
example 是独立 Gradle 工程:上游根构建(spotless 全仓格式化、Vanniktech 发布流、Dokka、cocoapods)一字不动,Gradle 与 Hvigor 分开执行,互不影响。这是 haze / miuix 先例确立、本仓沿用的边界——上游构建复杂时,替换式改写会砸坏整条流水线。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | OpenHarmony 构建使用 HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| Gradle | 8.14.1(example/ 独立 wrapper;上游根构建不动) |
| JDK | 21(org.gradle.java.home 写入 example/gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64 |
| DevEco product | 5.1.0(18),runtimeOS: HarmonyOS |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.jetlime |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 页面边界
标题区 JetLime · OpenHarmony / 版本握手行(桥 4.3.0-ohos.1 · 上游 4.3.0 · Kotlin 2.2.21-1.0.0)
Tab 区 垂直时间线 / 水平时间线 / 样式实验室 / 位置规则
参数区 对齐 chips、锚点 chips、节点类型 chips、半径/描边/填充/间距/虚线滑条、RTL 与主题开关
结果区 Image(PixelMap) 全宽展示 + 状态行(尺寸 / png 字节)+ 位置链 chips(位置规则 Tab)
页面启动时先做 jetLimeVersion() 版本握手并展示,再自动渲染默认帧:5 事件垂直时间线 + 线在左 + START 锚点 + Filled 0.5。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 / ohosX64
example/settings.gradle.kts 的 pluginManagement 与 dependencyResolutionManagement 都把两个 eazytec Maven URL 排在最前,否则 ohosArm64() 在官方 Kotlin 插件里不存在:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_jetlime"
include(":nativeApp")
nativeApp 只声明三个目标,sharedLib 只放这里,避免每个模块各吐一份带运行时的 so:
kotlin {
applyDefaultHierarchyTemplate()
jvm() // JVM 交叉验证目标
ohosArm64 { binaries.sharedLib { baseName = "ohosjetlime" } }
ohosX64 { binaries.sharedLib { baseName = "ohosjetlime" } }
}
2.2 构建核心库和 Native 产物
cd example
./gradlew :nativeApp:jvmTest \
:nativeApp:linkDebugSharedOhosArm64 \
:nativeApp:linkDebugSharedOhosX64
copyKnSoArm64 / copyKnSoX64 用 finalizedBy 挂在链接任务上,准备:
example/harmonyApp/entry/libs/arm64-v8a/libohosjetlime.so ≈8.5 MB
example/harmonyApp/entry/libs/arm64-v8a/libc++_shared.so ≈1.9 MB(aarch64-linux-ohos triple)
example/harmonyApp/entry/libs/x86_64/libohosjetlime.so ≈7.7 MB
example/harmonyApp/entry/libs/x86_64/libc++_shared.so ≈1.7 MB(x86_64-linux-ohos triple)
nm -gU libohosjetlime.so 可见 JetLimeRender / JetLimeVersion / libohosjetlime_symbols 均为全局 T 符号。
2.3 独立消费工程
example 不依赖上游任何 Gradle 模块,内核(模型 + 引擎 + 光栅化)就在 nativeApp/src/commonMain 里,同时被 jvm() 与两个 ohos 目标编译——同一份源码,JVM 上跑测试钉值,ohos 上跑真机。
第 3 阶段:参数模型建模(技术难点)
3.1 JSON 契约
Index.ets generate()
↓ TimelineRequest(扁平 JSON)
N-API jetLimeRender(string)
↓ const char*
@CName("JetLimeRender")
↓ TimelineEngine.render
布局(drawBehind 公式复刻)→ 光栅化(线/点/卡片)→ PNG Base64
↓ UTF-8 JSON
ArkTS Image(PixelMap) + 位置链/几何回显
页面不复制任何位置判定或几何公式,每个回显字段都由公共 Kotlin 代码生成。
3.2 复刻的数据模型
JetLimeModel.kt 把上游五个源文件的取值语义 1:1 镜像成纯 Kotlin,文件头与每个常量都注明出处:
object JetLimeModel {
const val VERSION = "4.3.0-ohos.1" // <上游>-ohos.1 约定
const val UPSTREAM_VERSION = "4.3.0"
object TimelineDefaults { // JetLimeDefaults.kt
const val CONTENT_DISTANCE_DP = 16f // ContentDistance = 16.dp
const val ITEM_SPACING_DP = 8f // ItemSpacing = 8.dp
const val LINE_THICKNESS_DP = 4f // LineThickness = 4.dp
const val LOAD_MORE_THRESHOLD = 3 // 分页预取阈值
}
object EventDefaults { // JetLimeEventDefaults.kt
const val POINT_RADIUS_DP = 12f // PointRadius = 12.dp
const val POINT_STROKE_WIDTH_DP = 2f // PointStrokeWidth = 2.dp
const val POINT_FILL_PERCENT = 0.5f // PointType = filled(0.5f)
const val POINT_ANIM_INITIAL = 1.0f // pointAnimation() 初值
const val POINT_ANIM_TARGET = 1.2f // 终值
const val POINT_ANIM_DURATION_MS = 800 // tween(800, FastOutLinearInEasing)
const val ADDITIONAL_CONTENT_MAX_WIDTH_DP = 72f
}
const val POINT_START_FACTOR = 1.1f // JetLimeStyle.pointStartFactor
}
颜色默认值取 Material 3 基线 colorScheme(亮色 primary #6750A4 / onPrimary #FFFFFF / secondary #625B71 / tertiary #7D5260,暗色 #D0BCFF 一族)。上游在组合期读 MaterialTheme,桥无法跨语言取主题,因此以基线值兜底、全部字段可覆写——Demo 观感与上游默认一致,且不锁死任何颜色。
3.3 EventPosition 两条解析规则
这是 JetLime 最有「复刻价值」的语义,两条全部复刻并钉值:
dynamic(index, listSize):上游先判listSize - 1 → END再判0 → START,所以单元素列表解析为 End 而不是 Start——照「直觉」复刻必错;dynamicPaginated(index, listSize, isLastPage):isLastPage=false时末条目保持 Middle(外向连接线继续伸向下一页);单条目待分页保持 Start(上游 commitfa8e9e2「fix: preserve START for single-item pending pages」专门修过这个);isLastPage=true时末条目正常解析为 End。
fun eventPositionDynamicPaginated(index: Int, listSize: Int, isLastPage: Boolean): String =
if (!isLastPage && index == listSize - 1) {
if (index == 0) POSITION_START else POSITION_MIDDLE
} else {
eventPositionDynamic(index, listSize)
}
真机「位置规则」Tab 把这两条规则做成了可操作的验收:关闭 isLastPage 时位置链末尾显示 Middle、单条显示 Start;打开开关末条目跳成 End,连接线终止。
第 4 阶段:几何引擎(技术难点)
4.1 drawBehind 公式逐条对照
TimelineEngine.layout() 是上游 VerticalEvent / HorizontalEvent 两个 drawBehind 块的纯 Kotlin 平移(垂直方向):
| 上游公式 | 复刻 |
|---|---|
xOffset = LEFT ? radius : size.width - radius | pointCross = left ? r : width - r |
START:yOffset = radius * 1.1 | anchor = r * POINT_START_FACTOR |
CENTER:(height - (notEnd ? spacing : 0)) / 2 | effectiveLength / 2 |
END:effectiveHeight - radius * 1.1 | effectiveLength - r * 1.1 |
START 连接线:(yOffset) → (height + yOffset * 0.1),仅 notEnd | 同,分段区间逐位一致 |
CENTER/END:0 → yOffset(notStart)+ yOffset → height(notEnd) | 同 |
内容留白:absolutePadding(radius * 2 + contentDistance) | contentStart = r * 2 + contentDistance |
描边圆:radius - strokeWidth / 2(Stroke 语义) | strokeRadius = r - sw / 2 |
填充圆:radius - radius * (1 - fillPercent) = radius * fillPercent | fillRadius = r * fillPercent |
自定义图标框:radius * 2.4 * animFactor 居中 | s = r * 2.4 * animFactor |
水平方向镜像对称(yOffset 由 HorizontalAlignment 决定),并完整复刻 RTL 分支:xOffset = size.width - logicalXOffset,START 分段的越界量从右缘 width + xShift 翻转为左缘 -xShift——这条分支在几何测试里有专门用例(锚点镜像为 boxLength - 13.2,分段翻转为 [-5.88, 58.8])。
4.2 渐变线逐事件盒重扫(上游语义细节)
上游 JetLimeDefaults.lineGradientBrush() 默认 start = Offset.Zero, end = Offset.Infinite, TileMode.Clamp。在 drawBehind 作用域里 Offset.Infinite 映射到每个事件盒子的尺寸,所以渐变是逐事件盒重扫的——每个条目都从 primary 重新渐到 tertiary,不是整条时间线一次渐变。引擎按投影公式取色:
// 事件盒坐标系内的投影:t = (x·w + y·h) / (w² + h²),w/h 为该事件盒的跨轴尺寸与盒长
private fun gradientT(event: EventLayout, cfg: Config, atMainCanvas: Float): Float
4.3 虚线相位语义
PathEffect.dashPathEffect([on, off], phase):距离 d 处 (d + phase) mod (on + off) < on 才绘制。phase 会把开头一段截短(留下 on - phase 存根),有专门测试用例钉住(on=8, off=4, phase=2 时首段为 [0, 6],第二段 [10, 18])。
4.4 软件光栅化管线
共享库里没有 Skia / DrawScope,Rasterizer.kt 提供带 1px 平滑抗锯齿的画布原语,与上游调用一一对应:
| 原语 | 对应上游 |
|---|---|
fillCircle(cx, cy, r, color) | drawCircle(color, radius, center) 填充 |
strokeCircle(cx, cy, r, sw, color) | drawCircle(style = Stroke(sw))(居中于半径) |
axisLine(fixed, from, to, thickness, color) | drawLine(上游连接线全部轴向对齐) |
segment(x0, y0, x1, y1, t, color) | 自定义图标笔划(任意角线段) |
roundedRect / hBar | 内容卡片与文字占位条 |
渲染顺序与 drawBehind 一致:先连接线,再内容卡片(卡片 + 标题条 / 正文条占位——上游 itemContent 是任意 Composable,Demo 以抽象卡片表示),最后节点(背景圆 → 填充圆 / 图标 → 描边环)。PNG 编码器沿用 cmp_qrose / haze 血统的零依赖实现(stored zlib 块),Demo 图像在几十 KB 量级。
动画是静态采样:上游 EventPointAnimation 是 infiniteRepeatable(tween(800, FastOutLinearInEasing), Reverse) 的 1.0→1.2 半径脉冲,PNG 是单帧,桥以 animFactor 参数(默认 1.0,Demo 滑条 1.0–1.3)采样脉冲过程中的一个半径系数,动画参数本身钉在模型测试里。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
ArkTS
│ 扁平 JSON string
▼
C++ N-API entry(libjetlime_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosjetlime.so)
│ TimelineEngine
▼
UTF-8 JSON + 显式 DisposeString
HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务函数挂到 ArkTS 模块,所以 ArkTS 永远只 import { jetLimeRender, jetLimeVersion } from 'libjetlime_napi.so',不 import KN so——与 cmp_qrose / haze / miuix 完全同一条规则。
5.2 Kotlin/Native 导出函数
@CName("JetLimeVersion")
fun jetLimeVersion(): String = TimelineEngine.versionJson()
@CName("JetLimeRender")
fun jetLimeRender(request: String): String = try {
TimelineEngine.render(request)
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
Kotlin 异常永不跨语言边界——跨过去是无信息崩溃,统一转 ok:false。引擎的 render() 自身也 catch-all 返回 ok:false,与 Bridge 双保险,这样该契约在 jvmTest 里可以直接验证。
5.3 C++ N-API 方法分发与内存释放
napi_property_descriptor desc[] = {
{"jetLimeRender", nullptr, NapiJetLimeRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"jetLimeVersion", nullptr, NapiJetLimeVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
C++ 层只做四件事:类型检查、字符串转换、调 C ABI、把返回字符串拷进 JS 后立刻 libohosjetlime_symbols()->DisposeString(raw) 释放——不 free,不让 ArkTS 管。
5.4 CMake 和类型声明
add_library(ohosjetlime SHARED IMPORTED)
set_target_properties(ohosjetlime PROPERTIES
IMPORTED_LOCATION ${KN_SO_DIR}/libohosjetlime.so
IMPORTED_NO_SONAME TRUE)
add_library(jetlime_napi SHARED napi_init.cpp)
target_link_options(jetlime_napi PRIVATE "-Wl,--no-undefined" "-Wl,-rpath,\$ORIGIN")
target_link_libraries(jetlime_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohosjetlime)
add_custom_command(TARGET jetlime_napi POST_BUILD # KN so + libc++ 保持在 NAPI so 旁边
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${KN_SO} $<TARGET_FILE_DIR:jetlime_napi>
...)
export const jetLimeRender: (request: string) => string;
export const jetLimeVersion: () => string;
第 6 阶段:ArkUI 宿主与交付
6.1 工程结构
example/
├── settings.gradle.kts / build.gradle.kts / gradle.properties / gradlew
├── nativeApp/
│ ├── build.gradle.kts # jvm + ohos 双 ABI + copyKnSo*
│ └── src/
│ ├── commonMain/kotlin/example/nativeapp/ # JetLimeModel / TimelineEngine / Rasterizer / PngEncoder / MiniJson
│ ├── ohosMain/kotlin/example/nativeapp/Bridge.kt
│ └── jvmTest/kotlin/example/nativeapp/ # 36 项交叉验证(13/14/9)
└── harmonyApp/
├── AppScope/app.json5 # bundle org.terminator.ohos.jetlime
└── entry/src/main/
├── cpp/{CMakeLists.txt, napi_init.cpp, include/, types/libjetlime_napi/}
├── ets/{entryability/EntryAbility.ets, pages/Index.ets}
└── module.json5 / resources/
6.2 构建、签名和安装
export JAVA_HOME="/path/to/jdk-21"
cd example
./gradlew :nativeApp:jvmTest \
:nativeApp:linkDebugSharedOhosArm64 \
:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp(不是仓库根目录!)
# File → Project Structure → Signing Configs 手动自动签名(证书不入库)
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc list targets -v
hdc -t <设备序列号> install -r \
example/harmonyApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.jetlime -m entry
四、完整代码对照
4.1 整体架构
JetLimeModel.kt
│ JetLime 4.3.0 默认值表 + EventPosition 解析规则(出处注释 + 钉值)
▼
TimelineEngine.kt
│ drawBehind 公式平移 → 布局回显 + 渲染编排 → PNG Base64 JSON
▼
Rasterizer.kt / PngEncoder.kt
│ AA 画布原语(圆/环/轴线/圆角矩形/线段)+ stored-zlib PNG
▼
Bridge.kt
│ JetLimeRender / JetLimeVersion(@CName C ABI)
▼
libjetlime_napi.so
│ jetLimeRender / jetLimeVersion
▼
Index.ets
│ JSON parse + PixelMap 刷新 + 位置链 chips
▼
ArkUI 真机页面
4.2 文件清单
| 文件 | 职责 |
|---|---|
example/settings.gradle.kts | 独立 OHOS 构建、eazytec 插件仓库、ohos_jetlime |
example/gradle.properties | JDK 21、native 三开关、version=4.3.0-ohos.1 |
example/nativeApp/build.gradle.kts | jvm + ohosArm64/ohosX64 sharedLib + copyKnSo* |
nativeApp/.../JetLimeModel.kt | JetLime 4.3.0 参数模型复刻(出处注释 + 钉值) |
nativeApp/.../TimelineEngine.kt | drawBehind 几何平移 + JSON 协议 + 渲染编排 |
nativeApp/.../Rasterizer.kt | AA 画布原语(圆 / 环 / 轴线 / 圆角矩形 / 线段 / 图标笔划) |
nativeApp/.../PngEncoder.kt / MiniJson.kt | 零依赖 PNG(stored zlib)/ 扁平 JSON 解析 |
nativeApp/.../Bridge.kt | @CName C ABI,catch-all JSON 错误 |
nativeApp/src/jvmTest/ | 36 项:钉值 / 几何 / 协议 |
harmonyApp/.../napi_init.cpp | N-API 导出、字符串转换与释放 |
harmonyApp/.../Index.ets | ArkUI 四 Tab 时间线工作台 |
README.OpenHarmony.md / _CN.md | 适配说明(中文版为主文档) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| KMP | JetLimeModel.TimelineDefaults | JetLimeDefaults:16dp / 8dp / 4dp / 阈值 3 |
| KMP | JetLimeModel.EventDefaults | JetLimeEventDefaults:12dp 半径 / 2dp 描边 / 填充 0.5 / 动画参数 |
| KMP | JetLimeModel.eventPositionDynamic | EventPosition.dynamic(先 END 后 START) |
| KMP | JetLimeModel.eventPositionDynamicPaginated | 分页位置规则(含 fa8e9e2 修复) |
| KMP | TimelineEngine.layout | 平移 VerticalEvent / HorizontalEvent 几何 |
| Native | JetLimeRender | JSON 请求 → JSON(含 pngBase64 与几何回显)响应 |
| Native | JetLimeVersion | fork / 上游 / Kotlin / 引擎握手 |
| N-API | jetLimeRender / jetLimeVersion | 向 ArkTS 暴露的两个函数 |
| ArkTS | generate | 组装请求、调桥、刷新 PixelMap 与状态行 |
4.4 ArkTS 与 Kotlin 的边界
const body: string = JSON.stringify({
orientation: this.currentOrientation(),
alignment: this.currentAlignment(),
items: this.tab === 'position' ? this.loadedItems : this.items,
pointPlacement: this.placement,
pointType: this.pointType,
density: 2,
width: horizontal ? 640 : 360,
height: horizontal ? 320 : 560,
});
const raw: string = jetLimeRender(body);
const parsed: RenderOk = JSON.parse(raw) as RenderOk;
val strokeRadius = radius - cfg.strokeWidthPx / 2f // Compose Stroke 语义
val fillRadius = radius * cfg.fillPercent // 上游填充圆公式
两者之间只传输扁平 JSON,不传输 Kotlin 对象或未校验的动态结构;位置判定、锚点公式、分段规则全部在 Kotlin 侧,ArkTS 不重写任何几何。
五、关键决策说明
决策 1:独立 example/ 构建,而不是整仓切换鸿蒙 Kotlin
上游根构建包含 spotless 全仓格式化、Vanniktech 发布流、Dokka 与 cocoapods;且上游 Kotlin 2.4.0 / Compose 1.10.3 与 HarmonyOS Kotlin 2.2.21-1.0.0 两条插件线不能共存于一个 Gradle 构建。独立构建零侵入、与上游可并行维护(haze / miuix 先例)。
决策 2:复刻参数与几何模型,而不是硬接 Compose 修饰符
JetLime commonMain 十三个文件全部锚定 androidx.compose,连 EventPosition / EventPointType 这些「模型」文件都挂着 @Immutable / Color / Painter,没有可整体搬走的纯 Kotlin 文件;有价值的正是取值语义与 drawBehind 公式。1:1 复刻 + 上游出处注释 + JVM 钉值,是对「代码复用」的诚实实现;硬接 drawBehind / Painter 等于再打一套 Compose 发行版。
决策 3:软件光栅化 + 动画静态采样,而不是等 Skia 或逐帧回放
共享库里没有 Skia / DrawScope。轴向对齐的连接线用一维覆盖率精确光栅化,圆 / 环 / 圆角矩形走 SDF 距离平滑,语义可对齐、可测试;infiniteRepeatable 脉冲以 animFactor 单帧采样(1.0–1.2 对应上游初值到终值区间),动画参数钉在模型测试里而非逐帧回放。
决策 4:JSON 作为跨语言数据契约
扁平键值对 + 手写 MiniJson,不引 kotlinx.serialization / coroutines:少一个需要与 HarmonyOS Kotlin 对齐的 KN 依赖,C 签名 const char* → const char* 永不改变,协议加字段不动 ABI。
决策 5:ArkUI 作为验收宿主,参数同源是底线
回显字段(位置链、锚点、半径、分段)必须与上游公式逐位一致;「设备上看起来对」不构成验收,jvmTest 钉值 + 状态行对账才构成。
六、测试与验证
6.1 测试环境
本次验证使用 macOS(Apple Silicon)、JDK 21、HarmonyOS Kotlin 2.2.21-1.0.0、Gradle 8.14.1、DevEco Studio 5.1.0(18) 工具链与鸿蒙真机(arm64-v8a,签名安装)。
6.2 共享样例测试
cd example && ./gradlew :nativeApp:jvmTest
36 项测试分三类全部通过:
- 模型钉值(13 项):16 / 8 / 4dp 默认、
LoadMoreThreshold=3、点半径 12dp、描边 2dp、填充 0.5、动画 1.0→1.2 @800ms Reverse、AdditionalContentMaxWidth=72dp、pointStartFactor=1.1、版本约定、dynamic三段解析、单元素→End、空列表拒绝、dynamicPaginated三条规则(含 fa8e9e2 单条 START 修复); - 几何钉值(14 项):START 锚点 = 13.2px、越界分段 13.2→73.32、盒子堆叠 72+72+64、CENTER 有效高度减间距后取半、CENTER 分段穿锚连续、END 锚点 = 50.8px、RIGHT 镜像 xOffset、内容留白 = 2r + contentDistance、水平 TOP / BOTTOM、RTL 锚点镜像与分段翻转、animFactor 缩放回显(radius 12 / fillRadius 6 / strokeRadius 11)、虚线相位语义(phase=2 时首段截为 6)、渐变三色等分插值;
- 协议钉值(9 项):版本握手四字段、空请求回显全部默认值、分页位置链两种形态、PNG 签名与 IHDR 尺寸、bytes 回显与解码字节数一致、非法色值转
ok:false、坏数字回退默认、几何坐标出现在 events 回显、density=2 时 dp 翻倍。
6.3 HAP 验证
CompileArkTS: finished
BuildNativeWithCmake / BuildNativeWithNinja: finished
PackageHap: finished
SignHap: skipped(签名配置不入库,DevEco 内自动签名)
BUILD SUCCESSFUL
未签名 HAP(≈7.9 MB)内含双 ABI 各三个原生库:
libs/arm64-v8a/libohosjetlime.so libs/arm64-v8a/libjetlime_napi.so libs/arm64-v8a/libc++_shared.so
libs/x86_64/libohosjetlime.so libs/x86_64/libjetlime_napi.so libs/x86_64/libc++_shared.so
6.4 功能用例(真机实测)
- 启动后版本握手行显示
桥 4.3.0-ohos.1 · 上游 4.3.0 · Kotlin 2.2.21-1.0.0 · jetlime-kn-rasterizer,并自动渲染默认帧(5 事件垂直时间线); - 垂直时间线 Tab:默认参数下节点为「白底 + 紫描边 + 50% 紫内圆」,连接线沿左缘贯穿;切换「线在右」线与卡片整体换边;CENTER 锚点连接线改为穿过节点中心连续;✓ / ⇄ 图标节点按 2.4×radius 图标框渲染;
- 水平时间线 Tab:线换到卡片上方 / 下方;打开 RTL 镜像后时间线方向整体翻转、START 越界段翻向左缘;
- 样式实验室 Tab:渐变模式每个条目从紫重新渐变到 tertiary 色(逐事件盒重扫);虚线开关配合 dashOn / dashOff 滑条实时改变线型;深色主题整帧切换;
- 位置规则 Tab:关闭
isLastPage时位置链末尾显示Middle、单条显示Start;打开开关末条目跳成End,连接线终止——dynamicPaginated两条规则直接可见; - 任意参数组合下状态行不出现
失败:,Native 异常显示在错误区域,HAP 不崩溃。
6.5 验证结论
Native 动态库(双 ABI + libc++)、C ABI、N-API、ArkUI 页面和签名 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实 Native 渲染的垂直 / 水平时间线、渐变与虚线线条、四类节点样式与分页位置规则,状态行回显与上游参数表逐位一致。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/JetLime.git
cd JetLime
export JAVA_HOME="/path/to/jdk-21"
cd example
./gradlew :nativeApp:jvmTest \
:nativeApp:linkDebugSharedOhosArm64 \
:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,自动签名后:
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc -t <设备序列号> install -r \
example/harmonyApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.jetlime -m entry
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须真正编译
ohosArm64/ohosX64动态库并在设备上 dlopen 成功; EventPosition.dynamic先判 END 再判 START,单元素列表是 End——照「直觉」复刻必错;dynamicPaginated的单条目 START 保留是上游专门修过的(fa8e9e2),复刻老版本语义会与 4.3.0 不一致;- 渐变
Brush.linearGradient(Offset.Zero → Offset.Infinite)在drawBehind里按事件盒作用域取尺寸,逐条目重扫,不是整线一次渐变; - Kotlin/Native 无
String.format,小数回显要手写(本仓fmt()去尾零,输出13.2而非13.20),否则协议测试对不上账; - 签名工程必须与源码分离,证书不能进入 AtomGit;模拟器是 x86_64,双 ABI 缺一不可(缺 x64 报安装错误
9568347)。
8.2 已知问题
- 动画是静态采样:
infiniteRepeatable脉冲以animFactor单帧表示,动画参数钉在测试里而非逐帧回放; - 内容卡片是抽象占位(标题条 + 正文条),上游
itemContent是任意 Composable,PNG 侧无法复刻任意 UI;事件文案在 ArkTS 层展示; - 自定义图标以程序化笔划复刻(✓ 与 ⇄ 两个 glyph,尺寸 2.4×radius 遵循上游),非上游的矢量资源本体;
- 软件光栅化无 GPU 加速,整帧 360×560 渲染毫秒级,满足演示与验收、非实时帧率场景;
JetLimeExtendedEvent的 additionalContent 双列布局未在桥中透出(模型常量AdditionalContentMaxWidth = 72dp已钉值);- HAP 安装必须使用与 bundle name
org.terminator.ohos.jetlime匹配的签名。
8.3 未来优化方向
- 接入相册 PixelMap 作为时间线内容背景(协议加
pixelData字段,不动 C 签名); - 透出
JetLimeExtendedEvent双列布局几何; - 社区已出现 Compose 1.12 鸿蒙分支(
-0.1.0线,compottie 已验证可直编),评估 JetLime 源码直接切线编译 ohos 目标,届时本内核退役为对照实现; - 在 CI 中加入 Native 链接、ELF 符号检查和 HAP 构建;
- 补充不同设备 ABI、API 版本与性能模式的验收矩阵。
九、总结
9.1 核心难点回顾
JetLime 4.3.0 参数与几何模型(commonMain 语义)
→ 纯 Kotlin 复刻 + JVM 钉值(13 项)
→ drawBehind 公式平移 + 几何钉值(14 项)
→ ohosArm64/ohosX64 Kotlin/Native
→ C ABI(JetLimeRender/JetLimeVersion)
→ C++ N-API(libjetlime_napi.so)
→ ArkTS JSON 解析 + PixelMap + 位置链回显
→ ArkUI 真机四 Tab 时间线工作台
9.2 封装层次
- KMP 层:提供 JetLime 4.3.0 默认值表、EventPosition 双解析规则、Placement 锚点与分段公式的同源语义;
- Native 层:生成双 ABI 动态库并通过两个 C ABI 符号输出 JSON + PNG + 几何回显;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理四 Tab 状态、请求组装、结果展示和错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先让参数模型在 JVM 与 ohos 双侧编译通过并钉值,再接 ArkUI;回显字段与上游公式逐位一致是验收底线;
- Compose 重度库的可复用资产往往是「取值语义 + 绘制公式」而不是某个文件——复刻 + 出处注释 + 测试钉值,好过硬接工具链;
- 用扁平 JSON 和少量 C ABI 代替跨语言对象传递,并明确字符串释放责任;分别记录自动测试、Native 链接、HAP 构建和真机运行,避免把编译成功误认为设备链路可用。
9.4 适配成果
当前适配已完成:
example/独立 OpenHarmony Gradle 构建(上游根构建零改动);- JetLime 4.3.0 参数与几何模型内核的双 ABI Kotlin/Native 编译(
libohosjetlime.soarm64/x64 + 匹配 libc++); - 两个 C ABI 符号和 C++ N-API 包装(
libjetlime_napi.so); - 36 项 JVM 交叉验证测试全部通过(模型 13 / 几何 14 / 协议 9);
- DevEco Stage ArkUI 四 Tab 验收页面;
- HAP 构建、签名、真机安装与运行验证;
- AtomGit 项目文档(
README.OpenHarmony.md/README.OpenHarmony_CN.md)与真机效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐




所有评论(0)