本文记录 DragSelectCompose 接入 OpenHarmony 的完整过程,覆盖 Compose 拖选网格库盘点、纯 Kotlin 决策引擎提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 手势宿主、HAP 构建和真机验收。

DragSelectCompose 是基于 Compose Multiplatform 的拖选网格库:给定一个条目列表和一个 LazyGridState,它提供「Google 相册式」长按拖拽多选——rememberDragSelectState 持有选中集,Modifier.gridDragSelect 把手势帧翻译成范围选择,边缘自动滚动、选择模式生命周期、数据刷新后的选中重对账(reconcile)全部内置。本次适配与 KStore 的「真编译整库」路线有本质区别:DragSelectCompose 的全部值钱代码都长在 Compose 上(上游 CMP 1.10.1 / Kotlin 2.3.10,社区鸿蒙 CMP 是 1.9.2 / Kotlin 2.2.21,两端工具链对不齐,@Composable 编不进 ohos),因此走的是社区已验证的 cmp_qrose / compose-pinch-grid 模式——把拖选决策算法(范围选择、命中测试、past-last 判定、自动滚动速度、选择模式状态机、重对账)逐行提取为零 Compose 依赖的纯 Kotlin 引擎 DragSelectEngine,编译成 OpenHarmony 动态库;Compose UI 原样停放进 composeMain 等工具链对齐后接回;ArkUI 重做宿主,只负责喂手势坐标与可见项布局数据。

项目地址: AtomGit/oh-tpc/drag-select-compose

上游仓库: jordond/drag-select-compose

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

DragSelectCompose 为 Compose 应用提供「多选拖拽」这一件事的完整答案:长按任意网格项起拖,手指扫过的索引区间全部进入选中集,先前已选中但被拖出区间的条目保持选中(previouslySelectedInRange 语义),拖到网格边缘自动滚动并继续扩张选中集;inSelectionMode 暴露选择模式,extensions 给条目补无障碍语义与点选切换,grid 模块直接给出带选中角标的 LazyDragSelectVerticalGrid。KMP/CMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套交互,但上游构建(Kotlin 2.3.10 / CMP 1.10.1 / Gradle 9.3.1 / AGP 8.13.2)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示网格,却无法证明上游的拖选决策算法在 OpenHarmony ARM64 设备上完成编译、链接和执行——那只是拿 ArkUI 重画了一个"长得像"的 Demo,算法是抄的,不是复用的。适配需要同时验证决策引擎、Native 动态库、C ABI、N-API、ArkUI 手势宿主和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.3.10 / CMP 1.10.1 与鸿蒙工具链无法共存于一个 Gradle 构建;鸿蒙 Kotlin 2.2.21 线要求 Gradle 8.14.1,上游 wrapper 是 9.3.1。
UI 不可编译全部公开 API(rememberDragSelectState、Modifier.gridDragSelect、LazyDragSelectVerticalGrid)是 @Composable / Modifier,依赖 androidx.compose.*,社区 CMP 1.9.2 对不上游 1.10.1。
注解不可用上游源码带 Poko、@Stable、@Composable、Compose runtime 注解,HarmonyOS Kotlin 编不过。
交互语义跨不了桥拖选是帧级交互:每个手势帧都要进引擎出决策(命中索引、选中区间、滚动速度),不是 qrose 那种"编码一次、PNG 过桥一次"的产物型库。
布局信息在宿主手里上游算法从 LazyGridState.layoutInfo.visibleItemsInfo 读可见项矩形,鸿蒙上没有 Compose LazyGrid,这些数据只能由 ArkUI 宿主测量后喂给引擎。

因此,本项目把边界放在:纯 Kotlin 决策引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 会话桥。拖选语义完全由引擎负责,ArkTS 只负责手势采集、布局测量、视觉呈现与滚动执行。

1.2 库提供的能力

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

  • 长按起拖:GestureGroup(Sequence, LongPressGesture(350ms), PanGesture) 对应上游 detectDragGesturesAfterLongPress 的 onDragStart → startDrag——引擎记录 initial / current 双索引并把该条目加入选中集,响应 haptic:true 触发视觉反馈(对齐上游长按触觉);
  • 拖动帧决策:ArkUI 每 16ms 喂一次 (x, y, viewport, total, bounds),引擎做命中测试(itemIndexAtPosition,右/下开区间,对齐 IntRect.contains)、范围选择(withinRange || previouslySelectedInRange)、自动滚动速度计算(顶部/底部阈值三区公式),一并返回;
  • 边缘自动滚动:响应里 autoScroll ≠ 0 时宿主启动 16ms 定时器执行 scroller.scrollBy,速度值由引擎算出,滚动后新可见项的矩形经 onAreaChange 回填,选中集随滚动持续增长;
  • 选择模式与点选:mode(enable/disable/toggle)、select(index) 对齐上游 enableSelectionMode / dragSelectToggleable;空选择时 enableSelectionMode 才置手动模式,选择清空时手动模式自动复位——全部是上游原件语义;
  • 数据刷新重对账:reconcile(keys) 对齐上游 reconcile:associateBy → mapNotNull → distinctBy,选中项按 key 在新列表里保留新实例、消失项剔除;
  • 版本握手:dragSelectVersion() 返回 3.3.0-ohos.1,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / count回显本次操作与当前条目总数。
