本文记录 tagcloud 接入 OpenHarmony 的完整过程,覆盖 3D 标签云库盘点、鸿蒙 Kotlin 工具链钉版、数学内核编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

tagcloud 是基于 Compose Multiplatform 的 3D 标签云库:FibonacciLattice 把标签均匀铺在球面上,四元数驱动整体旋转,拖拽手势被映射为球面向量间的旋转增量,标签按深度缩放与淡出。本次适配与 KStore 的"制品线齐备——真编译整库"路线不同:tagcloud 的公共 API 面就是 Compose(TagCloud Composable + rememberTagCloudState),上游快照(Compose 1.5 / Kotlin 1.9)与鸿蒙 CMP 1.9.2(HarmonyOS Kotlin 2.2.21-1.0.0)两条工具链无法共存于一个 Gradle 构建。但这个库的几何引擎恰好零依赖——Vector3 / Quaternion / FibonacciLattice 是纯 Kotlin,因此被原样编译成 OpenHarmony 双 ABI 动态库,ArkUI 页面通过 N-API 调用真实上游数学内核渲染球面标签云,ArkTS 侧零几何复制。这条路即社区早期已过审的 cmp_qrose / Vico / Reorderable 的"零依赖内核提取"路线。

项目地址: AtomGit/oh-tpc/tagcloud.git

上游仓库: oleksandrbalan/tagcloud

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

