开源鸿蒙平台 CMP 三方库「DragSelectCompose」适配全流程
本文记录
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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin,org.gradle.java.home 写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 从上游 9.3.1 降级——2.2.21 线不支持 Gradle 9.x) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release(5.1.0(18) SDK) |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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(上游) | 4 | androidx.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 | 参数 | 对齐上游 |
|---|---|---|
reset | count(默认 100)、keys(CSV,缺省 “0”…“n-1”)、initial、threshold(默认 40) | rememberDragSelectState(initialSelection) |
start | index | onDragStart → startDrag |
drag | x, y, viewport, total、bounds(五元组 CSV:index,x,y,w,h,...) | onDrag → 命中 + 范围选择 + 滚动速度 |
drop | — | onDragEnd / onDragCancel → stopDrag |
select | index | dragSelectToggleable |
mode | value = enable / disable / toggle | enableSelectionMode 族 |
add / remove | index | addSelected / removeSelected |
clear | — | disableSelectionMode(上游 clear() 的替代) |
reconcile | keys(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 方法 | 覆盖内核 |
|---|---|---|
DragSelectStep | dragSelectStep(req) | 引擎全部公开面 + 会话管理 |
DragSelectVersion | dragSelectVersion() | 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.kt | 28 项上游行为对齐测试 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、会话单例、MiniJson、错误通道 |
ohos/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
ohos/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
ohos/.../ets/pages/Index.ets | ArkUI 手势宿主 |
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 保留(上游同款) |
| Native | DragSelectStep / DragSelectVersion | C ABI JSON 导出 |
| N-API | dragSelectStep(req) / dragSelectVersion() | 向 ArkTS 暴露能力 |
| ArkTS | sendDrag(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 功能用例
- 启动后标题区出现
引擎版本 3.3.0-ohos.1(来自真实 Native 调用,非静态文字); - 长按任意格子 ≥350ms,状态行红点闪烁(
haptic:true视觉降级),该格子进入选中集并出现角标; - 按住拖动,手指扫过的索引区间连续入选;先向前拖再向后拖,先前选中但拖出区间的条目保持选中(上游核心语义);
- 拖到网格底边缘,列表自动向下滚动且选中集持续增长,状态行
autoScroll出现正值;拖到顶边缘为负值(真机日志可见auto=-21@91这样的速度/命中记录); - 拖出最后一行,
past-last判定把最后一项纳入选中; - 松手后角标保留,再拖从新起点重新计算区间;
- 点「选择模式」进入手动模式,点选/取消单个条目;点「清空」退出并清空;
- 点「重对账-3」(模拟数据刷新后前 3 项消失),选中集中 0/1/2 被剔除,其余按 key 保留;
- 每步操作进日志区(时间戳,
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 踩坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让
:core真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 2.2.21 工具链线不支持 Gradle 9.x,上游 wrapper 9.3.1 必须降到 8.14.1,wrapper jar 一并替换;
- Poko /
@Stable/@Composable/ compose runtime 注解鸿蒙编不过——处置不是剥注解,而是把整个 Compose 文件停放composeMain(不是 Kotlin 源集,ohos 编译器看不到),算法用纯 Kotlin 重写为引擎; - 桥层首次链接失败:
internal fun step暴露了 file-private 的MiniJson,把MiniJson提为internal class后通过——KN 的可见性检查在链接期才炸; - ArkTS 侧三连坑(本机 SDK 实测):
LongPressGestureInterface没有链式.duration()(必须走构造参数{ repeat, duration });GestureEvent类型上没有absoluteX/absoluteY;TouchObject上没有globalX/globalY(只有windowX/windowY与可选的globalDisplayX)。处置是直接读本机 DevEco SDK 的.d.ts按真实字段写代码,触点统一从.onTouch取windowX/windowY; Area.globalPosition与TouchObject.windowX/windowY同为窗口坐标系,相减即得容器内坐标——两套坐标混用屏幕系就会整体偏移一个状态栏;PanGesture的方向枚举是PanDirection.All,GestureDirection是 SwipeGesture 的,写错不报错但手势不响应;- 根构建的
/.idea与/build是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的example/ohos/.idea/要在 ohos 的.gitignore里单独追加; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();API 18 的napi_get_value_string_utf8缓冲区参数非 const,先取长度再开std::vector<char>; - 签名材料由 DevEco 自动写入本机,证书绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙上没有 Compose LazyGrid:上游
LazyDragSelectVerticalGrid/extensions三件套停留在composeMain,Demo 的网格与角标是 ArkUI 等价呈现,不是 Compose 渲染; - 上游
hapticFeedback依赖 ComposeLocalHapticFeedback,鸿蒙侧以视觉反馈降级(红点闪烁),不接系统振动; - 命中判定需要的可见项矩形由宿主
onAreaChange喂,极端快速滚动时可能有一帧矩形滞后(与上游读layoutInfo的时序差同量级); dragselect/extensions/grid/demo模块留盘不进构建,本 fork 不发 Maven Central;- 网格条目用纯色方块模拟照片,不引网络图,保证验收环境离线可复现。
8.3 未来优化方向
- 若社区 CMP 工具链与上游对齐(1.10+ 的
compose.uiohos 制品出现),把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 三条经验
- 路线选择取决于「值钱的部分长在哪」。 纯 Kotlin 库(KStore)走真编译整库;Compose 库(qrose / pinch-grid / 本项目)走算法提取 + UI 宿主重做。判断标准不是偏好,是
@Composable能不能进 ohos 构建——工具链对不齐时,把算法与 UI 切开是唯一可验收的路。 - 算法提取的验收底线是「行为对齐」,不是「能跑」。 五段算法逐行对齐上游(双向区间、旧
current比较、右/下开区间、past-last 条件、三区速度公式),28 项测试每条钉住一条上游行为;鸿蒙上扫出来的选择序列必须和上游 DragSelectState 一致,不一致就是回归,不是「鸿蒙特性」。 - 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。
参考文档
更多推荐


所有评论(0)