sel选中 key 的 CSV(如 "3,7,12"),宿主直接驱动角标。
inSelectionMode / dragging选择模式与拖拽中标志,驱动状态栏与角标显隐。
initial / current / index拖拽起点索引、当前索引、本次命中索引(非拖拽操作为 -1)。
autoScroll边缘自动滚动速度(vp/帧),0 表示不在边缘区。
haptic / changed / threshold长按反馈标志、选中集是否变化、当前阈值。

引擎侧完整能力(范围选择双向区间、先前选中保留、past-last 判定、自动滚动三区、选择模式生命周期、key 泛型对齐 compareSelector)全部在纯 Kotlin 引擎里随编译进入 .so,28 项 JVM 测试钉住每一条上游行为,证明 ohos .so 里执行的决策逻辑没有漂移。

1.3 实现适配

维度要求
代码复用上游 4 个 Compose 文件(DragSelectState / DragState / GridDragSelect / GridDragSelectDefaults)以 git mv 原样移入 core/src/composeMain,一个字节不改;新增的只有纯 Kotlin 引擎、28 项测试与桥层。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 dragselect / extensions / grid / demo / .run / .github 模块留盘不进 settings.gradle.kts。
桥接稳定2 个 C ABI(DragSelectStep / DragSelectVersion)和扁平 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律引擎是手写纯 Kotlin,不引任何第三方运行时,ohos 侧零依赖解析风险。
UI 完整单页覆盖全部 12 个桥接 op:长按拖选、范围选择、边缘自动滚动、选择模式、点选、清空、重对账、快照、版本握手与操作日志。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。DragSelectCompose 的 UI 面天然长在 Compose 上,鸿蒙侧按社区既定路线由 ArkUI 重做宿主;引擎与上游算法是同一份语义,这是「适配」与「重写」的分界线。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认工具链线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Compose 源码停放 composeMain
第 3 阶段:桥协议建模     ── 12 个 op、扁平 JSON 契约、sel/threshold CSV 通道、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、会话单例、C++ N-API、DisposeString 纪律
第 5 阶段:ArkUI 宿主     ── 手势链路(长按→Pan)、坐标换算链、bounds 喂养、角标视觉、自动滚动执行
第 6 阶段:示例与验证     ── 28 项 JVM 交叉验证、双 ABI 链接、HAP、签名、设备安装、真机验收

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
core/src/commonMain/kotlin/       DragSelectEngine + DragSelectItemBounds + DragSelectDefaults(纯 Kotlin,jvm+ohos)
core/src/composeMain/kotlin/      上游 Compose 四文件(git mv 原样停放,ohos 不编)
core/src/jvmTest/kotlin/          DragSelectEngineTest(28 项,JVM 交叉验证)
core/src/androidMain/             上游遗留,不声明 androidTarget 即为死文件,留盘
dragselect/ extensions/ grid/     上游模块,留盘不进 settings
demo/ .run/ .github/              上游遗留,留盘不进构建
example/nativeApp/                Kotlin/Native 动态库(Bridge + MiniJson)
example/ohos/               DevEco Stage、CMake N-API 和 ArkUI 页面
README.OpenHarmony.md / _CN.md    鸿蒙适配说明(中英双份)
README.upstream.md                上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :core 与 :example:nativeApp,rootProject.name = "ohos_drag-select-compose"。上游的 Poko、dokka、vanniktech 发布、binary-compatibility-validator、AGP classpath 全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 从上游 9.3.1 降级——2.2.21 线不支持 Gradle 9.x)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release(5.1.0(18) SDK)
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.dragselect

Kotlin 2.2.21-1.0.0 与 Gradle 8.14.1 的组合是社区此前批准的钉法(qrose / pinch-grid 同款)。上游源码虽然是 Kotlin 2.3.10 编的,但进入 ohos 构建的只有手写纯 Kotlin 引擎,不拖上游任何字节码,因此不需要 KStore 那种「跟着依赖升 2.4 线」的操作。

1.3 源码依赖度盘点
模块文件依赖形态处置
core(上游)4androidx.compose.* + Poko + @Stable整体停放 composeMain,内容零改动
新引擎2纯 Kotlin,零依赖新增,jvm + 双 ohos 编译
dragselect / extensions / grid / demo—全 Compose留盘,不进 settings.gradle.kts

引擎不是「翻译」,是逐行提取:上游 GridDragSelect.kt 里真正的算法只有五段——itemIndexAtPosition(命中)、getItemPosition / isPastLastItem(拖出末尾)、calculateScrollSpeed(边缘速度)、List.getSelectedItems(范围选择)、加上 DragSelectState 里的选择模式状态机。这五段不依赖任何 Compose 运行时,依赖的只是数据(visibleItemsInfo 的矩形、指针坐标),把数据改成参数就是纯函数。而 LazyGridState、pointerInput、HapticFeedback 是宿主职责,留在 composeMain。