tagcloud 为 Kotlin Multiplatform 应用提供"标签云"这一件事的完整答案:TagCloud(state) { items(labels) { ... } } 一个 Composable 把 N 个标签均匀铺上球面,rememberTagCloudState 暴露旋转状态,拖拽手势实时旋转,tagCloudItemFade / tagCloudItemScaleDown 让远处的标签缩小淡出,TagCloudState.rotateBy / animateRotateTo 支持程序化旋转与动画。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套 3D 标签云能力,但上游构建(Compose 1.5 / Kotlin 1.9 / AGP 8.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画出标签,却无法证明上游几何引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证分布器、四元数旋转、球面投影、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Compose 1.5 / Kotlin 1.9 / AGP 8.1 与鸿蒙 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21-1.0.0 无法共存于一个 Gradle 构建。
依赖矩阵库的 API 面整体依赖 androidx.compose.*(runtime / foundation / ui / animation)与 coroutines,Compose 层在鸿蒙线不可解析。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
手势语义跨不了桥TagCloudState 的 Animatable / suspend 动画是 Kotlin 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:零依赖数学内核的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。球的几何完全由上游原版代码负责,ArkTS 只负责调用 Native、解析 JSON 和绘制标签。

1.2 库提供的能力

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

  • 创建(create):FibonacciLattice(count) 把 N 个标签均匀铺满单位球面(上游 TagCloudScope.items 不传坐标时的默认分布器),旋转重置为单位四元数;
  • 拖拽(drag):两次触点映射为球面向量,Quaternion.create(from, to) 求旋转增量并按全局轴左乘——与上游 TagCloud.rotateGesture + rotateBy(globalAxis = true) 同一套公式;
  • 步进旋转(rotate):按角度/轴旋转(TagCloudState.rotateBy 的桥等价物),驱动自动旋转;
  • 重置(reset):回到 Quaternion.Identity;
  • 重投影(layout):尺寸变化时保持当前旋转重新投影(旋转屏 / 折叠屏);
  • 版本握手:tagcloudVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / count / radius回显本次操作、标签数与投影半径。
rotation当前四元数 {w, x, y, z},状态行实时回显。
items全部标签的投影结果,按 z 升序(由远及近)。
items[].x/y标签中心 x = R(1+cx)、y = R(1-cy)(上游 getItemOffset 同款投影)。
items[].z/s/a深度、缩放、透明度;s/a 复刻上游 rescale 曲线(最远 0.5 / 0.25)。

引擎侧完整能力(Quaternion 的 180° 反向边界处理、Vector3.rotate 旋转矩阵展开、晶格均匀分布、深度渲染曲线)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 compileKotlinJvm 同源双编译交叉验证,证明 ohos .so 里执行的几何逻辑没有漂移。

1.3 实现适配

维度要求
代码复用tagcloud 的 3 个数学文件(Vector3 / Quaternion / FibonacciLattice)原样编译,不提取、不重写;全仓零新增 Kotlin 源文件、零 expect/actual(比 KStore 的 8 行 dispatcher actual 还少)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose 层 5 个文件 git mv 到 src/composeMain 留盘不进构建;demo / desktopdemo / androiddemo / iosdemo / build-logic / config 留盘。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律内核零第三方依赖,只需 HarmonyOS Kotlin 发行版本身;不碰 kotlinx 四件套,不碰 Compose。
UI 完整单页覆盖全部桥接入口:拖拽旋转、自动旋转、重置、16/32/64/128 数量切换、四元数状态行、操作日志和版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。tagcloud 的 Compose 层因工具链不可混编而隔离在 composeMain;ArkTS Demo 负责把桥暴露的球面几何画出来——每次手势/步进,Kotlin 引擎返回全部标签的投影、缩放与透明度,ArkUI 按由远及近的数组顺序堆叠绘制。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 Compose 隔离面、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:桥协议建模     ── 会话状态、create/drag/rotate/reset/layout JSON 契约、深度渲染曲线
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 手势转发、markAnchor 中心定位、自动旋转、四元数状态行和日志
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
tagcloud/                              上游几何内核(进 ohos 构建,jvm + ohosArm64 + ohosX64)
tagcloud/src/commonMain/               math/(Vector3、Quaternion+rotate)+ distribution/(FibonacciLattice)
tagcloud/src/composeMain/              上游 Compose 层 5 文件(TagCloud/State/Scopes/RotationModifier),留盘不进构建
example/nativeApp/                     Kotlin/Native 动态库(Bridge + MiniJson + 会话)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)
README.upstream.md                     上游原文保存
demo/ desktopdemo/ androiddemo/        上游 Compose 示例,留盘不进 settings
iosdemo/ build-logic/ config/          上游构建插件与静态检查配置,留盘不进 settings

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :tagcloud 与 :example:nativeApp,rootProject.name = "ohos_tagcloud"。上游的 detekt / spotless / vanniktech 发布插件、AGP、build-logic 组合插件全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,cmp_qrose 同款钉版)
JDK21(org.gradle.java.home 指向 Adoptium jdk-21.0.12.101-hotspot)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 从上游 8.3 升级;2.2.21 线要求 8.14.x)
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.tagcloud
export JAVA_HOME="/path/to/jdk-21"   # Windows: C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot
java -version
1.3 源码依赖度盘点
文件依赖形态处置
math/Vector3.kt纯 Kotlin(kotlin.math.sqrt),零第三方依赖整体编译(jvm + 双 ohos)
math/Quaternion.kt(含 Vector3.rotate 扩展)纯 Kotlin,含 180° 反向旋转边界处理整体编译
distribution/FibonacciLattice.kt纯 Kotlin,黄金角均匀球面分布整体编译
TagCloud.ktCompose Layout / 手势 / coroutinesgit mv 至 composeMain,不进构建
TagCloudState.ktCompose Animatable / mutableStateOf / rememberSaveablegit mv 至 composeMain
TagCloudScope.kt / TagCloudItemScope.ktCompose @Composable / graphicsLayergit mv 至 composeMain
RotationModifier.ktCompose pointerInput 拖拽手势git mv 至 composeMain

