本文记录 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 连 jar 一起替换;2.2.21 旧线必须 8.x,KStore 的 2.4.20 线经验不能套用)
依赖零外部依赖——内核只有 kotlin-stdlib,不需要任何 kotlinx 制品线
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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(镜像 compose Offset / 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 方法覆盖内核
GuideKitDispatchguidekitDispatch(req)会话建立 + 三级样式解析 + 几何组装 + 导航
GuideKitVersionguidekitVersion()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.tomlKotlin 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.kt2 个 C ABI、错误通道、会话单例
nativeApp/.../GuideKitBridgeModel.ktoverride 合并镜像 + 颜色/单位约定
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鸿蒙适配主文档(19 节)

4.3 关键 API 对照

层次API作用
引擎GuideKitController.next/previous/skip步骤状态机(上游原件,含钳制与四态结果)
引擎GuideKitController.updateStepCount步数变更钳制(上游原件)
几何resolveGuideKitHighlightBoundspadding 膨胀 + 圆形吸附
几何calculateGuideKitAutoScrollDelta自动滚动增量(含上游默认 spacing)
几何resolveGuideKitArrowGeometry锚点取点、控制点、颈部裁剪、整体曲线
几何arrowHeadWings双翼箭头端点(角度/翼长)
镜像BridgeArrowCfgOverride.merge 等三级 override 合并(null=继承、none=清空)
NativeGuideKitDispatch / GuideKitVersionC ABI JSON 导出
N-APIguidekitDispatch(req) / guidekitVersion()向 ArkTS 暴露能力
ArkTSdispatch(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 项用例全部通过:

测试类用例数钉住什么
GuideKitControllerTest17上游原件:next/previous/skip/finish、空列表、越界钳制、步数收缩、圆/圆角高亮边界、三档自动滚动
GuideKitGeometryTest + GuideKitArrowGeometryTest16移植保真:锚点取点、方向表、虚线表、弯曲奇偶、颈部封顶、双翼对称、整体几何端点与 null 分支
GuideKitBridgeSessionTest8桥语义: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. 启动后页头出现 1.2.0-ohos.1 · Kotlin/Native ohosArm64 · idle(版本握手来自真实 Native 调用,非静态文字);
  2. 点「开始引导」,遮罩落下、头像被圆形高亮挖孔,箭头自说明卡顶部指向头像下方(Circle 吸附 + padding 6 覆盖);
  3. 点 Next 到第 2 步:步数卡圆角高亮 + 四层发光,「自动高亮」短语变绿加粗(descriptionHighlights),步骤点前进;
  4. 第 3 步箭头变点线且双端带箭头(lineStyle: Dotted + arrowHead: BothSides 步骤级覆盖);在卡片上右滑回到第 2 步;
  5. 第 4 步说明卡移到页面顶部(alignment: TopCenter 覆盖),按钮文案自动变 Got it;再点 → finished,遮罩消失;
  6. 任意步点右上 ✕ → skipped,覆盖层消失,页头状态行回显 skipped;
  7. 卡片布局变化后 state 重取,箭头曲线端点随 cardRect 更新;
  8. 每次交互页头的最近 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让纯 Kotlin 内核真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 2.2.21 旧线必须配 Gradle 8.14.1(连 wrapper jar 一起替换);KStore 的「2.4.20 线可留 Gradle 9.x」经验不能反向套用;
  4. 不要写 import kotlinx.cinterop.CName——HarmonyOS Kotlin 的默认导入已覆盖,显式 import 反而报 Unresolved;
  5. @CName 符号 ArkTS 不可见(CAdapter 只挂 stdlib),必须 CMake NAPI 再包一层;KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  6. 只拷 libohosguidekit.so 不拷 libc++_shared.so → dlopen 失败;triple 必须匹配(aarch64-linux-ohos / x86_64-linux-ohos);
  7. Kotlin/Native 没有 String.format,hex 颜色序列化要手写查表;源码里不要出现裸控制字符字面量(如换页符),用 12.toChar() 这类写法;
  8. 浮点 -0.0f != 0.0f:垂直向量取负得到负零,data class equals 判不等——几何测试断言要用容差比较;
  9. Compose Color.copy(alpha) 的取整是 (α×255+0.5).toInt(),0.68 → 173(0xAD)而不是 174,桥与测试的期望值都要照此算;
  10. ArkTS 严格模式连环坑(DevEco 首编 31 错):匿名对象类型不能作类型声明({from,to}[] 要抽 interface);对象字面量必须有显式接口上下文(请求模型全部接口化);Object.assign 属受限标准库(改为字段显式赋值);Area.width / globalPosition.x 是 Length 联合类型(lenToNum 收窄);@Builder 调用返回 void 不能链式 .margin()(margin 移进 builder);
  11. 根 .gitignore 的 /build/ 是根锚定,盖不住嵌套目录——example/harmonyApp/{oh_modules,.hvigor,entry/build,entry/libs,entry/.cxx} 与 *.so 要单独追加;
  12. 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 语义(ArkTS animateTo 平移/淡入),与上游切换动画对帧;
  • 模拟器(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 三条经验

  1. UI 库的适配面在模型与几何,不在像素:把「算什么」(状态机、样式解析、锚点曲线)和「画什么」(Compose Canvas / ArkUI Canvas)切干净后,纯 UI 库也能走「内核真编译 + 渲染重画」的已审路径——guidekit 补上了这条路在 UI 库上的先例;
  2. 移植保真靠测试不靠肉眼:17 项上游测试原样保留 + 16 项几何测试钉魔法数 + 8 项桥测试钉 override 语义,三层闸门保证「重画」不会变成「重写」;
  3. 严格 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。

参考文档

Logo

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

更多推荐