compareSelector 泛型语义也被保留:引擎是 DragSelectEngine<K>,选中集按 key 去重保序,桥层用字符串 key,sel 响应就是 key 的 CSV。

1.4 页面边界
标题区          DRAGSELECT · OPENHARMONY / 版本握手行(引擎版本 3.3.0-ohos.1)
状态行          选中 N 项 · 选择模式 B · 拖拽中 B + 触觉反馈视觉位(红点闪烁)
操作区          选择模式 / 清空 / 重对账-3 / 快照 四按钮
网格区          100 个纯色方块模拟照片(黄金角色相 + Landscape/Portrait/Nature/Urban 四类)
                每格 onAreaChange 记录矩形,选中角标对齐上游 SelectedIcon/UnselectedIcon 语义
日志区          带 [HH:mm:ss] 时间戳的操作日志(最新在前,上限 30 条)
关于区          版本握手、库简介、架构分层、12 个桥接 op 全表、仓库与社区链接

页面启动时先调 dragSelectVersion() 握手,再发 {"op":"reset","count":100,"threshold":40} 初始化会话。

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

2.1 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

ohosArm64() / ohosX64() 只存在于 HarmonyOS Kotlin 插件。settings.gradle.kts 必须先写 eazytec Maven,再写 gradlePluginPortal():

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_drag-select-compose"

include(":core")
include(":example:nativeApp")

仓库顺序写错,配置期直接 Unresolved reference: ohosArm64。Gradle wrapper 必须从上游 9.3.1 降到 8.14.1(2.2.21 线的兼容上限),JDK 21 用 org.gradle.java.home 钉死,gradle.properties 带上 kotlin.native.ignoreDisabledTargets=true 与 kotlin.native.distribution.downloadFromMaven=true。

2.2 根构建替换(cmp_qrose 模式)

根 build.gradle.kts 收敛为:

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

allprojects {
    group = "com.dragselectcompose"
    version = providers.gradleProperty("version").orElse("3.3.0-ohos.1").get()
}

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

最后一段是纪律:测试统一在 JVM 跑,ohos 侧只承担编译与链接,ohos 测试任务全局禁用。