expect 声明全库 0 个:几何内核连 dispatchers 都不需要。这是本批适配里最干净的一座库——KStore 还要补 8 行 ohos dispatcher actual,tagcloud 连这 8 行都省了。上游写在 Compose 文件里的两段纯算术(投影 getItemOffset、手势球面映射 getSphereCoordinates)按 cmp_qrose 的 Rasterizer 先例在 example/nativeApp 桥内按同款公式复刻(见 3.1 节),ArkTS 侧零数学。

1.4 页面边界
标题区          TAGCLOUD · OPENHARMONY / 版本副标题(CMP 3D 标签云 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        标签云 / 关于
控制区          标签数量 chips:16 / 32 / 64 / 128
云区            正方形 Stack(aspectRatio 1),ForEach 渲染标签 Text:
                .position(引擎投影中心) .markAnchor('-50%','-50%') .scale(s) .opacity(a) .zIndex(z)
操作区          重置 / 自动旋转(30ms × 0.5° 绕 Y 步进)按钮
状态行          标签数 + 当前四元数 q(w=…, x=…, y=…, z=…)
日志区          带时间戳的操作日志(最新在前,上限 6 条)
关于区          版本握手、库简介、架构分层、操作说明和仓库链接

页面启动时先调 tagcloudVersion() 做握手,云区 onAreaChange 量出尺寸后注入半径开球。

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

2.1 边界验证(本适配的成败手)

KStore 适配的成败手是 kotlinx 四件套的 ohos 制品线;tagcloud 的成败手是另一道题——上游 API 面就是 Compose:

io.github.oleksandrbalan:tagcloud:1.1.0
  api(compose.runtime) / api(compose.foundation)   ← 上游模块声明
  上游快照:Compose 1.5 / Kotlin 1.9 / AGP 8.1
  鸿蒙线:  CMP 1.9.2 / HarmonyOS Kotlin 2.2.21-1.0.0

两条工具链无法共存于一个 Gradle 构建(cmp_qrose 适配时已验证并过审的同款结论)。但盘点依赖后发现:库的全部行为内核——分布、旋转、投影——恰好不碰任何 Compose API,是 commonMain 里三个零依赖纯 Kotlin 文件。制品可达性检查因此从"有没有 Compose ohos 变体"变成"内核是否零依赖",后者天然成立。再抽内核就不是权宜而是正确答案:编进去的每一个字节都是上游原件。

2.2 工具链钉版(为什么是 2.2.21-1.0.0)

内核零第三方依赖,不需要跟着任何 kotlinx 制品线走版本,因此钉社区标准线 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同款)。这条线要求 Gradle 8.14.x——上游 wrapper 是 8.3,必须升级(wrapper jar / gradlew 与 cmp_qrose 同源替换)。JDK 走 21。

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()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories { /* 同上 */ }
}

rootProject.name = "ohos_tagcloud"

include(":tagcloud")
include(":example:nativeApp")
2.3 编译真实内核

tagcloud/build.gradle.kts 收敛为纯 KMP 模块:

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()          // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}
    }
}

commonMain.dependencies {} 是空的——不是删漏了,是内核真的零依赖。实测:

./gradlew :tagcloud:compileKotlinJvm :tagcloud:compileKotlinOhosArm64 :tagcloud:compileKotlinOhosX64
# BUILD SUCCESSFUL

三个数学文件一个字符未动,零新增源码,零 expect/actual。

2.4 测试依赖的处置:上游没有测试

上游仓库没有测试目录(既无 commonTest 也无 androidTest),KStore 式"上游测试零改动跑 desktopTest"在本库无对象可跑。处置改为同源双编译交叉验证:jvm() 与 ohosArm64()/ohosX64() 编译的是同一份 commonMain(math + distribution),两套工具链对同一份源码的编译通过即为"内核没漂"的最低门槛,几何正确性由真机功能用例(6.4 节)兜底。根构建仍全局禁用 ohos 测试任务(防御性保留社区纪律):

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

第 3 阶段:桥协议建模

