开源鸿蒙平台 CMP 三方库「GuideKit」适配全流程
本文记录
GuideKit接入 OpenHarmony 的完整过程,覆盖引导覆盖层库的能力盘点、纯 Kotlin 内核与 Compose 实现的源码集拆分、真实状态机的 ohos 编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机引导页、HAP 构建和真机验收。
GuideKit是一个 Compose Multiplatform 的新手引导(coach mark)库:给定一组GuideKitStep(目标矩形 + 标题 + 描述 + 步骤级样式覆盖)和一个全局GuideKitStyle,它在应用最顶层画一层「半透明遮罩 + 目标挖孔 + 发光描边 + 卡片↔目标智能箭头 + 说明卡片 + 步骤指示器」,内部自持 next / previous / skip / finish 的步骤状态机,并按「库默认 → 全局样式 → 步骤 override」三级解析每一步的最终样式。本次适配与 KStore(依赖 kotlinx 制品线、整库真编译)的路线不同:GuideKit是纯 UI 库,公开 API 全部是 Compose 类型,而社区 HarmonyOS CMP 线是 1.9.2、上游是 CMP 1.11.1 / Kotlin 2.3.0,混编不可行。因此本仓采用 cmp_qrose 已审核的「纯 Kotlin 内核编译 + Compose 隔离 + ArkUI 重画」路径:步骤状态机GuideKitController原样编译为 OpenHarmony KLIB,高亮边界 / 自动滚动 / 锚点箭头曲线的全部几何计算逐行移植进commonMain,Compose 渲染隔离到不参与编译的src/composeMain,ArkTS Demo 按 Kotlin 侧算出的几何数据在 ArkUI Canvas 上重画遮罩、挖孔、发光与箭头——呈现的是 CMP 的模型,不是重写的引导规格。
项目地址: AtomGit/oh-tpc/guidekit
上游仓库: tharukack/guidekit
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
GuideKit 为 Compose 应用提供「新手引导」这一件事的完整答案:GuideKit(steps, style, onFinished, ...) 一个 Composable 挂在屏幕顶层,目标控件用 onGloballyPositioned { boundsInRoot() } 上报 Rect,库负责剩下的全部——遮罩挖孔(圆角矩形 / 圆形)、四层发光描边、9 宫格锚点的贝塞尔箭头(7 种线型、4 种箭头朝向、多层描边)、说明卡片(短语高亮、步骤指示器、Next/Got it 按钮文案)、自动滚动、每一步 field 级样式覆盖。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要引导能力,但上游构建(Kotlin 2.3.0 / Gradle 9.3.0 / AGP 8.13.2 / CMP 1.11.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明引导的决策逻辑(状态机、样式解析、几何计算)在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证:纯 Kotlin 内核的 ohos 编译、override 合并语义的镜像正确性、Native 动态库、C ABI、N-API、ArkUI 覆盖层和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.3.0 / Gradle 9.3.0 / AGP 8.13.2 与鸿蒙工具链无法共存于一个 Gradle 构建;HarmonyOS Kotlin 旧线钉 2.2.21-1.0.0,必须配 Gradle 8.14.1。 |
| API 全是 Compose | 库的公开 API(GuideKitStep / GuideKitStyle / 三组 Override)全部由 Dp / Color / Alignment / Shape / Modifier 组成,compose.ui 不在 ohos 类路径上,整库编译无从谈起。 |
| 渲染依赖 Canvas | 遮罩挖孔走 BlendMode.Clear 离屏合成,箭头是 Path.cubicTo + PathEffect,这些 API 与 Compose 运行时强绑定。 |
| 语言边界 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:能离开 Compose 的部分全部真编译(状态机、几何、枚举),离不开 Compose 的部分隔离留档(Composable 与样式模型),渲染由 ArkUI 按 Kotlin 几何重画。引导语义完全由上游原版实现负责,ArkTS 只负责测量上报、调用 Native、按返回的几何数据画图。
1.2 库提供的能力
Demo 提供一次真实的四步引导会话,对应桥接的全部入口:
- 开始(start):上报四步的目标矩形(头像 / 步数卡 / 设置行 / 底部 Tab)与全局样式,Kotlin 侧建会话、返回第 0 步的完整几何;
- 下一步(next):走上游
GuideKitController.next(),返回StepChanged的新步骤几何或Finished; - 上一步(previous):卡片上右滑触发,首步保持不动(上游钳制语义);
- 跳过(skip):返回
Skipped,覆盖层消失; - 状态重取(state):说明卡片布局稳定后补报
cardRect,Kotlin 侧重算箭头曲线; - 版本握手(version):页头状态行显示
guidekitVersion()真实返回的1.2.0-ohos.1,最近一次 op 同步回显——每次交互都是真实的 Kotlin/Native 调用。
响应包含:
| 字段 | 作用 |
|---|---|
ok / result | 操作是否成功;step / finished / skipped / nosteps。 |
step.highlight | 挖孔边界(padding + 圆形吸附后的矩形)、形状、圆角、四层发光描边(宽 + 已合成 alpha 的颜色)、外框、内嵌框。 |
step.arrow | 贝塞尔曲线的 start / c1 / c2 / end / bodyStart / bodyEnd、虚线节拍、多层描边、双翼箭头线段。 |
step.instruction | 说明卡片对齐、外边距、内边距、圆角、容器色、边框色。 |
step.primaryButtonText 等 | 三级解析后的文案与颜色(中间步 Next、末步 Got it)。 |
step.autoScrollDelta | 自动滚动增量(上游公式,含默认 spacing = minVisibleDistance + 1)。 |
引擎侧完整能力(状态机钳制、override 三级解析、锚点数学、箭头颈部裁剪)全部在上游 commonMain 语义内随编译进入 .so,JVM 侧保留 41 项单测交叉验证,证明 ohos .so 里执行的引导逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | GuideKitController 与导航结果原样编译(上游文件拆出 Compose 解析函数后一行未改);高亮边界、自动滚动、锚点箭头几何从 GuideKit.kt 私有函数逐行移植(魔法数 0.18 edgeInset、0.34 控制距、44..128 弯曲钳制、hash % 2 弯曲方向全部保留)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose 实现整体迁 src/composeMain(非源集,不参与 ohos 编译);上游 sample/(Android/iOS)留盘不进 settings。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 语义镜像 | override 合并链在桥模型里字段级镜像(null = 继承,"none" = 显式清空,对应上游 GuideKitOverride.Value(null)),8 项 JVM 测试钉住上游 GuideKitStepResolutionTest 的同等场景。 |
| UI 完整 | 单页覆盖全部桥接入口:四步 tour 覆盖圆形高亮、短语高亮、点线双端箭头、卡片换位四类 override。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit,不出现任何第三方镜像站字样。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
GuideKit的价值在模型与几何(状态机、样式解析、锚点曲线),不在像素——几何在 Kotlin 算,像素在 ArkUI 画。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose 隔离线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:源码集拆分 ── 纯 Kotlin 内核留 commonMain,Compose 实现迁 composeMain,测试同步拆分
第 4 阶段:桥协议建模 ── override 合并镜像、扁平 JSON 契约、单位与颜色约定、错误通道
第 5 阶段:原生桥接 ── @CName C ABI × 2、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 目标测量、Canvas 遮罩/挖孔/发光/箭头、四步 tour、真机验收
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
src/commonMain/ 纯 Kotlin 内核:Controller + Geometry + ArrowGeometry + Enums(进 ohos 构建)
src/commonTest/ 上游 17 项控制器测试 + 16 项几何移植测试
src/composeMain/ 上游 GuideKit.kt(Composable + Compose 样式模型,不进构建)
src/composeTest/ 上游 GuideKitStepResolutionTest 留档(不进构建)
example/nativeApp/ Kotlin/Native 动态库(Bridge + 模型镜像 + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
sample/ docs/ 上游示例与文档站,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :example:nativeApp,rootProject.name = "ohos_guidekit"。上游根项目本身就是库模块(这个布局保留),AGP / Compose 插件 / vanniktech 发布配置全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向本机 JDK 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 连 jar 一起替换;2.2.21 旧线必须 8.x,KStore 的 2.4.20 线经验不能套用) |
| 依赖 | 零外部依赖——内核只有 kotlin-stdlib,不需要任何 kotlinx 制品线 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.guidekit |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
上游只有两个源文件,但职责泾渭分明:
| 文件 | 内容 | Compose 依赖 | 处置 |
|---|---|---|---|
GuideKit.kt(1028 行) | 样式模型 + GuideKit() Composable + 箭头几何私有函数 | Dp/Color/Shape/Canvas/Path/动画全家桶 | 整体迁 composeMain;四个纯枚举抽回 commonMain |
GuideKitController.kt(219 行) | 控制器状态机 + 导航结果 + Compose 类型解析函数 | 仅 Rect(纯几何)与样式类型 | 控制器留 commonMain;解析函数迁 composeMain |
拆分判定标准只有一条:不 import androidx.compose.* 的代码才进 commonMain。全库 expect/actual 为 0,是最理想的起点。上游 drawGuideKitArrow 里的 toPx() 依赖 DrawScope 密度,移植版把单位决定权交给调用方(桥传 vp),数学一行未改。commonMain 新增三个文件:
GuideKitGeometry.kt:GuideKitPoint/GuideKitRect(镜像 composeOffset/Rect的最小子集)、resolveGuideKitHighlightBounds(padding 膨胀 + 圆形吸附)、calculateGuideKitAutoScrollDelta;GuideKitArrowGeometry.kt:锚点取点(edgeInset 0.18、outside padding)、锚点自然方向、曲线弯曲方向 / 幅度(hash 奇偶 + 44…128 钳制)、虚线节拍表、箭头颈部距离(cos(翼角) × 长度 + 最粗描边 × 0.35,0.36 × 距离封顶)、双翼端点、resolveGuideKitArrowGeometry整体组装;GuideKitEnums.kt:GuideKitAnchor/GuideKitArrowLineStyle/GuideKitArrowHead/GuideKitTargetHighlightShape(枚举无 Compose 依赖,是两边共享的类型词表)。
1.4 页面边界
标题区 GUIDEKIT DEMO / 版本握手行(1.2.0-ohos.1 · Kotlin/Native ohosArm64 · 最近 op)
开始按钮 开始引导 · Start tour
模拟业务页 用户卡片(头像 + ID)、三格统计(步数/睡眠/心率)、设置列表、底部 TabBar
覆盖层 Canvas(遮罩 + 挖孔 + 发光 + 外框 + 内框 + 箭头曲线 + 双翼箭头)
说明卡片 标题、短语高亮描述、步骤指示点、主按钮(Next/Got it)、右上跳过 ✕
交互 按钮点击 → next;卡片右滑 → previous;✕ → skip
四个引导目标即模拟页的四个真实控件,onAreaChange 的 globalPosition + 宽高换算成页面坐标(vp),覆盖层 Canvas 与页面同坐标系。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的路线分岔点)
KStore 的成败手是「kotlinx ohos 制品线存在,所以整库真编译」。GuideKit 的分岔点相反:它依赖的是 org.jetbrains.compose.* 的 runtime/ui/foundation/material3,社区 HarmonyOS CMP 线是 1.9.2,上游是 1.11.1——compose 制品既不同版本也接不进上游源码。结论:Compose 部分不编译,走 cmp_qrose(以及更早的 Vico / Reorderable)已审核的「内核编译 + 渲染重画」路径。幸而引导库的价值重心恰好可以切干净:状态机和几何不碰 Compose。
2.2 工具链钉版
settings.gradle.kts 把 eazytec 双仓写在最前(pluginManagement 与 dependencyResolutionManagement 都要),否则配置期直接 Unresolved reference: ohosArm64:
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_guidekit"
include(":example:nativeApp")
gradle/libs.versions.toml 裁到只剩一个插件——HarmonyOS Kotlin 2.2.21-1.0.0。Gradle 钉 8.14.1(gradlew / gradlew.bat / wrapper jar 全套替换),JDK 21 写进 org.gradle.java.home,版本号 1.2.0-ohos.1(上游版本 + -ohos.N),group 保留上游 io.github.tharukack。
2.3 编译纯 Kotlin 内核
根 build.gradle.kts 收敛为纯 KMP 模块(根项目即库):
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
group = "io.github.tharukack"
version = providers.gradleProperty("version").orElse("1.2.0-ohos.1").get()
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
allprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
实测:
./gradlew compileKotlinJvm jvmTest :example:nativeApp:jvmTest # 41 项全绿
./gradlew compileKotlinOhosArm64 compileKotlinOhosX64 # BUILD SUCCESSFUL
状态机与几何的源文件一个没动,Compose 实现整体留在 src/composeMain(不是 Kotlin 源集,Gradle 看不见它)。
2.4 测试依赖的处置
上游测试分两类:GuideKitControllerTest(17 项,仅依赖 Rect 与两个纯函数)与 GuideKitStepResolutionTest(依赖 Dp / Color / BorderStroke)。处置:前者把 androidx.compose.ui.geometry.Rect 等价替换为 GuideKitRect(数值断言一字不改)留在 commonTest;后者整体迁 src/composeTest 留档不编译,其关键场景(override 只改显式字段、Value(null) 显式清空、默认值表)移植为桥模型的 8 项 JVM 测试,语义不丢。ohos 侧测试任务由根构建全局禁用(Ohos*Test*),测试统一 JVM 跑——这是「内核没漂」的闸门。
第 3 阶段:桥协议建模
3.1 JSON 契约
io.github.tharukack.guidekit/**(状态机 + 几何,上游语义原样编译)
↓ GuideKitController / resolveGuideKit* / resolveGuideKitArrowGeometry
GuideKitBridgeSession(会话:三级解析 + 几何组装 + 序列化)
↓ UTF-8 JSON
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓
N-API guidekitDispatch() / guidekitVersion()
↓
ArkUI Index.ets(测量上报 + Canvas 重画 + 原生卡片)
请求与响应:
请求 : {"op":"start","density":3.0,"style":{"accent":"#FF5ED5B3"},"steps":[...],"initialStepIndex":0}
{"op":"next"|"previous"|"skip"|"state","density":3.0,"cardRect":{"l":18,"t":620,"r":342,"b":760}}
响应 : {"ok":true,"result":"step","version":"1.2.0-ohos.1","step":{
"highlight":{"bounds":{...},"shape":"Circle","glow":[{"widthPx":10,"color":"#1C5ED5B3"}...],...},
"arrow":{"start":{...},"c1":{...},"c2":{...},"end":{...},"dash":[20,15],
"strokes":[{"widthPx":3,"color":"#3D000000"}...],
"targetWings":[{"from":{...},"to":{...}}...]},
"instruction":{"alignment":"BottomCenter","cardRadius":30,...},
"primaryButtonText":"Next","autoScrollDelta":0,...}}
{"ok":true,"result":"finished"|"skipped"|"nosteps"}
{"ok":false,"error":"..."}
三个不对称设计:density 由 ArkTS 随每次请求带上(display.getDefaultDisplaySync().densityPixels),上游 px 系的 int(描边 9/6/2、箭头头长 38、挖孔 padding、边框宽)由桥除以 density 折成 vp,dp 系(minVisibleDistance 40、卡片圆角 30)原样即 vp——与上游在真机上的视觉一致;cardRect 只在卡片布局稳定后随 state 补报,桥据此重算箭头几何;导航 op 返回 result 枚举而不是布尔,对应上游 GuideKitNavigationResult 四态。
3.2 模型镜像与 override 语义
上游 applyTo 三连的合并语义在桥模型(GuideKitBridgeModel.kt,纯 Kotlin)字段级镜像:每个 Override 类的 nullable 字段用 ?: 继承全局值,等价于上游写法。唯一不能直译的是上游的 GuideKitOverride<T> 泛型(Inherit / Value(null) 区分「不覆盖」与「覆盖成 null」)——桥以 JSON 字符串 "none" 表示显式清空,例如 "borderColor": "none" 让全局边框色归 null。颜色统一 #AARRGGBB(6 位按不透明处理),colorWithAlpha 的取整与 Compose Color.copy 相同((α×255+0.5).toInt(),0.68 → 0xAD),多层描边的最终颜色由桥预合成,ArkTS 拿到即用。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("GuideKitDispatch")
fun guideKitDispatch(request: String): String = try {
dispatchOrThrow(request)
} catch (t: Throwable) {
jObj("ok" to false.j(), "error" to (t.message ?: t.toString()).j()).serialize()
}
非法 JSON、缺 op、不支持的 op、会话未建,全部走同一个 ok:false 通道,ArkTS 侧统一进页头状态行与 hilog。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libguidekit_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosguidekit.so)
│ GuideKitDispatch / GuideKitVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面重算几何,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("GuideKitDispatch")
fun guideKitDispatch(request: String): String // start / next / previous / skip / state / version 一口进
@CName("GuideKitVersion")
fun guideKitVersion(): String = "1.2.0-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
GuideKitDispatch | guidekitDispatch(req) | 会话建立 + 三级样式解析 + 几何组装 + 导航 |
GuideKitVersion | guidekitVersion() | fork 版本握手(真机页头状态行的数据来源) |
一个必须记录的细节:不要写 import kotlinx.cinterop.CName——HarmonyOS Kotlin 2.2.21-1.0.0 里 Kotlin/Native 默认导入已覆盖该注解,显式 import 反而 Unresolved reference(cmp_qrose 的 Bridge.kt 就没有这行)。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"guidekitDispatch", nullptr, NapiGuideKitDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"guidekitVersion", nullptr, NapiGuideKitVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosguidekit_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosguidekit_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohosguidekit SHARED IMPORTED)
set_target_properties(ohosguidekit PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(guidekit_napi SHARED napi_init.cpp)
target_link_libraries(guidekit_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosguidekit)
OHOS_ARCH 为空或 libohosguidekit.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const guidekitDispatch: (request: string) => string;
export const guidekitVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 目标测量和触发操作
private onTargetArea(key: string): (oldArea: Area, newArea: Area) => void {
return (_oldArea: Area, newArea: Area) => {
const gp = newArea.globalPosition;
const x: number = this.lenToNum(gp !== undefined ? gp.x : undefined);
const y: number = this.lenToNum(gp !== undefined ? gp.y : undefined);
// ... 组装 TargetRect{l,t,r,b}(vp,与覆盖层 Canvas 同坐标系)
};
}
private dispatch(op: string, extra: StartExtra | null): DispatchResponse {
const req: BridgeRequest = { op: op, density: this.density };
if (this.cardRect !== null) { req.cardRect = this.cardRect; }
if (extra !== null) { req.style = extra.style; /* ... */ }
return JSON.parse(guidekitDispatch(JSON.stringify(req))) as DispatchResponse;
}
测量三处细节:Area.width / globalPosition.x 的类型是 Length(string | number | Resource 联合),必须经 lenToNum 收窄成 number;覆盖层 Canvas 与页面根 Stack 同坐标系,globalPosition 即画布坐标;卡片先布局、箭头后补——首次响应渲染卡片,onAreaChange('card') 稳定后带 cardRect 重发 state 取箭头(矩形 key 去重防止循环)。
5.2 页面交互
覆盖层是两层 Stack:下层全屏 Canvas 画遮罩(globalCompositeOperation = 'destination-out' 挖孔 + 四层发光 + 外框 + 内嵌框 + bezierCurveTo 箭头 + setLineDash 虚线 + 双翼线段),上层原生说明卡片(标题、Span 短语高亮、步骤指示点、主按钮、右上 ✕)。卡片挂 PanGesture,右滑超过 72vp 触发 previous——对应上游「右滑回上一步」。步骤指示点复刻上游形态:当前步 18×6 胶囊、其余 6×6 圆点。
5.3 对比度和错误边界
主按钮强调色(accent #FF5ED5B3)配深色文案 #FF062D25,跳过图标弱化为次级灰;ok:false 时错误原文进页头状态行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosguidekit)+ cxxShared + copyKnSo*
│ └── src/
│ ├── commonMain/ MiniJson + GuideKitBridgeModel(镜像)+ GuideKitBridgeSession(解析/几何/序列化)
│ ├── commonTest/ 8 项桥语义测试(JVM)
│ └── ohosMain/Bridge.kt @CName × 2 + 会话单例
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.guidekit、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 + napi_init.cpp + include/libohosguidekit_api.h + types/libguidekit_napi
└── src/main/ets/pages/Index.ets ArkUI 四步引导 Demo
6.2 构建、签名和安装
./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.guidekit -m entry
四、完整代码对照
4.1 整体架构
io.github.tharukack.guidekit/**(状态机 + 几何 + 枚举,上游语义原样编译)
│ GuideKitController / resolveGuideKitHighlightBounds / resolveGuideKitArrowGeometry
GuideKitBridgeModel(override 合并镜像,null=继承、none=清空)
│ GuideKitBridgeSession(三级解析 + 几何组装 + JSON 序列化)
libohosguidekit.so
│ GuideKitDispatch / GuideKitVersion
libguidekit_napi.so
│ guidekitDispatch / guidekitVersion
Index.ets
│ 测量上报 + JSON.parse + @State 刷新
▼
ArkUI 真机引导页(Canvas 覆盖层 + 原生说明卡片)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_guidekit、sample 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单插件钉版 |
build.gradle.kts(根=库) | jvm + ohosArm64 + ohosX64、版本 1.2.0-ohos.1、全局禁用 Ohos*Test* |
src/commonMain/.../GuideKitGeometry.kt | 几何原语 + 高亮/滚动解析(逐行移植) |
src/commonMain/.../GuideKitArrowGeometry.kt | 锚点/曲线/虚线/颈部/双翼几何(逐行移植) |
src/commonMain/.../GuideKitController.kt | 上游状态机(原样编译) |
src/composeMain/.../GuideKit.kt + GuideKitResolution.kt | 上游 Compose 实现留档(不编译) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、错误通道、会话单例 |
nativeApp/.../GuideKitBridgeModel.kt | override 合并镜像 + 颜色/单位约定 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 四步引导验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(19 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | GuideKitController.next/previous/skip | 步骤状态机(上游原件,含钳制与四态结果) |
| 引擎 | GuideKitController.updateStepCount | 步数变更钳制(上游原件) |
| 几何 | resolveGuideKitHighlightBounds | padding 膨胀 + 圆形吸附 |
| 几何 | calculateGuideKitAutoScrollDelta | 自动滚动增量(含上游默认 spacing) |
| 几何 | resolveGuideKitArrowGeometry | 锚点取点、控制点、颈部裁剪、整体曲线 |
| 几何 | arrowHeadWings | 双翼箭头端点(角度/翼长) |
| 镜像 | BridgeArrowCfgOverride.merge 等 | 三级 override 合并(null=继承、none=清空) |
| Native | GuideKitDispatch / GuideKitVersion | C ABI JSON 导出 |
| N-API | guidekitDispatch(req) / guidekitVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch(op) | 触发操作并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(guidekitDispatch(JSON.stringify({
op: 'next',
density: display.getDefaultDisplaySync().densityPixels,
cardRect: { l: 18, t: 620, r: 342, b: 760 } // 说明卡 onAreaChange 实测(vp)
}))) as DispatchResponse;
// res.ok === true;res.step.arrow.c1/c2 即上游贝塞尔控制点,Canvas 直接 bezierCurveTo
val arrowJson = resolveGuideKitArrowGeometry(
cardBounds = cardRect,
highlightBounds = highlightBounds,
from = arrowCfg.from, to = arrowCfg.to, curveSeed = arrowCfg.curveSeed,
minVisibleDistancePx = arrowCfg.minVisibleDistance,
arrowHead = arrowCfg.arrowHead,
arrowHeadLengthPx = arrowCfg.arrowHeadLength / density,
arrowHeadAngleDegrees = arrowCfg.arrowHeadAngleDegrees,
widestStrokePx = arrowCfg.strokes.maxOfOrNull { it.width }?.let { it / density } ?: 0,
lineStyle = arrowCfg.lineStyle,
)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:真编译状态机而不是重写
GuideKitController 是上游原件,桥直接 new。上游 17 项控制器测试在 JVM 原样跑绿——「内核没漂」有测试背书,而不是靠肉眼。重写一个四态状态机看似便宜,但它恰恰是引导库全部交互语义(钳制、回退、完成、跳过)的所在。
决策 2:几何移植而不是 ArkTS 重算
锚点取点、自然方向、弯曲方向与幅度、虚线节拍、颈部裁剪、双翼端点的每一条公式(含 0.18 edgeInset、0.34 控制距、44..128 钳制、hash % 2 奇偶这些上游魔法数)逐行从 GuideKit.kt 移植,ArkTS 只画不算。若在 ArkTS 里重算,箭头风格必然漂移,且上游调参时无法同步。
决策 3:override 合并镜像放 nativeApp 而非库
上游模型是 Compose 类型,库的 commonMain 保持「无 Compose 依赖」的纯粹;合并逻辑在桥模型里字段级镜像,8 项 JVM 测试把上游 GuideKitStepResolutionTest 的关键场景移植钉住。上游改 applyTo 时,同步改桥并补测试即可。
决策 4:根项目继续当库模块
保留上游「根项目即 guidekit 库」的布局(src/ 在根),settings 只增 :example:nativeApp,桥模块以 implementation(project(":")) 依赖根。cmp_qrose / KStore 的多模块库则相反——布局跟上游走,规则不变(eazytec 置前、三目标、无 publish)。
决策 5:返回几何而不是返回位图
qrose 返回 PNG 因为条码是静态图形;引导层需要透明遮罩叠加在真实 UI 上、卡片按钮要可点、遮罩要拦截穿透点击——位图方案覆盖不了交互。返回几何让 ArkUI 原生渲染,正是 UI 库跨语言的正解。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21、Gradle 8.14.1、HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew jvmTest :example:nativeApp:jvmTest
41 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
GuideKitControllerTest | 17 | 上游原件:next/previous/skip/finish、空列表、越界钳制、步数收缩、圆/圆角高亮边界、三档自动滚动 |
GuideKitGeometryTest + GuideKitArrowGeometryTest | 16 | 移植保真:锚点取点、方向表、虚线表、弯曲奇偶、颈部封顶、双翼对称、整体几何端点与 null 分支 |
GuideKitBridgeSessionTest | 8 | 桥语义:override 只改显式字段、none 显式清空、默认值逐项相等、导航四态、卡片就位后箭头出现、颜色/hex/JSON 往返 |
GuideKitControllerTest 的断言值与上游 Compose 版完全一致(Rect → GuideKitRect 等价替换,数值一字不改)——这就是「同一份数学,JVM 与 ohos 跑同一份代码」的直接证据。
6.3 HAP 验证
:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: so + libc++_shared.so 回填 entry/libs
llvm-nm -D: GuideKitDispatch / GuideKitVersion 均为全局 T 符号(双 ABI)
CompileArkTS: finished(三轮严格模式修正后通过)
PackageHap: finished
install bundle successfully
start ability successfully
实测产物:arm64-v8a/libohosguidekit.so 4,290,344 B、x86_64/libohosguidekit.so 3,926,552 B(debugShared 未 strip,含 KN 运行时),配套 libc++_shared.so 1,850,184 B / 1,731,432 B。
6.4 功能用例
- 启动后页头出现
1.2.0-ohos.1 · Kotlin/Native ohosArm64 · idle(版本握手来自真实 Native 调用,非静态文字); - 点「开始引导」,遮罩落下、头像被圆形高亮挖孔,箭头自说明卡顶部指向头像下方(Circle 吸附 + padding 6 覆盖);
- 点 Next 到第 2 步:步数卡圆角高亮 + 四层发光,「自动高亮」短语变绿加粗(
descriptionHighlights),步骤点前进; - 第 3 步箭头变点线且双端带箭头(
lineStyle: Dotted+arrowHead: BothSides步骤级覆盖);在卡片上右滑回到第 2 步; - 第 4 步说明卡移到页面顶部(
alignment: TopCenter覆盖),按钮文案自动变Got it;再点 →finished,遮罩消失; - 任意步点右上 ✕ →
skipped,覆盖层消失,页头状态行回显skipped; - 卡片布局变化后
state重取,箭头曲线端点随cardRect更新; - 每次交互页头的最近 op 同步刷新,hilog(TAG
GuideKitNapi)记录请求字节数。
6.5 验证结论
状态机、几何移植、override 镜像、Native 动态库、C ABI、N-API、ArkUI 覆盖层和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四步引导会话按上游语义运转:圆形/圆角高亮、短语高亮、点线双端箭头、卡片换位、右滑回退、跳过与完成的状态行回显(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/guidekit
cd guidekit
export JAVA_HOME="/path/to/jdk-21"
./gradlew compileKotlinOhosArm64 compileKotlinOhosX64 # klib
./gradlew jvmTest :example:nativeApp:jvmTest # 41 项交叉验证
./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.guidekit -m entry
hdc shell hilog | grep GuideKit
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让纯 Kotlin 内核真正编译成
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 2.2.21 旧线必须配 Gradle 8.14.1(连 wrapper jar 一起替换);KStore 的「2.4.20 线可留 Gradle 9.x」经验不能反向套用;
- 不要写
import kotlinx.cinterop.CName——HarmonyOS Kotlin 的默认导入已覆盖,显式 import 反而报 Unresolved; @CName符号 ArkTS 不可见(CAdapter 只挂 stdlib),必须 CMake NAPI 再包一层;KN 返回字符串必须DisposeString归还运行时,N-API 层不能free();- 只拷
libohosguidekit.so不拷libc++_shared.so→ dlopen 失败;triple 必须匹配(aarch64-linux-ohos/x86_64-linux-ohos); - Kotlin/Native 没有
String.format,hex 颜色序列化要手写查表;源码里不要出现裸控制字符字面量(如换页符),用12.toChar()这类写法; - 浮点
-0.0f != 0.0f:垂直向量取负得到负零,data class equals 判不等——几何测试断言要用容差比较; - Compose
Color.copy(alpha)的取整是(α×255+0.5).toInt(),0.68 → 173(0xAD)而不是 174,桥与测试的期望值都要照此算; - ArkTS 严格模式连环坑(DevEco 首编 31 错):匿名对象类型不能作类型声明(
{from,to}[]要抽interface);对象字面量必须有显式接口上下文(请求模型全部接口化);Object.assign属受限标准库(改为字段显式赋值);Area.width/globalPosition.x是Length联合类型(lenToNum收窄);@Builder调用返回 void 不能链式.margin()(margin 移进 builder); - 根
.gitignore的/build/是根锚定,盖不住嵌套目录——example/harmonyApp/{oh_modules,.hvigor,entry/build,entry/libs,entry/.cxx}与*.so要单独追加; build-profile.json5的signingConfigs条目缺material会被 hvigor schema 拒绝(00303038)——仓库内留空数组,勾选自动签名后由 IDE 回填完整配置。
8.2 已知问题
- Compose
GuideKit()Composable 不参与 ohos 编译(社区 CMP 1.9.2 vs 上游 1.11.1);AnimatedContent步骤切换动画、拖拽回弹由 ArkTS 等价交互近似(PanGesture 右滑、animateTo 可后补); - 桥同步一问一答:
onScrollBy以autoScrollDelta数值返回,是否滚动由 ArkTS 决定(Demo 页不滚动,数值进页头状态行);onStepChanged/onSkipped/onFinished对应响应result字段; - 桥模型以
"none"字符串近似上游GuideKitOverride.Value(null)泛型语义;modifier、Shape对象、tonalElevation等强 Compose 字段不跨桥; - android / ios 目标剥离(源文件留在树里),发布配置一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.guidekit匹配的签名。
8.3 未来优化方向
- 补齐
AnimatedContent语义(ArkTSanimateTo平移/淡入),与上游切换动画对帧; - 模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本(
uitest+hdc file recv); - 评估
.har形态发布桥层,让 ArkTS 工程直接依赖而不是手工拷 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 社区 CMP 1.11+ / compose.ui ohos 制品出现后,把
composeMain接回commonMain,桥降级为 fallback。
九、总结
9.1 核心难点回顾
上游 guidekit(纯 Compose UI 库)
→ 路线分岔:compose 制品不可用 → cmp_qrose 模式(内核编译 + 渲染重画)
→ 源码集拆分:状态机/几何/枚举留 commonMain,Compose 实现迁 composeMain
→ 工具链对齐 Kotlin 2.2.21-1.0.0 / Gradle 8.14.1 / JDK 21
→ ohosArm64 / ohosX64 KLIB + 41 项 JVM 交叉验证
→ override 合并镜像(null=继承、none=清空)+ 扁平 JSON 契约
→ C ABI(@CName × 2)+ C++ N-API(DisposeString 纪律)
→ ArkTS 测量上报 + Canvas 几何重画
→ ArkUI 真机四步引导验收(圆形高亮 / 短语高亮 / 点线双端箭头 / 卡片换位 / 右滑回退)
9.2 封装层次
- 引擎层:上游原版
GuideKitController步骤状态机(钳制、四态导航结果); - 几何层:上游公式逐行移植的高亮边界、自动滚动增量、锚点箭头曲线与双翼几何;
- 镜像层:三级 override 合并的字段级纯 Kotlin 镜像(含显式清空语义);
- Native 层:双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:目标测量、请求分发、Canvas 重画、卡片渲染、手势与状态行;
- DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 库的适配面在模型与几何,不在像素:把「算什么」(状态机、样式解析、锚点曲线)和「画什么」(Compose Canvas / ArkUI Canvas)切干净后,纯 UI 库也能走「内核真编译 + 渲染重画」的已审路径——guidekit 补上了这条路在 UI 库上的先例;
- 移植保真靠测试不靠肉眼:17 项上游测试原样保留 + 16 项几何测试钉魔法数 + 8 项桥测试钉 override 语义,三层闸门保证「重画」不会变成「重写」;
- 严格 ArkTS 是最后一公里也是最多坑的一公里:DevEco 首编 31 错全部来自类型严格化(匿名类型、字面量上下文、Length 联合、builder 链式、受限标准库),把请求模型接口化 + 统一的 Length 收窄 + margin 入 builder 三板斧下去即可收敛——这轮坑值得在下一个 CMP 适配仓直接预置。
9.4 适配成果
当前适配已完成:
- GuideKit
1.2.0-ohos.1纯 Kotlin 内核的双 ABI Kotlin/Native 编译(状态机原样、几何逐行移植、零外部依赖); - override 三级解析链的纯 Kotlin 镜像与 2 个 C ABI 符号、C++ N-API 包装层(
llvm-nm验证全局 T 符号); - 41 项 JVM 交叉验证全绿(上游测试保真 + 几何移植测试 + 桥语义测试);
- DevEco Stage ArkUI 四步引导验收页面(圆形/圆角高亮、短语高亮、点线双端箭头、卡片换位、右滑回退、版本握手、状态行回显);
- 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)