2.3 引擎提取(本适配的核心一刀)

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

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()
    ohosArm64()
    ohosX64()

    sourceSets {
        jvmTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

没有 androidLibrary、没有 compose 插件、没有 Poko——composeMain 不是 Kotlin 源集,ohos 编译器看不到 androidx.compose。上游四文件以 git mv 移入,git 识别为 rename,上游历史可追溯。

引擎公开面(com.dragselectcompose.core,零注解、零依赖):

public class DragSelectEngine<K>(initialSelection: List<K> = emptyList()) {
    public val selected: List<K>              // 选中集,去重保序(拖选期间为索引序)
    public val inSelectionMode: Boolean       // manualSelectionMode || selected.isNotEmpty()
    public val manualSelectionMode: Boolean   // private set
    public val isDragging: Boolean
    public val dragInitial: Int               // -1 = None,对齐上游 DragState.None
    public val dragCurrent: Int               // private set
    public fun startDrag(key: K, index: Int): List<K>
    public fun updateDrag(currentIndex: Int, keys: List<K>): List<K>
    public fun stopDrag()                     // 只清 initial,current 保留(对齐上游)
    public fun enableSelectionMode() / disableSelectionMode() / toggleSelectionMode()
    public fun isSelected(key: K): Boolean
    public fun updateSelected(keys: List<K>) / addSelected(key: K) / removeSelected(key: K)
    public fun toggleSelected(key: K): Boolean
    public fun reconcile(availableKeys: List<K>)
    public fun itemIndexAtPosition(x: Float, y: Float, bounds: List<DragSelectItemBounds>): Int?
    public fun itemIndexAtPosition(x: Float, y: Float, bounds: List<DragSelectItemBounds>, totalItems: Int): Int?
    public fun calculateAutoScrollSpeed(y: Float, viewportHeight: Float, threshold: Float): Float
}

五段算法逐行对齐上游:

// 范围选择:对齐上游 List<Item>.getSelectedItems
keys.filterIndexed { i, key ->
    val withinRange = i in dragInitial..currentIndex || i in currentIndex..dragInitial
    val previouslySelectedInRange =
        isSelected(key) && i !in dragInitial..dragCurrent && i !in dragCurrent..dragInitial
    withinRange || previouslySelectedInRange
}
// 注意:previouslySelectedInRange 用"旧" dragCurrent 比较,比较发生在 current 更新之前

实测:

./gradlew :core:compileKotlinJvm :core:jvmTest   # BUILD SUCCESSFUL,28 项全绿
2.4 测试怎么钉住上游行为

DragSelectEngineTest 的 28 项用例每条断言都注释了对应的上游行为:向前/向后拖选区间、先前已选拖出区间保留、命中测试右/下开区间边界(x = left + width 不命中)、past-last 只认 totalItems - 1 且要求末项可见、自动滚动三区含符号、选择模式生命周期(enable 只在空选择时生效、disable 清空、updateSelected 清空复位 manual)、reconcile 的保留/剔除/去重/空选择 no-op、stopDrag 后 isDragging 为 false 且 current 保留。这 28 项就是「引擎没漂」的闸门。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.dragselectcompose.core.DragSelectEngine(纯 Kotlin 决策引擎)
        ↓ startDrag / updateDrag / toggleSelected / reconcile
Bridge.kt(@CName × 2,扁平 JSON 进出,会话单例)
        ↓ UTF-8 JSON
N-API dragSelectStep() / dragSelectVersion()
        ↓
ArkUI Index.ets(GestureGroup 手势宿主)

请求扁平 JSON,op 十二选一:

op参数对齐上游
resetcount(默认 100)、keys(CSV,缺省 “0”…“n-1”)、initial、threshold(默认 40)rememberDragSelectState(initialSelection)
startindexonDragStart → startDrag
dragx, y, viewport, total、bounds(五元组 CSV:index,x,y,w,h,...)onDrag → 命中 + 范围选择 + 滚动速度
drop—onDragEnd / onDragCancel → stopDrag
selectindexdragSelectToggleable
modevalue = enable / disable / toggleenableSelectionMode 族
add / removeindexaddSelected / removeSelected
clear—disableSelectionMode(上游 clear() 的替代)
reconcilekeys(CSV,新顺序全量)reconcile(availableItems)
state—只读快照
version—fork 版本握手

响应统一:

{"ok":true,"op":"drag","count":100,"sel":"7,8,9","inSelectionMode":true,"dragging":true,
 "initial":7,"current":9,"index":9,"autoScroll":0.0,"haptic":false,"changed":true,"threshold":40.0}

失败:{"ok":false,"error":"..."}。

3.2 两个关键设计

命中判定在桥内、布局数据由宿主喂。 上游从 LazyGridState.layoutInfo.visibleItemsInfo 读可见项矩形;鸿蒙上对应物是 ArkUI 每个格子的 onAreaChange。宿主把可见项矩形按 index,x,y,w,h 五元组 CSV 塞进 bounds,桥还原成 List<DragSelectItemBounds> 交给引擎——判定逻辑一行不复制到 ArkTS,这是「适配」与「重写」的分界线。

sel / keys / initial 用 CSV 而不是 JSON 数组。 桥层 MiniJson 继承 cmp_qrose 的扁平解析(逗号切顶层字段、引号状态机),列表字段按逗号切分即可,协议保持一层深度。布尔/数字不加引号,sel 空集回空串。

3.3 错误通道

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

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

非法 JSON、缺 op / index、未知 op、index 越界、bounds 解析失败,全部走同一个 ok:false 通道,ArkTS 侧统一进日志区。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libdragselect_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosdragselect.so)
  │ DragSelectStep / DragSelectVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;拖选又是带会话状态的交互,所以选择 C ABI + JSON + 进程级单例:DragSelectSession 持有引擎、key 表与阈值,reset 换新会话,其余 op 变异当前会话——与 pinch-grid 的 PinchGridSession 同款。

4.2 Kotlin/Native 导出函数
@CName("DragSelectStep")
public fun dragSelectStep(request: String): String   // 12 op 一口进

@CName("DragSelectVersion")
public fun dragSelectVersion(): String = "3.3.0-ohos.1"   // 版本握手
C ABI 符号ArkTS 方法覆盖内核
DragSelectStepdragSelectStep(req)引擎全部公开面 + 会话管理
DragSelectVersiondragSelectVersion()fork 版本握手(真机版本行的数据来源)

changed 字段在桥里以「op 前后 selected 快照比对」得出,宿主据此决定是否刷新角标动画。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"dragSelectStep", nullptr, NapiDragSelectStep, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"dragSelectVersion", nullptr, NapiDragSelectVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

nm_modname 必须是 "dragselect_napi",与 ArkTS import 'libdragselect_napi.so'、index.d.ts 三处同名——错一个,表现都是「能安装、一调用就崩」。每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。API 18 的 napi_get_value_string_utf8 缓冲区参数非 const,先取长度再开 std::vector<char> 拷贝再构造 std::string。KN 返回的字符串必须经 libohosdragselect_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosdragselect_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

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

add_library(dragselect_napi SHARED napi_init.cpp)
target_link_libraries(dragselect_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosdragselect)

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

export const dragSelectStep: (request: string) => string;
export const dragSelectVersion: () => string;

Kotlin 侧的 so 由 Gradle 任务自动回填:example/nativeApp/build.gradle.kts 声明 binaries.sharedLib { baseName = "ohosdragselect" },copyKnSoArm64 / copyKnSoX64 在 link 后把 libohosdragselect.so 与对应 triple 的 libc++_shared.so(从 ~/.konan/dependencies 定位)拷进 example/ohos/entry/libs/<abi>/。

第 5 阶段:ArkUI 宿主