3.1 JSON 契约
eu.wewox.tagcloud.math / distribution(上游几何引擎,原样编译)
        ↓ FibonacciLattice / Quaternion.create / Vector3.rotate
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API tagcloudDispatch() / tagcloudVersion()
        ↓
ArkUI Index.ets(Stack + position + markAnchor,零数学,纯渲染)

会话状态(标签球面坐标 + 当前四元数)保存在 Kotlin 侧单例——ArkTS 不持有 Kotlin 对象,也避免每次手势重传全量标签坐标。请求与响应:

请求 : {"op":"create","count":64,"radius":300}
       {"op":"drag","fromX":180.5,"fromY":402.0,"toX":240.0,"toY":380.0,"radius":300}
       {"op":"rotate","angle":0.5,"axis":"y","global":true,"radius":300}
       {"op":"reset","radius":300}
       {"op":"layout","radius":336}
响应 : {"ok":true,"op":"layout","count":64,"radius":300,"version":"1.1.0-ohos.1",
        "rotation":{"w":0.912,"x":0.021,"y":0.403,"z":-0.044},
        "items":[{"i":12,"x":431.2,"y":96.4,"z":0.977,"s":0.988,"a":0.982}, …]}
       {"ok":false,"error":"call op=create first"}

三个关键设计:

  • 由远及近排序:items 按 z 升序返回,ArkTS 按数组顺序渲染即天然满足画家算法堆叠,每项再带 zIndex(z) 双保险;
  • 深度曲线在引擎侧算:s = rescale(z, 0.5)、a = rescale(z, 0.25),公式逐字复刻上游 TagCloudItemScope 的私有 rescale((((z+1)*(1-min))/2)+min)——即 tagCloudItemScaleDown() / tagCloudItemFade() 默认值,ArkTS 不重复实现;
  • 投影即中心点:x = R(1+cx)、y = R(1-cy)(上游 Placeable.getItemOffset 同款),ArkTS 用 .markAnchor('-50%','-50%') 以中心对齐,免去测量每个标签尺寸。
3.2 手势数学为什么忠实

op=drag 不是"方向键"式的近似旋转,而是上游手势链路的逐事件等价:

// 复刻上游 TagCloud.kt 的 Offset.getSphereCoordinates(私有 → 桥侧同款公式)
private fun sphereVector(x: Float, y: Float, radius: Float): Vector3 {
    val dx = (x - radius) / radius
    val dy = (y - radius) / radius * (-1f)
    val dz = sqrt(1 - dx * dx - dy * dy).takeUnless { it.isNaN() } ?: 0f
    return Vector3(dx, dy, dz).normalized()
}

// 复刻上游 rotateGesture + rotateBy(globalAxis = true)
val from = sphereVector(fromX, fromY, radius)
val to = sphereVector(toX, toY, radius)
session.rotation = Quaternion.create(from, to) * session.rotation

Quaternion.create(from, to) 内含上游的 180° 反向边界处理(点积 < -0.999999 时用正交轴构造 w=0 四元数),这些边界随原样编译直接生效。

3.3 错误通道

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

@CName("TagCloudDispatch")
fun tagCloudDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、缺 op、未 create 先操作、不支持的 op,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行与日志区。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libtagcloud_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohostagcloud.so)
  │ TagCloudDispatch / TagCloudVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制几何,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("TagCloudDispatch")
fun tagCloudDispatch(request: String): String   // create/drag/rotate/reset/layout 一口进

@CName("TagCloudVersion")
fun tagCloudVersion(): String = "1.1.0-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
TagCloudDispatchtagcloudDispatch(req)会话 + FibonacciLattice 分布 + Quaternion 手势/步进旋转 + 投影与深度曲线
TagCloudVersiontagcloudVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"tagcloudDispatch", nullptr, NapiTagCloudDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"tagcloudVersion", nullptr, NapiTagCloudVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

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

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

add_library(tagcloud_napi SHARED napi_init.cpp)
target_link_libraries(tagcloud_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohostagcloud)

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

