本文记录 swipe 接入 OpenHarmony 的完整过程,覆盖滑动露出动作手势库盘点、鸿蒙 Compose 制品线现状确认、手势引擎提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

swipe 是 Compose Multiplatform 的「滑动露出动作」手势库:SwipeableActionsBox / SwipeToReveal 让列表项可以左右滑动露出不删除自身的动作(归档、稍后提醒、删除确认),与 Material SwipeToDismiss 形成互补;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 / settleFromrelease 的触发判定与回弹起点。
progress / alpha / radius / centerXripple 的逐帧几何。

引擎侧完整能力(权重分配循环、橡皮筋增量、严格大于的阈值、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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 指向 C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 8.13 升一档,与社区标准一致;2.2.21 线支持 Gradle 8.x)
引擎依赖零(手势数学只吃 kotlin.math,不需要 kotlinx 制品线)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.swipe
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
位置文件依赖形态处置
commonMaindefaults.kt纯 Kotlin(400ms 常量)原样保留
commonMainSwipeableActionsBox.kt 等 v1 六件androidx.compose.*(runtime/foundation/animation/ui 全占了)git mv 到 composeMain
commonMainswipe2/ v2 三件androidx.compose.* + 引用 v1 的 horizontalDraggablegit 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 永远是纯函数。这样做的三个理由:

  1. 手势事件天然自带全量上下文(当前 offset + 本帧 delta + 动作集),引擎不需要记忆;
  2. ArkTS 侧的状态可序列化、可恢复(页面旋转/重建不丢手势状态),Native 侧零内存生命周期问题;
  3. 单帧开销 = 一次 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 方法覆盖内核
SwipeStepswipeStep(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.tomlKotlin 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.ktCompose-free 动作模型(上游 SwipeAction 的镜像)
library/src/jvmTest/.../SwipeEngineTest.kt18 项数值钉死交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、六 action 分发、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/include/libohosswipe_api.h手写 CAdapter 镜像头(extern “C”)
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(收件箱/参数实验室/关于)
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, ...)双通道涟漪:进度/透明度/半径/圆心
NativeSwipeStepC ABI JSON 导出(单符号覆盖全部入口)
N-APIswipeStep(request)向 ArkTS 暴露能力
ArkTScallDrag(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 项用例全部通过(新增数值钉死用例):

断言组用例数钉住什么
权重切分21:1.5 → 432/648(1080px);weight ≤ 0 拒绝(上游 require 同款)
命中测试4零偏移无动作;正负偏移侧别;权重边界 ±432/433;超界 clamp 到末位动作
橡皮筋4空侧 delta/10 且逐帧累积;回弹中全量;回零放行
阈值/触发2严格大于;越阈值且命中才触发
背景色/图标3when 次序(swiped > 占位 > 可见 > 透明);两侧锚点算术
回弹曲线1t=0/400/超时端点 + 中点 ≈ −67.4 + 全程单调趋零
涟漪2400/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 功能用例

  1. 启动后状态行出现 Kotlin 引擎已连接 · swipe 1.4.0-ohos.1 · Kotlin 2.2.21-1.0.0(来自真实 Native 调用,非静态文字);
  2. 收件箱右滑露出绿色「归档」,继续拖过阈值背景由深灰占位色切换为动作绿,图标脉冲放大;
  3. 松手越过阈值:行内打"已归档"标签 + 主题删除线,涟漪从露出边缘扩散盖过内容,700ms(undo)或 400ms 后回弹归位;
  4. 左滑露出黄色「稍后提醒」与红色「删除」,删除段明显更宽(权重 1:1.5 的可视化),参数实验室的分段宽度条给出精确像素;
  5. 朝空侧拖拽橡皮筋行实时读出「raw 拖拽 320 px → 引擎 offset 32 px」(delta/10);
  6. isUndo 开关让涟漪反向收缩且时长变 700ms;RTL 开关交换左右动作列表(上游 ActionFinder 映射);
  7. 阈值 Slider 20–200px 实时改变触发手感;
  8. 每次触发/越阈值/参数变更按时间戳滚动进操作日志。

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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :library 真正编译成 ohosArm64 / ohosX64——对手势库,还要保证编译进去的是上游数学而不是重写;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;同时必须删除旧的 groovy settings.gradle,两个 settings 文件并存时 Gradle 优先读 groovy 版,新配置静默失效;
  3. 手写 CAdapter 镜像头漏 extern "C" {} 时,编译不报错,链接期才报 undefined symbol: SwipeStep(char const*)——lld 提示 did you mean: extern "C" 就是答案,Flippable 的头文件是现成范本;
  4. PanGesture.onActionUpdate 给的是自手势开始的累计 offsetX,直接喂引擎会把橡皮筋量级放大一个数量级,必须先自减出本帧增量;
  5. ArkTS 严格模式:自定义组件的 @State 成员不能叫 offset / background——与通用属性方法冲突,编译器报 not assignable to the same property in base type 'CustomComponent',改名 swipeOffset / bgColor 即可;Stack 的 alignContent 要 Alignment.Start 而不是 HorizontalAlign.Start;
  6. Circle().fill() 在 compat 5.1.0(18) 上是 API 26 兼容性告警,涟漪圆改用 borderRadius 的 Column 实现零告警;
  7. 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 的 JBR java 进 PATH(否则 PackageHap 报 spawn java ENOENT);
  8. 回弹曲线的参考常数不能想当然:第一版测试按 0.8175 断言失败,实测 CubicBezier(0.4,0,0.2,1) 在 x=0.5 处 y≈0.7754(手解 t≈0.6933, y=3t²−2t³)——求解器是对的,记忆是错的,交叉验证的意义就在这;
  9. Kotlin 里 "{"ok":true}".json() 这类花括号块不是合法语法,响应一律老实写字符串模板;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();DevEco 构建还会在 harmonyApp 下生成 .clang-tidy / .clangd,要进 gitignore,别混进提交;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内保持空数组,证书绝不进入 AtomGit。

8.2 已知问题

  • Compose UI(SwipeableActionsBox / SwipeToReveal)未编译,隔离在 composeMain 待社区 compose.ui ohos 制品就绪后回归;
  • 触觉反馈未接:上游在越阈值时 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.ui ohos 制品就绪后,评估 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)作为 Compose SwipeAction 的镜像,跨 FFI 唯一载荷;
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:手势采集与增量换算、页面状态、定时采样、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 制品可达性定路线,且要按库重新验证:KStore 适配时 kotlinx 四件套有 ohos 变体,答案是「换线不换版、整库真编译」;swipe 的 compose.ui 制品缺席,答案就变成「数学提取、UI 隔离」。两条路线共用同一套骨架(根构建替换 + 双 ABI + C ABI + NAPI + ArkUI Demo + JVM 交叉验证),分岔只发生在第 2.1 节那一次查询——历史结论不能直接套用,当天的制品可达性才是依据;
  2. 手势库的适配面 = 数学 + 宿主替换:把「谁采手势、谁持状态、谁渲染」换宿主(ArkUI Pan / ArkTS @State / ArkUI 组件),把「算什么」留在 Kotlin。无状态纯函数桥在每帧调用的 60fps 场景实测成立,这为列表滚动、拖拽排序等更高频的手势库探了路;
  3. 交叉验证要随验证对象变形:存储库的证明是「上游测试零改动全绿」(语义没漂),手势库上游测试是截图渲染,没法在 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。

参考文档

Logo

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

更多推荐