5.1 手势链路(对齐 detectDragGesturesAfterLongPress)
GestureGroup(GestureMode.Sequence,
  LongPressGesture({ repeat: false, duration: LONG_PRESS_MS })   // 350ms,构造参数形式
    .onAction((_event: GestureEvent) => {
      // 长按命中即发一帧 drag —— 引擎按 (x,y) 命中格子、进入拖选并回 haptic=true,
      // 语义上就是上游 onDragStart → startDrag 在拖拽起点的首次调用。
      this.lastDragMs = Date.now();
      const resp = this.sendDrag(this.touchWinX - this.containerWinX,
        this.touchWinY - this.containerWinY);
    }),
  PanGesture({ fingers: 1, direction: PanDirection.All, distance: 0 })
    .onActionStart((_event: GestureEvent) => { /* 补一帧,防手指微动丢帧 */ })
    .onActionUpdate((_event: GestureEvent) => { /* 16ms 节流,一帧一次桥调用 */ })
    .onActionEnd((_event: GestureEvent) => { this.endDrag(); })
    .onActionCancel((_event: GestureEvent) => { this.endDrag(); })
)

Sequence 组保证 Pan 只在长按成功后接管,与上游「长按后进入拖拽、拖拽期间压制滚动」的行为一致。endDrag 发 {op:"drop"} 并停掉自动滚动定时器。

5.2 坐标换算链(本 SDK 的真实约束)

API 18 SDK 的 GestureEvent 类型上没有 absoluteX / absoluteY,TouchObject 上没有 globalX / globalY——这两个坑都真实踩过。最终链路:

TouchObject.windowX / windowY(窗口系,onTouch 持续刷新)
  − Area.globalPosition(窗口系,容器 onAreaChange 记录)
  = 容器内坐标(vp)→ 进 drag op 的 x / y

两套坐标同属窗口坐标系,相减即得容器内坐标;触点不读手势回调的参数,统一从 .onTouch 的 TouchEvent.touches[0] 取——Down/Move 与手势回调同步触发,长按与拖动帧拿到的都是实时位置。

每个格子 .onAreaChange 把相对容器的 (x, y, w, h) 写进 cellBounds 映射,滚动时自动刷新——这正是引擎命中判定要的「可见项布局信息」,等价上游 visibleItemsInfo。发 drag 帧时把映射展平成五元组 CSV。

5.3 自动滚动与页面交互

drag 响应里 autoScroll ≠ 0 时启动 16ms setInterval 执行 scroller.scrollBy({ dx: 0, dy: autoScroll }),回 0 或 drop 必停;滚动让新格子进入视口、onAreaChange 回填矩形,下一帧命中判定自然覆盖新条目——选中集随边缘滚动持续增长,与上游 LaunchedEffect 驱动的 scrollBy 循环同构。

页面自上而下:标题区(版本握手行)、状态行(选中 N 项 · 选择模式 · 拖拽中 + 触觉反馈红点)、四按钮(选择模式 / 清空 / 重对账-3 / 快照)、100 格照片网格(黄金角色相纯色块 + 分类文字,免网络依赖)、操作日志(时间戳,最新在前,上限 30 条)。关于 Tab 展示版本握手、架构分层(ArkTS → NAPI → C ABI → Kotlin 引擎)与 12 个 op 全表。

5.4 视觉与触觉的边界处理

选中项右下角白圆蓝勾 + 2vp 高亮描边,选择模式下未选项灰空心圆——对齐上游 grid 模块 SelectedIcon / UnselectedIcon 语义。触觉反馈在鸿蒙侧不做系统振动(避免权限与多设备差异),以状态行红点闪烁 300ms 作视觉降级,与 pinch-grid 的处理策略一致——haptic:true 是引擎的真实输出,呈现方式是宿主的决定。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosdragselect)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/
│       └── Bridge.kt                         @CName × 2 + DragSelectSession + MiniJson(扁平 JSON + CSV)
└── ohos/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.dragselect、versionName 3.3.0-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosdragselect_api.h
        ├── src/main/cpp/types/libdragselect_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 手势宿主(拖选网格 + 关于)
6.2 构建、签名和安装
./gradlew :core:compileKotlinJvm :core:jvmTest                                 # 28 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 example/ohos(不要打开仓库根目录,根目录是 Gradle KMP)
# File → Project Structure → Signing Configs 勾选自动签名后直接 Run

签名在 DevEco Studio 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,product 引用 "signingConfig": "default",勾选自动签名后由 IDE 回填完整配置)。

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.dragselect -m entry
hdc shell hilog | grep dragselect

四、完整代码对照

4.1 整体架构

core/src/commonMain(DragSelectEngine,纯 Kotlin)
    │ 范围选择 / 命中测试 / past-last / 自动滚动速度 / 选择模式 / reconcile
core/src/composeMain(上游 Compose 四文件,原样停放)
    │ DragSelectState / DragState / GridDragSelect / GridDragSelectDefaults
Bridge.kt + MiniJson + DragSelectSession
    │ DragSelectStep / DragSelectVersion
libdragselect_napi.so
    │ dragSelectStep / dragSelectVersion
Index.ets
    │ 手势采集 + bounds 喂养 + @State 更新
    ▼