export const tagcloudDispatch: (request: string) => string;
export const tagcloudVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 注入尺寸和触发操作
aboutToAppear(): void {
  const handshake: string = tagcloudVersion();   // 版本握手:来自真实 Native 调用
  this.version = handshake;
  this.pushLog(`version → ${this.version}`);
}

private onCloudArea(oldArea: Area, newArea: Area): void {
  const size = Math.floor(Math.min(newArea.width as number, newArea.height as number));
  this.originX = newArea.globalPosition.x as number;   // 手势坐标换算原点
  this.originY = newArea.globalPosition.y as number;
  if (!this.created || first) this.createCloud();      // 首次量出尺寸 → create
  else if (changed) this.relayout();                   // 尺寸变化 → 保旋转重投影
}

云区挂 priorityGesture(PanGesture({ fingers: 1, distance: 1 })):onActionStart 记录触点全局坐标(event.fingerList[0].globalX/globalY),onActionUpdate 把 (last → current) 换算为云区局部坐标后发 op=drag——与上游 RotationModifier 的 onRotate(previous, current) 逐事件语义一致。priorityGesture 保证球面拖拽优先于外层 Scroll。

5.2 页面交互

标签云 Tab 自上而下:数量 chips(16/32/64/128)、正方形云区(彩色圆角标签由远及近堆叠,近大而实、远小而淡)、重置/自动旋转双按钮(深灰/绿红语义切换)、四元数状态行与操作日志。每条日志带 [HH:mm:ss] 时间戳(最新在前、上限 6 条)。关于 Tab 展示版本握手行、库简介、ArkTS → NAPI → C ABI → Kotlin 引擎 分层说明与 AtomGit 仓库 / 社区链接。

自动旋转用 30ms 定时器每次 op=rotate 0.5°绕 Y 轴——TagCloudState.animateRotateTo 的动画语义不跨 C ABI(见 8.2),由步进近似,四元数连续变化在状态行肉眼可见。

5.3 对比度和错误边界

