开源鸿蒙平台 CMP 三方库「swipe」适配全流程
本文记录
swipe接入 OpenHarmony 的完整过程,覆盖滑动露出动作手势库盘点、鸿蒙 Compose 制品线现状确认、手势引擎提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
swipe是 Compose Multiplatform 的「滑动露出动作」手势库:SwipeableActionsBox/SwipeToReveal让列表项可以左右滑动露出不删除自身的动作(归档、稍后提醒、删除确认),与 MaterialSwipeToDismiss形成互补;SwipeAction提供weight权重与isUndo涟漪方向,内置的horizontalDraggable在触 slop 时以 22.5° 角度过滤垂直误触。本次适配与存储类库(KStore 的「换线不换版、整库真编译」)是同一套方法论的两面:鸿蒙侧compose.ui制品线尚未追上上游(Kotlin 2.3 / Compose 1.10),因此上游手势引擎的全部数学——权重分配、橡皮筋阻力、阈值判定、松手触发、400ms 回弹曲线、双通道涟漪——被逐行提取为纯 Kotlin,原样编译成 OpenHarmony KLIB 与双 ABI 动态库,ArkTS 页面通过单一 C ABI 逐帧驱动引擎并渲染结果,设备上执行的滑动数学与 Android 上执行的滑动数学逐位一致,JVM 侧 18 项交叉验证测试钉住了这件事。这是 CPF-KMP-CMP 社区路线(qrose / Flippable / Vico / Reorderable)第一次落到实时手势类库上:引擎不是一次性计算(二维码编码、存储事务),而是每帧被手势事件驱动——「无状态 C ABI + 调用方持状态」的协议形态在 60fps 场景同样成立。
项目地址: AtomGit/oh-tpc/swipe.git
上游仓库: saket/swipe
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
swipe 为 Kotlin Multiplatform 应用提供"列表项左右滑动露出动作"这一件事的完整答案:SwipeableActionsBox 上内容随手指横移,背后按权重切分宽度铺动作背景与图标;越过阈值(swipeThreshold,默认 40dp)的瞬间占位色切换为动作色并触发触觉反馈;松手时若越过阈值则命中「当前露出的动作」执行 onSwipe 并播放涟漪,随后 400ms 回弹归位;朝没有动作的一侧拖拽会进入 delta / 10 的橡皮筋阻力。上游 v1 已发布为 me.saket.swipe2:swipe2:1.3.0,本 fork 基于 trunk@eb0dde0(v2 SwipeToReveal Gmail 风格圆形动作的起草版)。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套滑动交互能力,但上游构建(Kotlin 2.3.10 / AGP 8.13.2 / Compose 1.10.2)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以滑动,却无法证明手势引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行——滑动手感是逐帧数学(权重边界、橡皮筋增量、贝塞尔采样),任何一处自作主张的重写都会造成与 Android 端可感知的差异。适配需要同时验证手势引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.3.10 / AGP 8.13.2 / paparazzi 截图测试与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵 | 上游 commonMain 直接 implementation(compose.ui/foundation),而社区鸿蒙 CMP 制品线是 1.9.2,Kotlin 2.3 / Compose 1.10 的 androidx.compose.* 没有 ohos 变体——这是本库与存储类库最大的不同。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 手势语义跨不了桥 | Animatable、DraggableState、mutableStateOf 驱动的 Compose 重组是 Kotlin 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:手势引擎数学的 ohos 编译(逐行提取而非重写)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。滑动语义完全由上游数学负责,ArkTS 只负责采集手势、调用 Native、解析 JSON 和渲染结果。
1.2 库提供的能力
Demo 提供一个真实的手势会话,对应桥接的全部入口:
- 拖拽(drag):
PanGesture每帧增量喂给引擎,引擎按上游语义返回权威偏移(朝空侧delta/10橡皮筋)、命中动作(侧别/序号/分得宽度)、是否越过阈值、背景色与图标锚点; - 松手(release):引擎判定是否触发动作(越阈值且命中),返回触发结果与回弹起点;
- 回弹(settle):按 Compose
tween(400, FastOutSlowInEasing)的采样曲线逐帧返回偏移,400ms 归零; - 涟漪(ripple):按上游双通道动画(进度 0→1;非 undo 透明度 0.25→0 延迟半程,undo 0→0.5 且时长 ×1.75=700ms)逐帧返回圆形涟漪几何;
- 布局(layout):按权重切分行宽,返回每个动作的分得像素宽;
- 版本握手(info):
SwipeStep({action:"info"})返回 fork 版本号与引擎能力清单,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
offset / crossed | 引擎权威偏移与阈值状态(drag/settle)。 |
side / index / actionWidth | 当前露出的动作及其分得宽度。 |
background | 背景色(占位色 → 动作色 → 透明的上游次序),"" 即透明。 |
iconX | 动作图标锚点(钉在露出边缘,不随内容移动)。 |
triggered / settleFrom | release 的触发判定与回弹起点。 |
progress / alpha / radius / centerX | ripple 的逐帧几何。 |
引擎侧完整能力(权重分配循环、橡皮筋增量、严格大于的阈值、handleOnDragStopped 触发语义、背景色 when 次序、图标 layout 锚定、贝塞尔求解)全部来自上游 ActionFinder / SwipeableActionsState / SwipeableActionsBox / SwipeRippleState 的逐行提取并随编译进入 .so,JVM 侧保留 18 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的滑动数学没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 10 个源文件零改动:1 个纯 Kotlin(defaults.kt)原样留在 commonMain,9 个 Compose 文件 git mv 到 src/composeMain(隔离不编译);引擎 SwipeEngine.kt + SwipeActionSpec.kt 为新增,每个函数带上游出处注释。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 Android sample/、paparazzi 截图测试、GitHub CI 留盘不进构建。 |
| 桥接稳定 | 使用 1 个 C ABI(SwipeStep)和 UTF-8 JSON,不传递 Kotlin 对象地址;引擎无状态,异常永远不跨 FFI。 |
| 依赖纪律 | 引擎零第三方依赖(不需要 kotlinx 制品线,手势数学只吃 kotlin.math),版本主体与社区标准一致(鸿蒙 Kotlin 2.2.21-1.0.0)。 |
| UI 完整 | 三 Tab 覆盖全部桥接入口:收件箱(双侧动作、权重 1:1.5、涟漪、回弹)、参数实验室(权重/阈值/undo/RTL/橡皮筋实测)、关于(握手、架构、日志)。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。swipe 的价值在手势引擎的数学与语义,Compose UI 隔离在
composeMain待社区制品线就绪后回归;ArkTS Demo 把桥暴露的逐帧手势闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 compose.ui ohos 制品线现状、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:桥协议建模 ── 无状态单 C ABI、六个 action 的 JSON 契约、增量 delta 约定、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、extern "C" 纪律和内存释放
第 5 阶段:ArkUI 宿主 ── 版本握手、PanGesture 增量换算、16ms 定时器采样、三 Tab 页面
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
library/src/commonMain/ 引擎:defaults.kt(上游原样)+ SwipeEngine.kt + SwipeActionSpec.kt(新增)
library/src/composeMain/ 上游 Compose UI(v1 六个文件 + v2 三个文件,git mv 隔离,不编译)
library/src/jvmTest/ 引擎交叉验证(18 项,新增)
library/src/test/ 上游 paparazzi 截图测试(留盘不进构建)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
sample/ 上游 Android 示例,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library 与 :example:nativeApp,rootProject.name = "ohos_swipe"。上游的 AGP classpath、compose 插件、vanniktech 发布、paparazzi、dokka、throwIfVersionIsNotSnapshot CI 门禁全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin,org.gradle.java.home 指向 C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 8.13 升一档,与社区标准一致;2.2.21 线支持 Gradle 8.x) |
| 引擎依赖 | 零(手势数学只吃 kotlin.math,不需要 kotlinx 制品线) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.swipe |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 位置 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
commonMain | defaults.kt | 纯 Kotlin(400ms 常量) | 原样保留 |
commonMain | SwipeableActionsBox.kt 等 v1 六件 | androidx.compose.*(runtime/foundation/animation/ui 全占了) | git mv 到 composeMain |
commonMain | swipe2/ v2 三件 | androidx.compose.* + 引用 v1 的 horizontalDraggable | git mv 到 composeMain |
commonMain(新增) | SwipeEngine.kt / SwipeActionSpec.kt | 零依赖纯 Kotlin | 引擎落点 |
值得单独说明的是 ActionFinder.kt:它本体就是纯数学(权重分配 + 命中测试,只 import 了 kotlin.math.abs),但它持有 SwipeAction 这个 Compose 类型(@Composable 图标 + Color 背景),只能随 UI 一起隔离。引擎侧重新落了一份逐行对齐的实现,连上游 offsetSoFar += actionEndX 的累积写法(4+ 个动作时边界偏移的疑似笔误)都原样保留——两侧逐位一致优先于「顺手修 bug」,真要修先给上游提 PR。
horizontalDraggable 的处置也说明手势库适配的一个特点:它是上游为规避 Google issuetracker 269627294 而内置的 Modifier.Node 拖拽实现(22.5° 角度过滤),这层的职责是「方向消歧」,鸿蒙侧由 ArkUI PanGesture(PanDirection.Horizontal) 原生承担,引擎只消费「每帧增量 delta」——手势采集换宿主,数学不换。
1.4 页面边界
标题区 swipe · OpenHarmony Demo / 状态行(Kotlin 引擎已连接 · swipe 1.4.0-ohos.1 · Kotlin 2.2.21-1.0.0)
Tab 切换 收件箱 / 参数实验室 / 关于
收件箱 8 封中文邮件卡片;左滑露「稍后提醒 + 删除(权重 1.5)」、右滑露「归档」;
越阈值图标脉冲,触发后打"已归档"标签 + 删除线,涟漪扩散后 400ms 回弹
参数实验室 权重 chips(1,1 / 1,1.5 / 1,2)+ 分段宽度条 / 阈值 Slider 20–200px /
isUndo 与 RTL 开关 / 橡皮筋读数行(raw 拖拽 X px → 引擎 offset Y px)
关于区 版本握手、库简介、架构分层、仓库与社区链接
日志区 带时间戳的操作日志(最新在前,上限 100 条)
页面启动时先调 SwipeStep({action:"info"}) 做握手,把 fork 版本、Kotlin 版本、引擎能力清单写进状态行。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的路线分岔点)
与 KStore 适配相反的验证结论:先查社区 Maven(https://maven.eazytec-cloud.com)上 androidx.compose.ui:foundation / org.jetbrains.compose 有没有 ohos 变体。结论是没有——社区鸿蒙 CMP 制品线停留在 1.9.2,而本 fork 上游是 Kotlin 2.3.10 / Compose 1.10.2 / AGP 8.13.2,矩阵不可混。这一步把适配路线钉死在「引擎提取」:
- 若制品齐(如 KStore 的 kotlinx 四件套)→ 整库真编译,只补 actual;
- 若制品缺(swipe 的 compose.ui)→ 把数学从 Compose 文件里逐行提取成纯 Kotlin,UI 隔离等待制品线。
另外一个好消息:提取出来的手势引擎零第三方依赖——权重分配是 Double 运算,回弹曲线是手搓贝塞尔,连 kotlinx-coroutines 都不用(上游用 Animatable.animateTo 挂起驱动,引擎侧改为按时间采样)。这意味着本适配不受任何 kotlinx 制品线版本牵制。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
引擎零依赖,没有 stdlib 元数据兼容问题,直接沿用社区批准的标准钉法(与 qrose / Flippable 同线):HarmonyOS Kotlin 2.2.21-1.0.0、Gradle 8.14.1(上游 wrapper 8.13 升一档)、JDK 21。上游源码里的 Kotlin 2.3 特性全部在被隔离的 Compose 文件里,进 commonMain 的引擎代码用 2.2 语法写,零降级成本。
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()
}
}
rootProject.name = "ohos_swipe"
include(":library")
include(":example:nativeApp")
2.3 编译真实引擎
library/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 引擎零依赖
jvmTest.dependencies {
implementation(kotlin("test"))
}
}
}
注意库模块不开 binaries.sharedLib——动态库只在 example/nativeApp 里出一份,避免 Kotlin/Native 运行时被复制多次。实测:
./gradlew.bat :library:compileKotlinJvm :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64
# BUILD SUCCESSFUL
上游源码一个没动:9 个 Compose 文件 git mv 进 composeMain(普通目录,不注册为任何 target 的 source set),defaults.kt 原样保留,新增的只有引擎两个文件。
2.4 测试的处置:paparazzi 截图测试没法在鸿蒙构建里跑
上游测试是 paparazzi 截图测试(7 张黄金 PNG,依赖 AGP + Robolectric),在极简 ohos 构建里没有宿主。处置是两条纪律的组合:测试统一在 JVM 跑(新写 jvmTest 数值钉死),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
上游 paparazzi 文件零改动留盘(library/src/test/),ohos 侧只承担编译与链接。交叉验证的有效性靠「数值钉死」保证:上游截图测试验证的是渲染结果,鸿蒙侧没有渲染面,就改为把渲染背后的每个数学断言写死(见 6.2)。
第 3 阶段:桥协议建模
3.1 JSON 契约
me.saket.swipe/**(上游数学,逐行提取进 SwipeEngine)
↓ actionAt / advanceDrag / settleOffset / rippleFrame / backgroundColor / iconAnchor
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API swipeStep(request)
↓
ArkUI Index.ets(三 Tab,16ms 采样驱动渲染)
六个 action 的请求与响应:
请求 : {"action":"info"}
{"action":"layout","width":1044,"weights":"1,1.5"}
{"action":"drag","offset":-123.4,"delta":-6.2,"width":1044,"threshold":120,
"iconWidth":96,"leftWeights":"1","leftColors":"#008A36",
"rightWeights":"1,1.5","rightColors":"#F9A825,#D93025",
"leftUndo":"0","rightUndo":"0,1","placeholder":"#FF444746"}
{"action":"release",...同 drag 去 delta/iconWidth...}
{"action":"settle","from":-456.7,"elapsed":160,...动作集字段...}
{"action":"ripple","elapsed":120,"isUndo":false,"rightSide":true,"width":1044,"height":132}
响应 : {"ok":true,"offset":-129.6,"side":"right","index":0,"actionWidth":417.6,
"crossed":true,"background":"#FFF9A825","isUndo":false,"label":"","iconX":914}
{"ok":false,"error":"..."}
几个不对称设计:动作集(权重/颜色/undo)用逗号串编码而不是嵌套数组——与 qrose / Flippable 的 MiniJson 约定一致,桥上的 JSON 解析器保持 ~50 行手写、零依赖;颜色 #RRGGBB / #AARRGGBB 进、统一 #AARRGGBB 出,background: "" 表示透明(上游 Color.Transparent);浮点统一两位小数定点输出,避免科学计数法跨桥。
3.2 无状态设计(60fps 的关键)
引擎不持有任何状态——offset、阈值、动作集全部由调用方每帧回传,C ABI 永远是纯函数。这样做的三个理由:
- 手势事件天然自带全量上下文(当前 offset + 本帧 delta + 动作集),引擎不需要记忆;
- ArkTS 侧的状态可序列化、可恢复(页面旋转/重建不丢手势状态),Native 侧零内存生命周期问题;
- 单帧开销 = 一次 JSON 往返 + 纯数学,实测远低于 16ms 预算,不需要 worker 线程。
配套的增量约定:PanGesture.onActionUpdate 给的是自手势开始的累计 event.offsetX,ArkTS 侧必须自减出本帧增量再喂引擎——这是引擎 advanceDrag(offset, delta, ...) 的上游语义(上游 DraggableState 收到的就是增量),Demo 里用 lastPanX 成员完成换算,onActionStart 归零。
3.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("SwipeStep")
fun swipeStep(request: String): String = try {
when (json.str("action")) {
"info" -> info(); "layout" -> layout(json); "drag" -> drag(json)
"release" -> release(json); "settle" -> settle(json); "ripple" -> ripple(json)
else -> err("unknown action")
}
} catch (t: Throwable) {
err(t.message ?: t.toString())
}
非法 JSON、缺字段、未知 action、权重非法(引擎 require(weight > 0.0) 与上游同款守卫),全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行与日志。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libswipe_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosswipe.so)
│ SwipeStep
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;让 ArkTS 复制一份滑动数学又会造成漂移——所以选择 C ABI + JSON,与 qrose / Flippable / KStore 同款。
4.2 Kotlin/Native 导出函数
@CName("SwipeStep")
fun swipeStep(request: String): String // info / layout / drag / release / settle / ripple 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
SwipeStep | swipeStep(req) | 引擎全部入口:权重分配、拖拽增量、阈值、触发判定、回弹采样、涟漪帧、握手 |
llvm-nm -D libohosswipe.so 确认 SwipeStep 与 libohosswipe_symbols 均为全局 T 符号。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"swipeStep", nullptr, NapiSwipeStep, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosswipe_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosswipe_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与一个业务符号)——且必须带 extern "C" {} 块:本次实测漏掉后链接器直接报 undefined symbol: SwipeStep(char const*)(C++ 名字修饰),lld 的提示 did you mean: extern "C" SwipeStep 就是答案。
4.4 CMake 和类型声明
add_library(ohosswipe SHARED IMPORTED)
set_target_properties(ohosswipe PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(swipe_napi SHARED napi_init.cpp)
target_link_libraries(swipe_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosswipe)
OHOS_ARCH 为空或 libohosswipe.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见,Windows 构建机的绝对路径不会写进 DT_RUNPATH。
export const swipeStep: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 握手和逐帧驱动
aboutToAppear(): void {
const res = JSON.parse(swipeStep(JSON.stringify({ action: 'info' }))) as InfoRes;
this.status = `Kotlin 引擎已连接 · swipe ${res.fork} · Kotlin ${res.kotlin}`;
}
private callDrag(delta: number): void {
const res = JSON.parse(swipeStep(JSON.stringify({
action: 'drag', offset: this.swipeOffset, delta: delta, width: this.rowWidth,
threshold: this.threshold, iconWidth: this.iconWidth,
leftWeights: this.leftWeights, leftColors: this.leftColors,
rightWeights: this.rightWeights, rightColors: this.rightColors,
placeholder: PLACEHOLDER }))) as DragRes;
this.swipeOffset = res.offset ?? this.swipeOffset; // 引擎是唯一权威
this.bgColor = res.background ?? '';
this.iconX = res.iconX ?? -1;
}
拖拽中 PanGesture.onActionUpdate 先算本帧增量(event.offsetX - this.lastPanX)再 callDrag;松手 onActionEnd 调 release,触发则起两个 16ms setInterval 分别采样 settle 与 ripple(undo 涟漪 700ms 比回弹 400ms 长,两个定时器独立生命周期),done 或新手势开始时统一 clearInterval。行宽经 onAreaChange 实测注入,不写死分辨率。
5.2 页面交互
收件箱 Tab:8 封中文邮件,每行三层 Stack——引擎背景色层、按 iconX 锚定的动作图标层(钉在露出边缘)、.translate({ x: swipeOffset }) 的白色卡片层,行容器 .clip(true) 防溢出;右滑露「归档」(绿 #008A36),左滑露「稍后提醒」(黄 #F9A825,权重 1)+「删除」(红 #D93025,权重 1.5,更宽的一段就是权重语义的可视化);越阈值的瞬间图标脉冲放大;触发后行内打"已归档"标签 + 主题删除线,涟漪从露出边缘扩散盖过内容再回弹归位。参数实验室 Tab:权重 chips 切换即调 layout 刷新分段宽度条;阈值 Slider 实时改 threshold;isUndo 开关让左侧动作变 undo(涟漪反向收缩、700ms);RTL 开关按上游 ActionFinder 的映射交换左右动作列表;橡皮筋行实时读出「raw 拖拽 X px → 引擎 offset Y px」。关于 Tab:握手明细、ArkTS → NAPI → C ABI → Kotlin 引擎 分层说明、仓库/社区链接与操作日志。
5.3 对比度和错误边界
动作色沿用上游 Gmail 示例语义:归档绿、稍后提醒黄、删除红,破坏性与颜色对齐;占位色用上游默认的 Material 深灰 #FF444746;ok:false 时错误原文进状态行与日志。涟漪层 hitTestBehavior(HitTestMode.Transparent),不遮挡卡片手势。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosswipe)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 1 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.swipe、versionName 1.4.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/libohosswipe_api.h
├── src/main/cpp/types/libswipe_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面(三 Tab)
6.2 构建、签名和安装
.\gradlew.bat :library:jvmTest # 18 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 `
:example:nativeApp:linkDebugSharedOhosX64
cd example\harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 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.swipe -m entry
四、完整代码对照
4.1 整体架构
me.saket.swipe(上游数学逐行提取)
│ SwipeEngine:actionWidths / actionAt / advanceDrag / hasCrossedSwipeThreshold
│ actionToTriggerOnRelease / backgroundColor / iconAnchor
│ settleOffset / rippleFrame(+ FastOutSlowIn 贝塞尔求解)
SwipeActionSpec(Compose-free 动作模型:ARGB Long / weight / isUndo)
│
Bridge.kt + MiniJson
│ SwipeStep(单 C ABI,六 action 分发)
libswipe_napi.so
│ swipeStep
Index.ets
│ JSON parse + @State 更新 + 16ms 采样
▼
ArkUI 真机页面(三 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_swipe、sample 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 钉版(仅此一条) |
build.gradle.kts(根) | 版本 1.4.0-ohos.1、全局禁用 Ohos*Test* 任务 |
library/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,库模块不开 sharedLib |
library/src/commonMain/.../SwipeEngine.kt | 引擎(每个函数带上游出处注释,含贝塞尔求解器) |
library/src/commonMain/.../SwipeActionSpec.kt | Compose-free 动作模型(上游 SwipeAction 的镜像) |
library/src/jvmTest/.../SwipeEngineTest.kt | 18 项数值钉死交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、六 action 分发、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/include/libohosswipe_api.h | 手写 CAdapter 镜像头(extern “C”) |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(收件箱/参数实验室/关于) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(17 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | actionWidths(totalWidth, actions) | 按 weight/totalWeights 切分行宽(上游分配循环) |
| 引擎 | actionAt(offset, width, left, right) | 命中测试:正偏移露左侧、负偏移露右侧,coerceAtMost(totalWidth) 封顶 |
| 引擎 | advanceDrag(offset, delta, ...) | 拖拽增量:空侧 delta/10 橡皮筋,回弹中全量,回零永远放行 |
| 引擎 | hasCrossedSwipeThreshold(offset, thresholdPx) | abs(offset) > thresholdPx,严格大于 |
| 引擎 | actionToTriggerOnRelease(...) | 松手触发判定(越阈值且命中) |
| 引擎 | backgroundColor(...) | 上游 when 次序:swiped > 占位 > 可见 > 透明 |
| 引擎 | iconAnchor(offset, side, width, iconWidth) | 图标钉在露出边缘(上游 ActionIconBox layout) |
| 引擎 | settleOffset(from, elapsedMs) | 400ms 回弹,FastOutSlowInEasing 逐帧采样 |
| 引擎 | rippleFrame(elapsedMs, isUndo, ...) | 双通道涟漪:进度/透明度/半径/圆心 |
| Native | SwipeStep | C ABI JSON 导出(单符号覆盖全部入口) |
| N-API | swipeStep(request) | 向 ArkTS 暴露能力 |
| ArkTS | callDrag(delta) / startSettle() / startRipple() | 手势分发与定时采样 |
4.4 ArkTS 与 Kotlin 的边界
const delta: number = event.offsetX - this.lastPanX; // 累计 → 增量
this.lastPanX = event.offsetX;
const res = JSON.parse(swipeStep(JSON.stringify({
action: 'drag', offset: this.swipeOffset, delta: delta, width: this.rowWidth,
threshold: 120, iconWidth: 96,
leftWeights: '1', leftColors: '#008A36',
rightWeights: '1,1.5', rightColors: '#F9A825,#D93025',
placeholder: '#FF444746' }))) as DragRes;
// res.offset 是唯一权威;res.iconX 把图标钉在露出边缘;res.crossed 驱动脉冲反馈
val offset = SwipeEngine.advanceDrag(
offset = json.float("offset", 0f), delta = json.float("delta", 0f),
canSwipeTowardsRight = left.isNotEmpty(), canSwipeTowardsLeft = right.isNotEmpty())
val hit = SwipeEngine.actionAt(offset, width, left, right)
val background = SwipeEngine.backgroundColor(offset, threshold, width, left, right, placeholder)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:引擎提取,而不是 ArkTS 重写、也不是等待 compose.ui 制品
上游 commonMain 九成文件 import androidx.compose.*,社区鸿蒙 CMP 制品线(1.9.2)追不上上游(Compose 1.10),「整库真编译」路线在本库不可行。反过来,若只用 ArkTS 重写交互,设备上跑的就不是上游引擎,滑动手感无法与 Android 端对齐,上游修 bug 也同步不回来。提取的边界划在「数学」:权重分配、橡皮筋、阈值、触发、回弹、涟漪全部进引擎,Compose 状态与渲染留给制品线成熟后回归。连上游 ActionFinder 的 offsetSoFar += actionEndX 疑似笔误都原样保留——两侧逐位一致是第一优先级。
决策 2:工具链沿用社区标准钉法(Kotlin 2.2.21-1.0.0 + Gradle 8.14.1)
引擎零第三方依赖,没有 stdlib 元数据牵制,直接与 qrose / Flippable 同线。上游源码的 Kotlin 2.3 特性全在被隔离的文件里,进 commonMain 的引擎代码按 2.2 语法书写,零降级成本;Gradle 从上游 8.13 升一档到 8.14.1 对齐社区标准。
决策 3:根构建替换(cmp_qrose 模式),composeMain 隔离而非删除
上游根构建(AGP classpath / compose 插件 / vanniktech / paparazzi / dokka / CI 门禁)对鸿蒙没有保留价值,整体替换;但上游 Compose 文件隔离而非删除——git mv 到 src/composeMain 保留 100% rename 历史,上游后续演进可 diff 可合并,社区 compose.ui ohos 制品就绪后整体移回 commonMain 即可。sample/ 与 paparazzi 测试同法留盘。
决策 4:无状态单 C ABI + 扁平 JSON
单符号 SwipeStep 覆盖六个 action,替代为每个操作开一个导出:协议面越小,ArkTS 侧集成越薄,加 action 只是桥内加一个分支,C 签名永不变。引擎无状态让 C ABI 永远是纯函数——这对手势库尤其重要,每帧调用、零跨 FFI 生命周期,60fps 场景实测单帧 JSON 往返 + 纯数学远低于 16ms 预算。
决策 5:jvmTest 数值钉死,替代上游截图测试
上游 paparazzi 验证渲染结果,ohos 构建里没有宿主;鸿蒙侧 Demo 的渲染是 ArkTS,不是验证对象。交叉验证改为把渲染背后的每个数学断言写死:权重切分(432/648 @ 1:1.5)、命中边界(±432/433)、橡皮筋增量、严格大于的阈值、背景色次序、图标锚点、回弹中点(-300 × (1 − 0.7754) ≈ −67.4,常数由手解贝塞尔 t ≈ 0.6933, y = 3t² − 2t³ 得出,修正过一次想当然的 0.8175)、涟漪 400/700ms 与透明度通道。截图测试没法跑,数值就是合同。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11(x86_64)、JDK 21(Temurin)、DevEco Studio 5.1.0 Release(内置 hvigor 6.26.8 / ohpm / Node 24 / hdc)、OpenHarmony 命令行工具与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :library:jvmTest
18 项用例全部通过(新增数值钉死用例):
| 断言组 | 用例数 | 钉住什么 |
|---|---|---|
| 权重切分 | 2 | 1:1.5 → 432/648(1080px);weight ≤ 0 拒绝(上游 require 同款) |
| 命中测试 | 4 | 零偏移无动作;正负偏移侧别;权重边界 ±432/433;超界 clamp 到末位动作 |
| 橡皮筋 | 4 | 空侧 delta/10 且逐帧累积;回弹中全量;回零放行 |
| 阈值/触发 | 2 | 严格大于;越阈值且命中才触发 |
| 背景色/图标 | 3 | when 次序(swiped > 占位 > 可见 > 透明);两侧锚点算术 |
| 回弹曲线 | 1 | t=0/400/超时端点 + 中点 ≈ −67.4 + 全程单调趋零 |
| 涟漪 | 2 | 400/700ms 时长;双通道透明度(延迟半程/undo 0.5)、半径起止与圆心两侧锚点 |
上游截图测试(7 张黄金 PNG)留盘不进构建,其数值面已全部被上述断言覆盖。
6.3 HAP 验证
:library:compileKotlinJvm / OhosArm64 / OhosX64: BUILD SUCCESSFUL
:library:jvmTest: 18 项全绿
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: SwipeStep / libohosswipe_symbols 均为全局 T 符号
BuildNativeWithNinja: finished(双 ABI 编出 libswipe_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 6.88 MB,内含双 ABI 各三件套(libswipe_napi.so / libohosswipe.so / libc++_shared.so);HAP 内 strip 后 KN so arm64 约 1.96MB、x86_64 约 1.91MB(debugShared 未 strip 时分别约 3.71MB / 3.41MB,含 Kotlin/Native 运行时),NAPI 包装层约 65KB。
6.4 功能用例
- 启动后状态行出现
Kotlin 引擎已连接 · swipe 1.4.0-ohos.1 · Kotlin 2.2.21-1.0.0(来自真实 Native 调用,非静态文字); - 收件箱右滑露出绿色「归档」,继续拖过阈值背景由深灰占位色切换为动作绿,图标脉冲放大;
- 松手越过阈值:行内打"已归档"标签 + 主题删除线,涟漪从露出边缘扩散盖过内容,700ms(undo)或 400ms 后回弹归位;
- 左滑露出黄色「稍后提醒」与红色「删除」,删除段明显更宽(权重 1:1.5 的可视化),参数实验室的分段宽度条给出精确像素;
- 朝空侧拖拽橡皮筋行实时读出「raw 拖拽 320 px → 引擎 offset 32 px」(delta/10);
- isUndo 开关让涟漪反向收缩且时长变 700ms;RTL 开关交换左右动作列表(上游
ActionFinder映射); - 阈值 Slider 20–200px 实时改变触发手感;
- 每次触发/越阈值/参数变更按时间戳滚动进操作日志。
6.5 验证结论
手势引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的手势会话:版本握手、权重分配、橡皮筋、阈值切换、动作触发、涟漪反馈与 400ms 回弹闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/swipe.git
cd swipe
export JAVA_HOME="/path/to/jdk-21" # Windows: set JAVA_HOME=...
./gradlew :library:compileKotlinJvm :library:compileKotlinOhosArm64 # klib
./gradlew :library:jvmTest # 18 项交叉验证
./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.swipe -m entry
hdc shell hilog | grep SwipeNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:library真正编译成ohosArm64/ohosX64——对手势库,还要保证编译进去的是上游数学而不是重写; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;同时必须删除旧的 groovysettings.gradle,两个 settings 文件并存时 Gradle 优先读 groovy 版,新配置静默失效;- 手写 CAdapter 镜像头漏
extern "C" {}时,编译不报错,链接期才报undefined symbol: SwipeStep(char const*)——lld 提示did you mean: extern "C"就是答案,Flippable 的头文件是现成范本; PanGesture.onActionUpdate给的是自手势开始的累计offsetX,直接喂引擎会把橡皮筋量级放大一个数量级,必须先自减出本帧增量;- ArkTS 严格模式:自定义组件的
@State成员不能叫offset/background——与通用属性方法冲突,编译器报not assignable to the same property in base type 'CustomComponent',改名swipeOffset/bgColor即可;Stack的alignContent要Alignment.Start而不是HorizontalAlign.Start; Circle().fill()在 compat 5.1.0(18) 上是 API 26 兼容性告警,涟漪圆改用borderRadius的Column实现零告警;- Windows 命令行跑 hvigor 需要三件套齐备:
NODE_HOME指向 DevEco 自带 Node(否则 ohpm 报Failed to find the executable 'node')、DEVECO_SDK_HOME指向DevEco Studio/sdk(否则 hvigor 报Invalid value of 'DEVECO_SDK_HOME')、DevEco 的 JBRjava进 PATH(否则 PackageHap 报spawn java ENOENT); - 回弹曲线的参考常数不能想当然:第一版测试按 0.8175 断言失败,实测
CubicBezier(0.4,0,0.2,1)在 x=0.5 处 y≈0.7754(手解 t≈0.6933, y=3t²−2t³)——求解器是对的,记忆是错的,交叉验证的意义就在这; - Kotlin 里
"{"ok":true}".json()这类花括号块不是合法语法,响应一律老实写字符串模板; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();DevEco 构建还会在 harmonyApp 下生成.clang-tidy/.clangd,要进 gitignore,别混进提交; - 签名材料由 DevEco 自动写入
build-profile.json5,仓库内保持空数组,证书绝不进入 AtomGit。
8.2 已知问题
- Compose UI(
SwipeableActionsBox/SwipeToReveal)未编译,隔离在composeMain待社区compose.uiohos 制品就绪后回归; - 触觉反馈未接:上游在越阈值时
HapticFeedbackType.LongPress,鸿蒙接振动器需ohos.permission.VIBRATE,Demo 以图标脉冲替代,引擎契约(crossed字段)已备好; - 上游
ActionFinder对 4+ 个动作的边界笔误(offsetSoFar += actionEndX)原样保留以保持两侧一致,上游修复后跟随; - 手势采集换宿主:
horizontalDraggable的 22.5° 角度过滤由PanGesture(PanDirection.Horizontal)的方向消歧等效替代;上游Animatable的重组驱动改为 ArkTS 16ms 定时采样,语义(400ms/曲线/undo 时长)不变; - 上游 paparazzi 截图测试不在本构建(留盘),渲染验证由真机人工验收兜底;
- android / ios 目标剥离(源文件留在树里),发布、dokka 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.swipe匹配的签名。
8.3 未来优化方向
- 跟随上游 v2 演进:
SwipeToRevealState锚定语义稳定后并入引擎(桥协议加字段即可,C 签名不变); - 社区
compose.uiohos 制品就绪后,评估composeMain整体移回commonMain,鸿蒙直接吃原生 Compose UI; - 补模拟器(x86_64)验收矩阵与 uitest 自动化截图脚本;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 评估把
crossed接上鸿蒙振动器(需权限声明)完整还原上游触觉语义。
九、总结
9.1 核心难点回顾
上游 swipe(10 个文件,九成是 Compose)
→ 确认 compose.ui ohos 制品线缺席 → 路线钉死「引擎提取」
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1)
→ 根构建替换(cmp_qrose 模式)+ composeMain 隔离(git mv 保历史)
→ SwipeEngine 逐行提取(权重/橡皮筋/阈值/触发/回弹/涟漪,含贝塞尔求解器)
→ ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
→ C ABI(@CName × 1,无状态纯函数)六 action JSON 协议
→ C++ N-API(extern "C" 纪律 + DisposeString 纪律)
→ ArkTS PanGesture 增量换算 + 16ms 采样
→ ArkUI 真机三 Tab 验收(收件箱 / 参数实验室 / 关于)+ 18 项 jvmTest 钉数值
9.2 封装层次
- 引擎层:上游数学的逐行移植——
ActionFinder分配与命中、DraggableState橡皮筋、阈值与触发、tween(400)贝塞尔采样、SwipeRippleState双通道帧; - 模型层:
SwipeActionSpec(ARGB Long / weight / isUndo)作为 ComposeSwipeAction的镜像,跨 FFI 唯一载荷; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:手势采集与增量换算、页面状态、定时采样、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 制品可达性定路线,且要按库重新验证:KStore 适配时 kotlinx 四件套有 ohos 变体,答案是「换线不换版、整库真编译」;swipe 的
compose.ui制品缺席,答案就变成「数学提取、UI 隔离」。两条路线共用同一套骨架(根构建替换 + 双 ABI + C ABI + NAPI + ArkUI Demo + JVM 交叉验证),分岔只发生在第 2.1 节那一次查询——历史结论不能直接套用,当天的制品可达性才是依据; - 手势库的适配面 = 数学 + 宿主替换:把「谁采手势、谁持状态、谁渲染」换宿主(ArkUI Pan / ArkTS @State / ArkUI 组件),把「算什么」留在 Kotlin。无状态纯函数桥在每帧调用的 60fps 场景实测成立,这为列表滚动、拖拽排序等更高频的手势库探了路;
- 交叉验证要随验证对象变形:存储库的证明是「上游测试零改动全绿」(语义没漂),手势库上游测试是截图渲染,没法在 ohos 构建里跑,证明就换成「数值钉死」——权重边界、贝塞尔中点、涟漪时长全部写成断言。形式可以变,「有可复现的数字」不能变,连 0.7754 这种常数都要经得起手解贝塞尔的推敲。
9.4 适配成果
当前适配已完成:
- swipe trunk@
eb0dde0(v2 起草版)手势引擎的双 ABI Kotlin/Native 编译(上游数学逐行提取,9 个 Compose 文件 git mv 隔离零改动,连上游笔误都保持逐位一致); - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号),无状态六 action JSON 协议; - DevEco Stage ArkUI 三 Tab 验收页面(收件箱手势闭环、权重/阈值/undo/RTL/橡皮筋实验室、版本握手与日志);
- 18 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so,约 6.88MB)、签名、设备安装和真机验证;
- 适配提交(
鸿蒙适配,53 个文件)已推送 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)