ArkUI 真机页面(长按拖选网格)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_drag-select-compose、上游模块移出
gradle/libs.versions.toml仅 Kotlin 2.2.21-1.0.0 + kmp 插件别名
build.gradle.kts(根)版本 3.3.0-ohos.1、全局禁用 Ohos*Test* 任务
gradle/wrapper/8.14.1(从上游 9.3.1 降级,jar 一并替换)
core/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64
core/src/commonMain/.../DragSelectEngine.kt纯 Kotlin 决策引擎(唯一新增核心源文件)
core/src/composeMain/上游 Compose 四文件 git mv 停放,内容零改动
core/src/jvmTest/.../DragSelectEngineTest.kt28 项上游行为对齐测试
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、会话单例、MiniJson、错误通道
ohos/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
ohos/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
ohos/.../ets/pages/Index.etsArkUI 手势宿主
README.OpenHarmony_CN.md鸿蒙适配主文档(19 节)

4.3 关键 API 对照

层次API作用
引擎startDrag(key, index)长按起拖:记录双索引并加入选中
引擎updateDrag(currentIndex, keys)拖动帧:范围选择(双向区间 + 先前选中保留)
引擎itemIndexAtPosition(x, y, bounds, total)命中测试(右/下开区间)+ past-last 判定
引擎calculateAutoScrollSpeed(y, viewport, threshold)边缘三区滚动速度
引擎enable/disable/toggleSelectionMode选择模式生命周期(手动模式与空选择联动)
引擎reconcile(availableKeys)数据刷新后的选中重对账
引擎stopDrag()只清 initial,current 保留(上游同款)
NativeDragSelectStep / DragSelectVersionC ABI JSON 导出
N-APIdragSelectStep(req) / dragSelectVersion()向 ArkTS 暴露能力
ArkTSsendDrag(x, y) / endDrag()手势帧分发与滚动定时器管理

4.4 ArkTS 与 Kotlin 的边界

// ArkTS:只喂事实(触点、视口、可见项矩形),不做任何拖选决策
const res = JSON.parse(dragSelectStep(JSON.stringify({
  op: 'drag', x: localX, y: localY, viewport: this.viewportH,
  total: TOTAL_ITEMS, bounds: this.flattenBounds()
})));
// res.sel 驱动角标,res.autoScroll 驱动滚动定时器,res.haptic 驱动视觉反馈
// Kotlin:决策全部在引擎,桥只做协议翻译
"drag" -> {
    val hit = engine.itemIndexAtPosition(x, y, bounds, total)
    if (hit != null) engine.updateDrag(hit, session.keys)
    val auto = engine.calculateAutoScrollSpeed(y, viewport, session.threshold)
    /* 拼装响应 */
}

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

五、关键决策说明

决策 1:引擎提取而不是真编译整库

KStore 能「真编译整库」,是因为它是纯 Kotlin 库且 ohos 制品线齐全。DragSelectCompose 相反:全部值钱代码长在 Compose 上,上游 CMP 1.10.1 / Kotlin 2.3.10 与社区 CMP 1.9.2 / Kotlin 2.2.21 两端工具链对不齐,@Composable 编不进 ohos。此时正确动作是把算法与 UI 切开——算法五段(命中、past-last、范围选择、滚动速度、选择模式)本来就只依赖数据不依赖运行时,提取成纯函数;Compose 文件原样停放 composeMain,一个字节不改,等社区 CMP 工具链对齐后接回。硬编 Compose 或在 ArkTS 里重抄算法,一个是打不完的版本仗,一个是把「适配」做成「重写」。

决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1

进入 ohos 构建的只有手写纯 Kotlin 引擎,不拖上游任何字节码与第三方运行时,所以不需要 KStore 那种「跟着 kotlinx 制品线升 2.4 线」的操作,直接用社区已验证的 qrose/pinch-grid 钉法即可。2.2.21 线的 Gradle 上限是 8.14.x,上游 wrapper 9.3.1 必须降级——两条工具链线的经验不能互相套用。

决策 3:根构建替换(cmp_qrose 模式),上游模块留盘

上游根构建(Poko、dokka、vanniktech 发布、binary-compatibility-validator、AGP classpath)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。dragselect / extensions / grid / demo / .run / .github 留盘不进 settings.gradle.kts——它们是纯 Compose 模块,进构建只会把 AGP 拉回来;留盘是为了对照上游 API 与后续工具链对齐时参考。

决策 4:op 式会话桥,而不是单函数产物桥

qrose 是「编码一次、PNG 过桥一次」,一个函数够了。拖选是帧级交互:每个手势帧都要进引擎出决策,且引擎带会话状态(选中集、拖拽双索引、选择模式)。所以桥是 reset + 11 个变异/只读 op 的会话协议,进程级单例持有引擎,reset 换新会话,C ABI 永远是 const char* → const char*。命中判定放在桥内而非 ArkTS,判定逻辑就一行都不用在宿主复制。

决策 5:28 项 JVM 测试钉住上游行为

引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;28 项 jvmTest 的每条断言都注释了对应的上游行为,是「引擎没漂」的闸门。ohos 测试任务在根构建全局禁用,测试统一 JVM 跑——ohos 侧只承担编译与链接,与 KStore 的处置同款。