重置深灰、自动旋转绿(运行中变红"停止旋转"),语义与状态对齐;ok:false 时错误原文进状态行与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohostagcloud)+ copyKnSo*
│   └── src/ohosMain/kotlin/Bridge.kt         @CName × 2 + MiniJson + 会话状态(继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.tagcloud、versionName 1.1.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/libohostagcloud_api.h
        ├── src/main/cpp/types/libtagcloud_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :tagcloud:compileKotlinJvm                                        # 同源双编译交叉验证
./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 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 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.tagcloud -m entry

四、完整代码对照

4.1 整体架构

eu.wewox.tagcloud.math / distribution(上游几何引擎原样编译)
    │ FibonacciLattice 均匀分布 / Quaternion 旋转(含 180° 边界)/ Vector3.rotate
Bridge.kt + MiniJson + 会话状态
    │ TagCloudDispatch / TagCloudVersion
libtagcloud_napi.so
    │ tagcloudDispatch / tagcloudVersion
Index.ets
    │ JSON parse + @State 更新 + Stack/position/markAnchor 渲染
    ▼
ArkUI 真机页面(单页滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_tagcloud、demo 系列移出
gradle/libs.versions.toml只剩 HarmonyOS Kotlin 2.2.21-1.0.0 一个钉版
build.gradle.kts(根)版本 1.1.0-ohos.1、全局禁用 Ohos*Test* 任务
tagcloud/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,commonMain 零依赖
tagcloud/src/composeMain/**上游 Compose 层 5 文件留盘(git mv,100% rename 保留历史)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 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鸿蒙适配主文档(17 节)

4.3 关键 API 对照

层次API作用
引擎FibonacciLattice(count).item(i)黄金角把 N 个标签无聚簇铺满球面
引擎Quaternion.create(from, to)两球面向量间的旋转增量(含 180° 边界)
引擎Quaternion.create(angle, axis) × times步进旋转与复合(全局轴左乘)
引擎Vector3.rotate(q)旋转矩阵展开,逐标签投影
桥op=create / drag / rotate / reset / layout会话全生命周期
桥rescale(z, min) 复刻深度缩放/透明度曲线(0.5 / 0.25)
NativeTagCloudDispatch / TagCloudVersionC ABI JSON 导出
N-APItagcloudDispatch(req) / tagcloudVersion()向 ArkTS 暴露能力
ArkTSdispatch(op) / onCloudArea / Pan 转发触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(tagcloudDispatch(JSON.stringify({
  op: 'drag',
  fromX: lastX - originX, fromY: lastY - originY,
  toX: x - originX, toY: y - originY,
  radius: this.radius
}))) as DispatchOk;
// res.ok === true;items 按由远及近排序,逐项 position/markAnchor/scale/opacity/zIndex
val from = sphereVector(json.num("fromX"), json.num("fromY"), radius)
val to = sphereVector(json.num("toX"), json.num("toY"), radius)
session.rotation = Quaternion.create(from, to) * session.rotation
return opLayout(radius.toInt())   // 全量投影 + 深度曲线 + 由远及近排序

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

五、关键决策说明

决策 1:编几何内核而不是等 Compose 对齐

tagcloud 的 API 面就是 Compose,上游 Compose 1.5 / Kotlin 1.9 与鸿蒙 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 不可混编——这不是依赖缺制品,是两条编译器工具链线。而库的灵魂(分布 + 旋转 + 投影)恰好是零依赖纯 Kotlin。把几何内核编进 .so,"这个标签云是不是 tagcloud"由真机上的上游代码回答;ArkTS 侧零数学,不存在两套几何各说各话的漂移风险。

决策 2:钉社区标准线 Kotlin 2.2.21-1.0.0,Gradle 升 8.14.1

内核不依赖 kotlinx 四件套,没有 KStore 那种"stdlib 元数据反推 2.4.20"的压力,直接钉社区已审标准线 2.2.21-1.0.0(qrose / haze 同款)。这条线要求 Gradle 8.14.x,上游 wrapper 8.3 必须升级——wrapper jar 与 gradlew 脚本与 cmp_qrose 同源替换,避免 8.3 的 jar 拉 8.14.1 发行版的边角问题。

决策 3:根构建替换(cmp_qrose 模式),Compose 层与 demo 系列留盘

上游根构建(detekt / spotless / AGP / vanniktech 发布 / build-logic 组合插件)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。Compose 层 5 个文件 git mv 到 composeMain——不删除、不重写,git 历史以 100% rename 完整保留,上游工具链恢复 include 即可复编。demo / desktopdemo / androiddemo / iosdemo 同理留盘不进 settings。

决策 4:会话状态在 Kotlin 侧 + items 由远及近返回 + 深度曲线在引擎侧算

会话(球面坐标 + 四元数)放 Kotlin 单例:ArkTS 不持有 Kotlin 对象,手势也不必每次重传标签坐标。响应 items 按 z 升序,ArkTS 按数组顺序渲染即满足画家算法,zIndex(z) 双保险。缩放/透明度用上游私有 rescale 公式在引擎侧算好(s/a 字段),ArkTS 拿到即用——上游渲染语义(0.5 / 0.25 曲线)与 ArkUI 实现解耦。

决策 5:同源双编译交叉验证

上游没有测试目录,KStore 的"51 项 desktopTest 跑上游原测试"无对象可跑。改为 jvm() 与 ohosArm64()/ohosX64() 编译同一份 commonMain——两套工具链对同一份源码编译通过,加上真机功能用例(拖拽方向正确性、180° 边界、分布均匀性)兜底几何正确性。全局禁用 Ohos*Test* 任务作为社区纪律防御性保留。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21(Adoptium jdk-21.0.12.101-hotspot)、DevEco Studio 26.0、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :tagcloud:compileKotlinJvm

commonMain(math + distribution 三个文件)在 JVM 与 ohos 双 ABI 上编译的是同一份源码;上游无测试可跑,同源双编译即"内核没漂"的编译期闸门。

6.3 HAP 验证

:tagcloud:compileKotlinJvm / compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: TagCloudDispatch / TagCloudVersion / libohostagcloud_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake / BuildNativeWithNinja / DoNativeStrip: finished(双 ABI 编出 libtagcloud_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 6.8 MB,内含双 ABI 各三件套(libtagcloud_napi.so / libohostagcloud.so / libc++_shared.so);HAP 内 strip 后 KN so arm64 约 1.96MB、x86_64 约 1.91MB(debugShared 未 strip 时分别约 3.7MB / 3.4MB,含 Kotlin/Native 运行时),NAPI 包装层每个 ABI 约 65KB。

6.4 功能用例

  1. 启动后日志出现 version → 1.1.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. 云区量出尺寸后自动 create,64 个彩色标签铺满球面,无聚簇(斐波那契晶格);
  3. 拖动球面:标签云随手势旋转,拖拽方向与旋转方向一致(球面手势数学与上游一致);
  4. 旋转过程中近处标签大而不透明、远处标签缩到约 0.5、淡到约 0.25(引擎侧 rescale 曲线);
  5. 前后标签遮挡关系正确(由远及近排序 + zIndex);
  6. 自动旋转开启后绕 Y 轴连续自转,状态行四元数 w/x/y/z 连续变化,停止后数值停住;
  7. 重置后回到初始视角(w≈1,x/y/z≈0);
  8. 切换 16/32/64/128:标签即时重铺且仍均匀;
  9. 旋转屏幕 / 界面尺寸变化后,标签保持当前旋转重新投影(op=layout 保旋转重排);
  10. 每步操作按时间戳滚动留痕。

6.5 验证结论

几何引擎、Kotlin/Native 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的标签云会话:版本握手、球面均匀分布、拖拽旋转、深度渲染曲线、自动旋转、重置与数量切换(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/tagcloud.git
cd tagcloud
export JAVA_HOME="/path/to/jdk-21"
./gradlew :tagcloud:compileKotlinJvm                                                    # 同源双编译
./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.tagcloud -m entry
hdc shell hilog | grep TagCloud

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :tagcloud 的几何内核真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. gradlew.bat 自举需要 JAVA_HOME 环境变量,org.gradle.java.home 要等 JVM 起来才生效——先设环境变量再跑 wrapper;
  4. 2.2.21 工具链线要求 Gradle 8.14.x,上游 8.3 的 wrapper 必须升级,且 wrapper jar / gradlew 脚本一并替换;
  5. String.format 在 Kotlin/Native 不可用——桥内数值格式化改为手写定点格式化(3 位小数);
  6. 命令行 hvigor 需要 NODE_HOME(DevEco 自带 node)+ DEVECO_SDK_HOME + java 在 PATH,缺 java 时 PackageHap 报 00308018 spawn java ENOENT;
  7. ArkTS 严格模式:未验证模块导入(libtagcloud_napi.so)的返回值是隐式 any,必须显式标注 const raw: string = ...;
  8. GestureEvent 上没有 globalX/globalY(已从 SDK 移除),触点坐标只能走 event.fingerList[0].globalX/globalY;
  9. .gesture() 第二参数是 GestureMask 不是优先级(枚举值是 NORMAL/PRIORITY);让子组件手势赢过外层 Scroll 要用独立的 .priorityGesture();
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(报 00303038)——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  13. 根 .gitignore 的 build 用非锚定写法盖住嵌套目录(example/nativeApp/build 等);entry/libs 与 *.so 一并忽略,产物不入库;Compose 层迁移用 git mv 保住 100% rename 历史。

8.2 已知问题

  • TagCloudState 的 Compose 动画语义(Animatable / animateRotateTo / rememberSaveable)不跨 C ABI:桥是同步一次一调用,演示以 30ms 定时器步进近似连续动画;
  • fling(甩动惯性)未桥接——上游 TODO 列表本身也还没有 fling;
  • 标签点击回调未桥接(演示云只读),TagCloudScope 的 layer(多层球面)与单项 rotation 参数未逐一开桥,需要时按 op 扩展;
  • android / ios / desktop 目标与 demo 系列剥离(源文件留在树里),发布、静态检查一并剥离,本 fork 不发 Maven Central;
  • 上游无单元测试,交叉验证采用"JVM 与 ohos 编译同一份 commonMain"+ 真机功能用例,非 desktopTest 式闸门;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.tagcloud 匹配的签名。

8.3 未来优化方向

  • 把 layer(多层球面)与单项 rotation 参数开进 op=create(复刻上游"球内粒子"示例);
  • N-API 线程安全回表推送连续动画帧(替代 ArkTS 定时器步进);
  • fling 惯性(上游实现后同步桥接);
  • 标签点击回调桥接;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;补充模拟器(x86_64)验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 tagcloud(Compose 1.5 / Kotlin 1.9 的 CMP 库)
    → 确认 API 面=Compose、几何内核=零依赖的切面(决策成败手)
    → 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0(Gradle 8.14.1)
    → 根构建替换(cmp_qrose 模式,Compose 层 git mv 留盘)
    → ohosArm64 / ohosX64 KLIB + 零新增源码
    → C ABI(@CName × 2)+ 会话 JSON 契约 + 深度曲线引擎侧复刻
    → C++ N-API(DisposeString 纪律)
    → ArkTS 手势转发 + markAnchor 中心渲染
    → ArkUI 真机单页验收(拖拽 / 自动旋转 / 重置 / 数量切换 / 握手)

9.2 封装层次

  • 引擎层:上游原版 FibonacciLattice 均匀分布、Quaternion 旋转(含 180° 边界)、Vector3.rotate 投影;
  • 会话层:桥内单例持有球面坐标与当前四元数,一次一调用;
  • Native 层:双 ABI 动态库 + 2 个 C ABI 输出 JSON;
  • N-API 层:参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:手势转发、JSON 解析、@State 更新与由远及近渲染;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. UI 库适配的正确切面是"行为内核"而不是"渲染外壳":tagcloud 值钱的是分布 + 旋转几何,恰好零依赖——切面找对,ArkTS 只剩画标签;切面找错,就是用 ArkTS 重写一遍 tagcloud,过不了审核也守不住上游;
  2. 工具链线决定 Gradle 版本,而不是反过来:2.2.21 线要 8.14.1 就升 wrapper(连 jar 一起换),2.4.20 线可留 9.x——先定 Kotlin 钉版,再校 Gradle,最后动 wrapper;
  3. 无测试库的交叉验证退而求其次是同源双编译:同一份 commonMain 让 JVM 与 ohos 工具链各编一遍,配合真机几何用例(方向一致性、180° 边界、均匀性)兜底——验证形式可以降级,"内核没漂"这件事不能缺席。

9.4 适配成果

当前适配已完成:

  • tagcloud 1.1.0 几何内核的双 ABI Kotlin/Native 编译(上游 commonMain 文件零改动,全仓零新增 Kotlin 源文件、零 expect/actual);
  • Compose 层 5 文件 git mv 隔离留盘(100% rename 保留历史),demo / build-logic / config 留盘不进构建;
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号,双 ABI);
  • DevEco Stage ArkUI 单页验收页面(拖拽旋转、自动旋转、重置、数量切换、四元数状态行、操作日志、版本握手);
  • 同源双编译交叉验证 + HAP 构建(双 ABI 六件套 so,约 6.8MB)、签名、设备安装和真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