决策 6:坐标框架以本机 SDK 类型声明为准

ArkTS 侧的坐标 API 在不同 SDK 世代命名不同(链式 .duration()、GestureEvent.absoluteX、TouchObject.globalX 都在 API 18 类型上不存在)。处置不是猜 API,而是直接读本机 DevEco SDK 的 .d.ts:TouchObject.windowX/windowY(窗口系,必有必填)配 Area.globalPosition(窗口系),两套同系相减得容器内坐标。引擎对单位不敏感(阈值与坐标同单位即可),全链路统一 vp。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Temurin,org.gradle.java.home 钉版)、Gradle 8.14.1、HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)、DevEco Studio 5.1.0 Release(5.1.0(18) SDK)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :core:compileKotlinJvm :core:jvmTest

28 项用例全部通过(引擎新增测试,每条钉住一条上游行为):

用例组钉住什么
范围选择向前/向后拖选区间;先前已选但拖出区间的条目保留(previouslySelectedInRange 用旧 current 比较)
起拖/停拖startDrag 立即单选;stopDrag 后 isDragging=false 且 current 保留
命中测试矩形内/外;右/下开区间边界(x = left + width 不命中)
past-last末项可见且拖过其顶部才返回 totalItems - 1,否则 null
自动滚动顶部负速、底部正速、中间归零三区公式
选择模式enable 只在空选择时生效、disable 清空、updateSelected 清空复位 manual、toggle
点选/增删toggleSelected 双向、addSelected / removeSelected
reconcile保留存活项(新实例)、剔除消失项、按新列表去重、空选择 no-op

引擎是纯 Kotlin,JVM 与 ohos 执行同一份源码——28 项全绿即「ohos so 里的决策逻辑与上游语义一致」的交叉验证。

6.3 HAP 验证

:core:compileKotlinJvm / :core:jvmTest: BUILD SUCCESSFUL(28 tests, 0 failed)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套落盘 entry/libs/<abi>/
llvm-nm -D: DragSelectStep / DragSelectVersion / libohosdragselect_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libdragselect_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully / start ability successfully

libohosdragselect.so arm64 约 3.63 MB、x86_64 约 3.36 MB(含 Kotlin/Native 运行时),各带匹配 libc++_shared.so;HAP 内含双 ABI 各三件套,真机与真机通用。

6.4 功能用例

  1. 启动后标题区出现 引擎版本 3.3.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. 长按任意格子 ≥350ms,状态行红点闪烁(haptic:true 视觉降级),该格子进入选中集并出现角标;
  3. 按住拖动,手指扫过的索引区间连续入选;先向前拖再向后拖,先前选中但拖出区间的条目保持选中(上游核心语义);
  4. 拖到网格底边缘,列表自动向下滚动且选中集持续增长,状态行 autoScroll 出现正值;拖到顶边缘为负值(真机日志可见 auto=-21@91 这样的速度/命中记录);
  5. 拖出最后一行,past-last 判定把最后一项纳入选中;
  6. 松手后角标保留,再拖从新起点重新计算区间;
  7. 点「选择模式」进入手动模式,点选/取消单个条目;点「清空」退出并清空;
  8. 点「重对账-3」(模拟数据刷新后前 3 项消失),选中集中 0/1/2 被剔除,其余按 key 保留;
  9. 每步操作进日志区(时间戳,op → sel=N/auto=…),30 条滚动留痕。

6.5 验证结论

决策引擎、Native 动态库、C ABI、N-API、ArkUI 手势宿主和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的拖选会话:版本握手、长按起拖、范围选择、边缘自动滚动、选择模式与重对账闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/drag-select-compose.git
cd drag-select-compose
export JAVA_HOME="/path/to/jdk-21"
./gradlew :core:compileKotlinJvm :core:jvmTest                                 # 28 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/ohos,自动签名,Run 到真机
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.dragselect -m entry
hdc shell hilog | grep dragselect

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让 :core 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 2.2.21 工具链线不支持 Gradle 9.x,上游 wrapper 9.3.1 必须降到 8.14.1,wrapper jar 一并替换;
  4. Poko / @Stable / @Composable / compose runtime 注解鸿蒙编不过——处置不是剥注解,而是把整个 Compose 文件停放 composeMain(不是 Kotlin 源集,ohos 编译器看不到),算法用纯 Kotlin 重写为引擎;
  5. 桥层首次链接失败:internal fun step 暴露了 file-private 的 MiniJson,把 MiniJson 提为 internal class 后通过——KN 的可见性检查在链接期才炸;
  6. ArkTS 侧三连坑(本机 SDK 实测):LongPressGestureInterface 没有链式 .duration()(必须走构造参数 { repeat, duration });GestureEvent 类型上没有 absoluteX / absoluteY;TouchObject 上没有 globalX / globalY(只有 windowX / windowY 与可选的 globalDisplayX)。处置是直接读本机 DevEco SDK 的 .d.ts 按真实字段写代码,触点统一从 .onTouch 取 windowX/windowY;
  7. Area.globalPosition 与 TouchObject.windowX/windowY 同为窗口坐标系,相减即得容器内坐标——两套坐标混用屏幕系就会整体偏移一个状态栏;
  8. PanGesture 的方向枚举是 PanDirection.All,GestureDirection 是 SwipeGesture 的,写错不报错但手势不响应;
  9. 根构建的 /.idea 与 /build 是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的 example/ohos/.idea/ 要在 ohos 的 .gitignore 里单独追加;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();API 18 的 napi_get_value_string_utf8 缓冲区参数非 const,先取长度再开 std::vector<char>;
  12. 签名材料由 DevEco 自动写入本机,证书绝不进入 AtomGit。

8.2 已知问题

  • 鸿蒙上没有 Compose LazyGrid:上游 LazyDragSelectVerticalGrid / extensions 三件套停留在 composeMain,Demo 的网格与角标是 ArkUI 等价呈现,不是 Compose 渲染;
  • 上游 hapticFeedback 依赖 Compose LocalHapticFeedback,鸿蒙侧以视觉反馈降级(红点闪烁),不接系统振动;
  • 命中判定需要的可见项矩形由宿主 onAreaChange 喂,极端快速滚动时可能有一帧矩形滞后(与上游读 layoutInfo 的时序差同量级);
  • dragselect / extensions / grid / demo 模块留盘不进构建,本 fork 不发 Maven Central;
  • 网格条目用纯色方块模拟照片,不引网络图,保证验收环境离线可复现。

8.3 未来优化方向

  • 若社区 CMP 工具链与上游对齐(1.10+ 的 compose.ui ohos 制品出现),把 composeMain 接回 commonMain,Modifier.gridDragSelect 在鸿蒙直接跑,引擎降级为桥的内部实现;
  • 桥补 selectAll / 区间选择等批量 op,覆盖上游 updateSelected 的更多调用形态;
  • 自动滚动在宿主侧改为 VSync 驱动,消除 setInterval 的抖动;
  • 评估 .har 形态发布桥层类型包,让 ArkTS 工程直接依赖;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)验收矩阵与自动化截图脚本(uitest + hdc,与 qrose 的 capture.py 同思路)。

九、总结

9.1 核心难点回顾

上游 drag-select-compose(全部 Compose)
    → 算法与 UI 切开:五段决策算法提取为纯 Kotlin DragSelectEngine
    → 工具链钉版 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(2.2.21 线)
    → 根构建替换(cmp_qrose 模式,上游 Compose 四文件 git mv 停放 composeMain)
    → ohosArm64 / ohosX64 KLIB + 28 项 JVM 交叉验证
    → C ABI(@CName × 2)+ 会话单例 + 12 op 扁平 JSON 协议
    → C++ N-API(DisposeString 纪律)
    → ArkUI 手势宿主(Sequence 长按→Pan,windowX 坐标链,bounds 喂养)
    → ArkUI 真机验收(长按拖选 / 范围选择 / 边缘自动滚动 / 选择模式 / 重对账)

9.2 封装层次

  • 引擎层:DragSelectEngine<K>,纯 Kotlin 零依赖,范围选择/命中/滚动速度/选择模式/reconcile 全部上游语义;
  • 停放层:上游 Compose 四文件 composeMain 原样保留,内容零改动,git rename 可追溯;
  • Native 层:双 ABI 动态库通过 2 个 C ABI 输出 JSON,会话单例持有引擎;
  • N-API 层:参数检查、字符串转换和 Native 内存释放(DisposeString);
  • ArkTS 层:手势采集、坐标换算、bounds 喂养、角标呈现、滚动执行与操作日志;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 路线选择取决于「值钱的部分长在哪」。 纯 Kotlin 库(KStore)走真编译整库;Compose 库(qrose / pinch-grid / 本项目)走算法提取 + UI 宿主重做。判断标准不是偏好,是 @Composable 能不能进 ohos 构建——工具链对不齐时,把算法与 UI 切开是唯一可验收的路。
  2. 算法提取的验收底线是「行为对齐」,不是「能跑」。 五段算法逐行对齐上游(双向区间、旧 current 比较、右/下开区间、past-last 条件、三区速度公式),28 项测试每条钉住一条上游行为;鸿蒙上扫出来的选择序列必须和上游 DragSelectState 一致,不一致就是回归,不是「鸿蒙特性」。
  3. ArkTS 坐标 API 以本机 SDK 的 .d.ts 为准,不凭文档记忆写代码。 本项目真实踩了 duration 链式、absoluteX、globalX 三个类型坑,最终以 TouchObject.windowX/windowY + Area.globalPosition 同系相减收口——SDK 世代不同命名就不同,读声明文件比猜 API 快。

9.4 适配成果

当前适配已完成:

  • DragSelectCompose 3.3.0 的拖选决策引擎双 ABI Kotlin/Native 编译(上游 Compose 文件零改动停放 composeMain);
  • 纯 Kotlin 引擎 DragSelectEngine + 28 项 JVM 交叉验证全绿(逐条钉住上游行为);
  • 2 个 C ABI 符号、会话单例、12 op 扁平 JSON 协议和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