开源鸿蒙平台 CMP 三方库「scroll-effects」适配全流程
本文记录
scroll-effects(上游makzimi/scroll-effect-lazy)接入 OpenHarmony 的完整过程,覆盖纯 Compose 库的物理内核盘点、鸿蒙 Kotlin 工具链钉版、源码集二分、真实物理引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
scroll-effect-lazy是一个 Compose Multiplatform「弹性滚动」库:把LazyColumn/LazyRow换成ScrollEffectLazyColumn/ScrollEffectLazyRow,快速滚动时被「拽住」的 item 前方就像一条弹性带——越靠前滞后越大,松手后弹簧回位。上游实现不碰布局与测量,全部是每帧在graphicsLayer里做平移。本次适配与社区早期路线(qrose / haze 的「编码器进内核、Painter 隔离」、KStore 的「kotlinx 制品线真编译」)一脉相承但边界更极端:上游整库 100% 站在 Compose 公共 API 上,能下沉到纯 Kotlin 的只有物理——速度归一、滞后弹簧、拉伸回弹与逐 item 偏移公式。这套物理被原样编译成 OpenHarmony 动态库运行在真机上,ArkUI 列表每一帧的卡片平移数值都来自 so 内的真实内核计算,不是 ArkTS 手写的近似动画。
项目地址: AtomGit/oh-tpc/scroll-effect-lazy.git
上游仓库: makzimi/scroll-effect-lazy
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
scroll-effect-lazy 为 Compose 应用提供"懒列表弹性手感"这一件事的完整答案:scrollEffect lambda 每帧在每个可见 item 的 GraphicsLayerScope 上执行,内置 elastic() 预设吃掉速度模型输出带符号滞后 lagPx,把它换算成主轴平移;ScrollEffectController 通过 NestedScrollConnection 与 VelocityTracker 追踪手指速度,用两个 Animatable 弹簧驱动滞后与拉伸;ElasticStrength 三档枚举(Hard / Normal / Loose)给出 close/far 系数与间距倍率上限。KMP 项目把这些放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套手感,但上游构建(Kotlin 2.3.21 / Compose 1.11-beta / Gradle 9.4.1 / AGP 9.1.1)不会生成 OpenHarmony 变体。
如果只在 ArkTS 里手写一个平移动画,页面可以动起来,却无法证明上游的滚动物理在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证物理内核、Kotlin/Native 动态库、C ABI、N-API、ArkUI 逐帧驱动和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 整库皆 Compose | 9 个源文件全部 import androidx.compose.*(LazyList 体系、GraphicsLayerScope、NestedScrollConnection、VelocityTracker、Animatable),没有一行可以先编译的"纯算法"。 |
| 工具链不一致 | 上游 Kotlin 2.3.21 / Gradle 9.4.1 / AGP 9.1.1 与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 社区 CMP 对不齐 | 社区 Compose for HarmonyOS 是 1.9.2 线,上游基于 Compose 1.11-beta;混编在 qrose 判例中已论证不可行。 |
| 物理跑在哪 | Compose Animatable / VelocityTracker 不可用,弹簧与速度估计必须在纯 Kotlin 里重建,且常数必须与上游逐字一致。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 逐帧桥接 | 弹性效果是 60fps 逐帧语义,桥协议必须按帧循环设计,不能一次调用完事。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:滚动物理内核的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。物理语义完全由上游公式与常数负责(tanh 归一、弹簧刚度/阻尼、Elastic 系数与上限全部照搬),ArkTS 只负责喂滚动增量、取偏移、应用 translate。
1.2 库提供的能力
Demo 提供一个真实的弹性滚动会话,对应桥接的全部入口:
- 速度追踪(scroll):每帧喂主轴增量,内核用 120ms 滑动窗口最小二乘估计速度,
tanh(|v| / 1500)归一化成拉伸量; - 滞后弹簧(scroll):lag 目标
sign(delta) × normalized × spacing × 3(无间距时 1000px),1500 刚度 / 无弹性阻尼的闭式弹簧逐步积分——被拽 item 之后(新显露侧)的卡片像在弹性带上滞后于手指; - 回弹(settle):列表停止后逐帧调用,速度清零,滞后弹簧与 400 刚度 / 0.5 阻尼的软拉伸弹簧同时回零,直到
settled=true; - 逐 item 偏移(offsets):一次批量返回整屏卡片的平移值,公式即上游
Elastic.kt——仅新显露侧移动、近邻(|distance| ≤ 2)用 close 系数、强度上限与几何防重叠上限取小、maxTranslationDp收口; - 三档弹性:
Hard/Normal/Loose,即上游ElasticStrength枚举(同一份源码编进 so); - 版本握手:
scrollEffectsVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op | 回显本次操作(reset / touchDown / touchUp / scroll / settle / offsets)。 |
rawPxPerSec / direction | 估计速度(px/s)与方向(±1 / 0)。 |
stretch / lagPx | tanh 归一拉伸 [0,1] 与带符号滞后(px)——驱动卡片平移的两个核心量。 |
isDragging / isFlinging / settled | 相位机状态;settled 是回弹循环的终止条件。 |
offsets | offsets 操作返回,与请求 distances 顺序一一对应的平移值(px)。 |
上游的完整 Compose 能力面(ScrollEffectScope 上的任意自定义效果、stickyHeader 不包裹语义等)保留在 composeMain 源码集中,等社区 Compose 对齐后一行不改接回,见 8.2。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 物理常数与公式逐字来自上游:tanh(v/1500)、spacing × 3 滞后上限、MAX_LAG_PX_FALLBACK=1000、弹簧 1500/1.0 与 400/0.5、Elastic.kt 全部系数与上限;ElasticStrength 枚举原文件零改动编入;对上游源码的修改只有两处去 @Immutable 注解。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);sample-common / sample-android / iosApp / kotlin-js-store 留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 内核零第三方依赖(纯 stdlib),不引入 kotlinx 制品线,不引入序列化库(桥内手写 MiniJson)。 |
| UI 完整 | 单页覆盖全部桥接入口:效果开关、三档强度、实时速度/滞后/拉伸读数、32 张卡片弹性列表、About 版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游的
ScrollEffectLazyColumn是 Compose 组件,鸿蒙侧表示层由 ArkUI List 承担;ArkTS Demo 是把桥暴露的物理闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 9 个源文件的依赖边界、确认"整库皆 Compose"、确定物理内核拆分面
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、HarmonyOS Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:内核重建 ── SpringSolver 闭式弹簧 + ElasticEngine 相位机/速度估计/偏移公式 + commonTest 9 项
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── List 逐帧驱动内核、settle 回弹循环、开关/强度/读数/About
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
scroll-effects/ 上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
scroll-effects/src/commonMain/ 物理内核 + 模型 + ElasticStrength 枚举(新态)
scroll-effects/src/composeMain/ 上游 Compose 层 6 文件(保留源码,ohos 不编)
scroll-effects/src/commonTest/ 9 项内核物理测试(JVM 跑,ohos 测试任务全局禁用)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
sample-common/ sample-android/ 上游 Compose 示例,留盘不进 settings
iosApp/ kotlin-js-store/ 上游 Xcode 壳 / Wasm 锁文件,留盘
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :scroll-effects 与 :example:nativeApp,rootProject.name = "ohos_scroll_effect_lazy"。上游的 AGP / Compose / vanniktech 发布插件与 Android 配置全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(2.2.21 线要求,从上游 9.4.1 下调) |
| 第三方依赖 | 无——内核纯 stdlib,桥内 JSON 手写解析 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.scrolleffects |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
与 KStore 的 2.4.20 线不同:本库内核不依赖任何 kotlinx 制品,没有"stdlib 元数据反推工具链"的约束,直接采用 cmp_qrose 在同类场景验证过的 2.2.21-1.0.0 + Gradle 8.14.1 组合,两条工具链线的经验不能互相套用。
1.3 源码依赖度盘点
上游 9 个源文件逐一盘点:
| 文件 | 依赖形态 | 处置 |
|---|---|---|
ScrollVelocity.kt | 纯 Kotlin + @Immutable 注解 | 留在 commonMain,仅去注解(全仓唯一源码修改之一) |
ScrollingItemData.kt | 纯 Kotlin + @Immutable 注解 | 留在 commonMain,仅去注解 |
presets/ElasticStrength.kt | 纯 Kotlin 枚举(三档系数) | 零改动编入 ohos |
presets/Elastic.kt | GraphicsLayerScope 扩展 | 移入 composeMain;公式移植进内核 elasticItemOffset |
ScrollEffectLazyColumn.kt / ScrollEffectLazyRow.kt | Composable + pointerInput + nestedScroll | 移入 composeMain |
ScrollEffectScope.kt | GraphicsLayerScope 接口 | 移入 composeMain |
internal/ScrollEffectController.kt | NestedScrollConnection + VelocityTracker + Animatable | 移入 composeMain;物理常数在内核重建 |
internal/WrappingLazyListScope.kt | LazyListScope 装饰器 | 移入 composeMain |
关键结论:上游没有 expect/actual(grep 为零),平台差异全靠每平台宿主;能下沉的只有"模型 + 物理",而这正是效果的的全部灵魂——ScrollEffectController 的注释自己就写着"Scroll-physics brain for one list"。
composeMain 不声明任何 sourceSet,等于从编译里静默剥离(qrose 判例的"保留源码、ohos 不编"策略):上游文件一行不改躺在树里,等社区 compose.ui ohos 制品与 CMP 1.11 对齐后接回,光栅化路径届时退化为对照示例。
1.4 页面边界
标题区 SCROLL-EFFECTS · OPENHARMONY / 版本握手行(弹性滚动内核 0.2.0-ohos.1 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 Demo / About
控制区 「效果」开关(对照上游 Off/On Switch)+ Hard / Normal / Loose 三档芯片
读数区 速度 px/s · 滞后 lag px · 拉伸 stretch 三个实时数值
列表区 32 张卡片(Home / E-commerce / Menu / Social / Coffee 循环 + 彩色序号徽标),space 12vp
关于区 版本握手、库简介、调用链分层、三档参数、仓库与社区链接
页面启动时先调 scrollEffectsVersion() 做握手,再读 display.getDefaultDisplaySync().densityPixels 为 px/vp 换算做准备。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
KStore 适配首先要验证 kotlinx ohos 制品线;本库的对应问题是:内核是否需要任何制品? 答案是不需要——速度估计、闭式弹簧、Elastic 公式全部用 Kotlin stdlib(kotlin.math、kotlin.time.TimeSource)即可表达。因此制品线验证退化为两条:
org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21-1.0.0 → eazytec maven-public 存在 ✓(cmp_qrose 同款钉版)
ohosArm64() / ohosX64() preset → HarmonyOS Kotlin 发行版自带 ✓
工具链不需要跟着任何依赖走,直接复用社区已审组合:Kotlin 钉 2.2.21-1.0.0、Gradle 降到 8.14.1(2.2.21 旧线必须降,KStore 的 2.4.20 线才允许 9.x)。顺带删掉上游的 gradle/gradle-daemon-jvm.properties——它钉的 JetBrains JDK toolchain 会触发 Gradle 8.14 的自动下载,与鸿蒙构建的 JDK 钉法冲突。
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_scroll_effect_lazy"
include(":scroll-effects")
include(":example:nativeApp")
2.2 编译真实内核
scroll-effects/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
// 刻意为空:ohos 内核零依赖
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
实测:
.\gradlew.bat :scroll-effects:compileKotlinOhosArm64 :scroll-effects:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.3 测试依赖的处置:kotlin(“test”) 没有 ohos 制品
commonTest 的依赖表对 ohos 测试编译同样可见,kotlin-test 解析不到 ohos 变体就会卡死 compileTestKotlinOhosArm64。处置是两条纪律的组合:测试统一在 JVM 跑(jvmTest),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样测试文件零改动保留,ohos 侧只承担编译与链接。
第 3 阶段:内核重建(技术难点)
3.1 物理分层
上游 ScrollEffectController(Compose Animatable 驱动)
↓ 常数与公式逐字搬迁
ElasticEngine(纯 Kotlin,commonMain)
↓ ScrollVelocity 快照
C ABI ScrollEffectsDispatch
↓
ArkUI List 逐帧 translate
上游「大脑」的每一项物理在内核里都有对应物,且常数逐字一致:
| 物理量 | 上游 | 内核 |
|---|---|---|
| 速度归一 | tanh(v / 1500f) | velocityToStretch 同式 |
| stretch 抬升 | 目标高于当前时 snapTo | stretchSpring.snap(高值) |
| stretch 回零 | spring(StiffnessMediumLow=400, DampingRatioMediumBouncy=0.5) | SpringSolver(400, 0.5).step(0) |
| lag 目标 | -sign(consumed.y) × normalized × spacing × 3(无间距 1000px) | sign(deltaPx) × normalized × cap(符号约定等价,见 8.1) |
| lag 弹簧 | spring(StiffnessMedium=1500, DampingRatioNoBouncy=1) 每帧重定向 | SpringSolver(1500, 1).step(target) 保留速度重定向 |
3.2 SpringSolver:闭式弹簧
Compose spring 本身是解析实现的单位质量阻尼振子。内核用同一套闭式解逐步推进,重定向目标时延续当前速度(与 animateTo 重定向语义一致):
class SpringSolver(stiffness: Float, dampingRatio: Float) {
var position = 0f; private set
var velocity = 0f; private set
private val omega = sqrt(stiffness) // mass = 1
private val zeta = dampingRatio
fun snap(value: Float) { position = value; velocity = 0f }
fun step(target: Float, dt: Float) {
val x0 = position - target; val v0 = velocity
if (zeta < 1f) { // 欠阻尼(400/0.5)
val omegaD = omega * sqrt(1f - zeta * zeta)
val env = exp(-zeta * omega * dt)
val c = cos(omegaD * dt); val s = sin(omegaD * dt)
val c2 = (v0 + zeta * omega * x0) / omegaD
position = target + env * (x0 * c + c2 * s)
velocity = env * (omegaD * (-x0 * s + c2 * c) - zeta * omega * (x0 * c + c2 * s))
} else { // 临界阻尼(1500/1.0)
val b = v0 + omega * x0
val env = exp(-omega * dt); val poly = x0 + b * dt
position = target + poly * env
velocity = (b - omega * poly) * env
}
}
}
闭式解无累积误差;dt 钳制在 (0, 100ms] 抗掉帧;NaN 兜底直接落回静止,杜绝桥调用抖动炸掉积分器。
3.3 ElasticEngine:相位机 + 速度估计 + 偏移公式
- 相位机
Idle → Drag →(touchUp) Fling →(onScrollStop) Settle → Idle,对应上游isDragging(UserInput 源)/isFlinging(preFling→postFling 窗口)语义; - 速度估计:Compose
VelocityTracker的等价物——120ms 滑动窗口(≤64 样本)上的位置-时间最小二乘斜率,单位 px/s; - elasticItemOffset:
Elastic.kt公式逐字移植——
distance == 0 或 distance × lagPx <= 0 → 0(被拽项不动、trailing 侧不动)
coefficient = |distance| <= 2 ? close : far
strengthCap = capSpacingMultiplier × spacing
overlapSafeCap = spacing / (close − far) ← 防止近邻/远邻在系数跳变处相撞
effectiveCap = min(strengthCap, overlapSafeCap)
translationPx = clamp(lagPx, ±effectiveCap) × coefficient,再 clamp 到 ±maxTranslationDp×density
构造函数留了 nowMs: () -> Long 注入缝(默认 TimeSource.Monotonic),commonTest 用假时钟每步 +16ms 驱动,无 sleep、无 flake。
第 4 阶段:原生桥接
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libscrolleffects_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosscrolleffects.so)
│ ScrollEffectsDispatch / ScrollEffectsVersion
▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
@CName("ScrollEffectsVersion")
fun scrollEffectsVersion(): String = "0.2.0-ohos.1"
@CName("ScrollEffectsDispatch")
fun scrollEffectsDispatch(request: String): String = try {
/* op 分发 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ScrollEffectsDispatch | scrollEffectsDispatch(req) | reset / touchDown / touchUp / scroll / settle / offsets 六个 op 一口进 |
ScrollEffectsVersion | scrollEffectsVersion() | fork 版本握手(真机 About 页数据来源) |
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI;非法 JSON、缺字段、不支持的 op 全部走同一个 ok:false 通道。
4.3 C++ N-API 方法分发与内存纪律
static napi_value NapiScrollEffectsDispatch(napi_env env, napi_callback_info info) {
/* napi_typeof 校验必须是 string → JsToUtf8 取参 →
ScrollEffectsDispatch(request.c_str()) → napi_create_string_utf8 包装 →
libohosscrolleffects_symbols()->DisposeString(raw) 归还 Kotlin 运行时 */
}
KN 返回的字符串必须经 DisposeString 归还,绝不 free();libohosscrolleffects_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
set(KN_SO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH})
set(KN_SO ${KN_SO_DIR}/libohosscrolleffects.so)
if(NOT EXISTS ${KN_SO})
message(FATAL_ERROR "Missing ${KN_SO}. Run :example:nativeApp:linkDebugSharedOhosArm64 / OhosX64 first.")
endif()
add_library(ohosscrolleffects SHARED IMPORTED)
add_library(scrolleffects_napi SHARED napi_init.cpp)
target_link_libraries(scrolleffects_napi PUBLIC
libace_napi.z.so libhilog_ndk.z.so ohosscrolleffects)
OHOS_ARCH 为空或 KN so 缺失直接 FATAL_ERROR;$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。类型包 types/libscrolleffects_napi/index.d.ts 两行声明 scrollEffectsDispatch / scrollEffectsVersion。
第 5 阶段:ArkUI 宿主
5.1 事件接线:把 ArkUI List 的滚动喂给内核
// ArkTS 严格编译禁对象字面量(连 Record 字面量都拒),请求手拼 JSON 字符串
private dispatchRaw(request: string): EngineState {
return JSON.parse(scrollEffectsDispatch(request)) as EngineState;
}
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) this.dispatchRaw('{"op":"touchDown"}');
else if (event.type === TouchType.Up) this.dispatchRaw('{"op":"touchUp"}');
})
.onDidScroll((delta: number, offset: number) => {
const res = this.dispatchRaw(
`{"op":"scroll","deltaPx":${delta * this.density},"spacingPx":${this.spacingPx()}}`);
this.velocity = res.rawPxPerSec ?? 0;
this.lagPx = res.lagPx ?? 0;
this.stretch = res.stretch ?? 0;
this.applyOffsets(this.lagPx); // 批量取偏移并应用 translate
})
.onScrollStop(() => this.startSettle()) // 每帧 op:'settle' 直到 settled === true
被拽 item 的确定:卡片 onTouch(Down) 记录 index(对应上游 onTouchDown 的命中测试);未命中任何卡片时回退 onScrollIndex(first, last) 的可视区中点(对应上游「最近的可见 item」回退)。
5.2 逐 item 偏移与回弹循环
private applyOffsets(lagPx: number): void {
/* distances = i - draggedIndex 逐项拼接成 "1,2,3,…" */
const request = `{"op":"offsets","lagPx":${lagPx},"spacingPx":${this.spacingPx()},` +
`"strength":"${this.strengthName}","maxTranslationDp":150,"density":${this.density},` +
`"distances":"${distances.join(',')}"}`;
const res = JSON.parse(scrollEffectsDispatch(request)) as OffsetsResponse;
/* offsets 字符串 split 后 /density 转 vp,写入 offsetsVp Map,卡片 .translate({ y }) */
}
startSettle 用 setTimeout 链每 16ms 步进一次 settle,刷新读数与偏移,settled === true 后停表并清零速度读数——这就是松手后整条列表弹簧回位的动画来源,每一帧的数值都出自 so。
5.3 对比度和错误边界
效果开关关掉时:停 settle 定时器、清偏移 Map、读数清零——列表立即退回普通滚动,与上游「scrollEffect = null 直接透传原生 composable」的语义对应。ok:false 时错误原文进读数区;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosscrolleffects)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/org/terminator/ohos/scrolleffects/
│ └── Bridge.kt @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.scrolleffects、versionName 0.2.0-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohosscrolleffects_api.h
├── src/main/cpp/types/libscrolleffects_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面(Demo / About 双 Tab)
6.2 构建、签名和安装
.\gradlew.bat :scroll-effects:jvmTest # 9 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 产物自动拷入 example/harmonyApp/entry/libs/{arm64-v8a,x86_64}/
# DevEco Studio 打开 example/harmonyApp(不是仓库根目录)
# File → Project Structure → Signing Configs → 勾选 Automatically generate signature
签名材料写入本机,不进入仓库(仓库内 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.scrolleffects -m entry
四、完整代码对照
4.1 整体架构
com.maxkach.scrolleffects/**(上游物理常数 + 模型 + ElasticStrength 原样编译)
│ tanh 归一 / 弹簧 1500·1.0 与 400·0.5 / Elastic 系数与上限
internal/SpringSolver + ElasticEngine(内核重建,纯 stdlib)
│ 相位机 + LSQ 速度估计 + 闭式弹簧 + elasticItemOffset
Bridge.kt + MiniJson
│ ScrollEffectsDispatch / ScrollEffectsVersion
libscrolleffects_napi.so
│ scrollEffectsDispatch / scrollEffectsVersion
Index.ets
│ List 逐帧喂增量 / settle 回弹 / translate 应用偏移
▼
ArkUI 真机页面(Demo / About 双 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_scroll_effect_lazy、示例模块移出 |
gradle/libs.versions.toml | 只剩 Kotlin 2.2.21-1.0.0 单条目 |
build.gradle.kts(根) | 版本 0.2.0-ohos.1、全局禁用 Ohos*Test* 任务 |
scroll-effects/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,commonMain 零依赖 |
scroll-effects/src/commonMain/.../internal/{SpringSolver,ElasticEngine}.kt | 内核:闭式弹簧 + 引擎 |
scroll-effects/src/commonMain/.../{ScrollVelocity,ScrollingItemData}.kt | 上游模型(仅去 @Immutable) |
scroll-effects/src/commonMain/.../presets/ElasticStrength.kt | 上游枚举零改动编入 |
scroll-effects/src/composeMain/(6 文件) | 上游 Compose 层,保留不编 |
scroll-effects/src/commonTest/.../ElasticEngineTest.kt | 9 项物理测试 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(18 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | ElasticEngine.velocityToStretch | `tanh( |
| 内核 | ElasticEngine.onScrollDelta | 速度估计 + lag 目标 + 双弹簧一步积分(对应 onPostScroll) |
| 内核 | ElasticEngine.onSettleStep | 惯性结束后的回弹帧(对应 onPostFling 双弹簧回零) |
| 内核 | ElasticEngine.elasticItemOffset | Elastic.kt elasticEffect 公式:门控 / close-far 系数 / 三重上限 |
| 内核 | SpringSolver.step | 闭式阻尼弹簧,重定向保留速度(对应 animateTo 重定向) |
| 模型 | ElasticStrength.Hard/Normal/Loose | 上游枚举原样:0.2/0.05/×1、0.5/0.2/×3、0.9/0.6/×3 |
| Native | ScrollEffectsDispatch / ScrollEffectsVersion | C ABI JSON 导出 |
| N-API | scrollEffectsDispatch(req) / scrollEffectsVersion() | 向 ArkTS 暴露能力 |
| ArkTS | applyOffsets / startSettle | 批量偏移应用与回弹循环 |
4.4 ArkTS 与 Kotlin 的边界
// ArkTS:一帧滚动 + 批量偏移,两次 NAPI 往返
const state = JSON.parse(scrollEffectsDispatch(
`{"op":"scroll","deltaPx":${delta * density},"spacingPx":${12 * density}}`));
const offsets = JSON.parse(scrollEffectsDispatch(
`{"op":"offsets","lagPx":${state.lagPx},"spacingPx":${12 * density},` +
`"strength":"Loose","maxTranslationDp":150,"density":${density},"distances":"1,2,3,4,5"}`));
// offsets.offsets → "0.0,2.4,5.1,…" split 后 /density 转 vp 喂 .translate({ y })
// Kotlin:同一帧的另一端,公式与常数全部来自上游
val velocity = engine.onScrollDelta(deltaPx, spacingPx)
val target = sign(deltaPx) * velocityToStretch(abs(velocity.rawPxPerSec)) * lagCap(spacingPx)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:下沉物理而不是抽"零依赖内核"
上游没有可整体编译的纯算法文件(对比 qrose 的编码器、KStore 的存储引擎),能下移的只有公式与常数。做法是在 commonMain 重建载体、逐字搬运语义:tanh(v/1500)、spacing × 3、弹簧 1500/1.0 与 400/0.5、Elastic.kt 三重上限全部照搬,ElasticStrength 枚举整文件零改动编入。jvmTest 里专门有断言钉住这些数值(close 0.5 / far 0.2、Hard 上限 2.4px 等),上游调常数时测试立刻报错。
决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1,而不是追 2.4.20 线
KStore 升 2.4.20-0.1.0-07 是被 kotlinx 制品的 stdlib 元数据逼的;本库内核零依赖,没有这个约束,直接采用 cmp_qrose 已审、且在本机验证过可产双 ABI so 的组合。Gradle 随线降到 8.14.1,gradle-daemon-jvm.properties 一并删除。
决策 3:闭式弹簧而不是数值积分
Compose spring 是解析实现的阻尼振子,Animatable.animateTo 重定向保留当前速度。闭式解一步到位复刻这个语义:无累积误差、dt 钳制后对掉帧鲁棒、NaN 兜底,并且能在测试里精确断言(临界阻尼零过冲、欠阻尼过冲回零、重定向收敛到新目标)。
决策 4:桥协议面向帧循环设计
弹性效果是 60fps 逐帧语义。协议按帧组织:每帧一次 scroll(或 settle)+ 一次批量 offsets(distances 逗号串一次取回整屏偏移),避免每 item 每帧一次 NAPI 调用;状态留在 so 内单例,reset 显式复位。
决策 5:composeMain 保留不编 + jvmTest 交叉验证
composeMain 不声明 sourceSet 即静默不参与编译(qrose 判例),上游文件一行不改;内核与 so 共用同一份 commonMain,9 项 jvmTest 跑的就是 so 里执行的物理——这是"内核没漂"的唯一可信证明。
六、测试与验证
6.1 测试环境
本次验证使用 Windows x64 宿主机、JDK 21(Eclipse Adoptium 21.0.12)、Gradle 8.14.1、HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)、DevEco Studio 5.1.0 Release 与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :scroll-effects:jvmTest
9 项用例全部通过(假时钟逐步 +16ms 驱动,无 sleep 无 flake):
| 测试类 | 用例 | 钉住什么 |
|---|---|---|
ElasticEngineTest | criticallyDampedSpringReachesTargetWithoutOvershoot | lag 弹簧 1500/1.0 收敛且零过冲 |
bouncySpringSwingsPastTargetAndSettles | stretch 弹簧 400/0.5 越过目标回摆并归零 | |
retargetMidFlightSettlesAtNewTarget | 重定向保留速度、收敛到新目标 | |
draggedItemAndTrailingSideDoNotMove | Elastic 门控:被拽项与 trailing 侧恒为 0 | |
closeAndFarNeighboursUseTheirCoefficients | 近邻 0.5 / 远邻 0.2(Normal 档) | |
offsetIsCappedByStrengthAndMaxTranslation | 强度上限、防重叠上限、maxTranslationDp 三重 clamp | |
forwardScrollBuildsPositiveLagThatSettlesToZero | 全链路:前进增量 → lag>0 → settle 精确归零 | |
flingPhaseIsReportedAfterTouchUp | Drag/Fling 相位语义 | |
resetReturnsToRest | 显式复位 |
这套测试在适配过程中直接抓出过一次方向符号反转(见 8.1 第 2 条),是内核物理的活闸门。
6.3 HAP 验证
:scroll-effects:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填 entry/libs)
llvm-nm -D: ScrollEffectsDispatch / ScrollEffectsVersion / libohosscrolleffects_symbols 均为全局 T 符号
CompileArkTS: finished(Demo / About 双 Tab)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
KN so arm64 约 3.6 MB、x86_64 约 3.4 MB(debugShared 未 strip,含 Kotlin/Native 运行时);每个 ABI 目录另配 libc++_shared.so(约 1.8 / 1.7 MB),CMake POST_BUILD 把三件套收进 HAP。
6.4 功能用例
- 启动后 About / 标题区出现版本握手
scrollEffectsVersion() → 0.2.0-ohos.1(真实 Native 调用,非静态文字); - Demo 页自上而下:效果开关、Hard / Normal / Loose 三档芯片、速度 / 滞后 / 拉伸三个读数、32 张卡片列表;
- 快速上滑:下方(新显露侧)卡片滞后于手指、间距被拉开——lag 读数为正且随速度增大;
- 切换三档:Hard 几乎不可察觉、Normal 适中、Loose 明显,与上游三档系数(cap ×1 / ×3 / ×3 + close 0.2 / 0.5 / 0.9)一致;
- 松手:整条列表弹簧回位,lag 读数逐步衰减到 0,
settled后读数静止; - 反向快速下滑:滞后符号翻转,弹性行为对称;
- 关闭效果开关:立即退回普通列表(偏移清零),重新打开
reset后恢复; - 速度 / 滞后 / 拉伸读数随手指实时变化,
tanh归一使拉伸始终落在 [0,1]。
6.5 验证结论
物理内核、Kotlin/Native 动态库、C ABI、N-API、ArkUI 逐帧驱动和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,快速滚动时卡片呈现真实的弹性滞后与松手回弹,全部数值来自 libohosscrolleffects.so 内运行的上游物理(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/scroll-effect-lazy.git
cd scroll-effect-lazy
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :scroll-effects:jvmTest # 9 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco 打开 example/harmonyApp → 自动签名 → Build HAP
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.scrolleffects -m entry
hdc shell hilog | grep ScrollEffects
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:scroll-effects真正编译成ohosArm64/ohosX64动态库; - 平台滚动符号约定相反:Compose
consumed.y前进滚动为负(上游 lag 目标带-sign(delta)),ArkUIonDidScroll的 delta 前进为正。内核按"正 = 向列表末尾"重新表达为+sign(deltaPx)才与上游等价;首版照抄-sign导致弹性行至 trailing 侧,jvmTest的forwardScrollBuildsPositiveLag…断言当场抓出——这正是内核测试存在的意义; - 速度估计器首帧只有 1 个样本返回 0 是正常行为(
VelocityTracker同样需要至少两点),Demo 无需特判,方向读数从第二帧起有效; kotlin("test")没有 ohos 制品:测试统一 JVM 跑 + 根构建禁用Ohos*Test*任务,测试文件零改动;gradle-daemon-jvm.properties必须删:上游用它钉 JetBrains JDK toolchain,Gradle 8.14 会按 criteria 自动下载,与鸿蒙构建的org.gradle.java.home钉法冲突;- 用户级
~/.gradle/gradle.properties会覆盖项目级org.gradle.java.home:若其指向的 JDK 启动不稳(本机出现0xC0000142DLL 初始化失败),命令行-Dorg.gradle.java.home=<可用 JDK21>强制覆盖; - ArkTS 对象字面量限制比预期更严:hvigor 的
arkts-no-untyped-obj-literals连Record<string, string|number>字面量(实参与带类型声明的变量)一律拒绝。桥请求全部改为模板字符串手拼 JSON,字段全为数字与受控枚举,无注入面; - ArkUI
List的直接子组件必须是ListItem(ForEach里也要包一层);组件自定义状态不能叫tabIndex(与 CustomComponent 内置属性重名),改名currentTab; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();头文件只声明ExportedSymbols与两个业务符号,不拖生成头; - CMake 找不到 KN so 直接 FATAL_ERROR 并提示先跑 gradle link 任务;
abiFilters必须双 ABI,每个 ABI 三件套缺一不可,否则 HAP 安装报9568347; $ORIGINrpath + POST_BUILD 拷贝保证三件套同目录;ArkTS 只 importlibscrolleffects_napi.so,CAdapter 不导出@CName业务符号;build-profile.json5的signingConfigs留空数组:手写缺material字段会被 hvigor schema 拒绝(00303038),勾选 DevEco 自动签名后由 IDE 回填,证书绝不进入仓库;.gitignore的根锚定盖不住嵌套工程:DevEco 打开后生成的.idea/、oh_modules/、entry/libs/、*.so要单独追加忽略。
8.2 已知问题
- 鸿蒙侧无
ScrollEffectLazyColumn / ScrollEffectLazyRow / ScrollEffectScope:Compose 层在composeMain保留不编,等社区compose.uiohos 制品与上游 CMP(1.11 线)对齐后接回; scrollEffectlambda 的任意自定义效果(alpha / rotation / scale 等 GraphicsLayerScope 操作、自定义 preset)不跨桥——桥只暴露elastic预设的偏移输出;- 速度估计是最小二乘近似,与 Compose
VelocityTracker的多项式拟合实现不同源;tanh归一后差异被显著压缩; - 惯性(fling)阶段的速度来自 ArkUI 滚动增量反推,ArkUI 自带 fling 减速曲线不暴露,内核只看到合成后的逐帧 delta;
stickyHeader(上游刻意不包裹)在 Demo 中未呈现;- android / ios / wasmJs 目标与上游示例模块剥离出构建(源文件留在树里),发布(vanniktech / Maven Central / 签名)一并剥离,本 fork 不发 Maven Central;
- 桥为同步一调用一返回,每帧两次 NAPI 往返(状态 + 偏移),Demo 场景足够,未做大规模列表的性能剖析。
8.3 未来优化方向
- 补
ScrollEffectLazyRow横向 Demo Tab(内核天然支持,仅 ArkTS 演示面); - 评估把
distances/offsets改为二进制数组句柄,压缩每帧 JSON 体积; - 在 CI 中加入 ohos 编译、
llvm-nm符号检查、jvmTest与 HAP 构建; - 社区
compose.uiohos 制品对齐后把composeMain接回编译,ArkTS Demo 退化为对照示例; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套手工回填。
九、总结
9.1 核心难点回顾
上游 scroll-effect-lazy(整库皆 Compose)
→ 盘点出唯一可下沉资产:滚动物理(公式 + 常数)
→ 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 / Gradle 8.14.1)
→ SpringSolver 闭式弹簧 + ElasticEngine 相位机 / LSQ 速度估计 / Elastic 公式
→ ohosArm64 / ohosX64 KLIB + 9 项 jvmTest 交叉验证
→ C ABI(@CName × 2)+ 面向帧循环的 JSON 协议
→ C++ N-API(DisposeString 纪律)
→ ArkUI List 逐帧驱动(scroll / settle / offsets)
→ 真机验收(弹性滞后 / 三档强度 / 松手回弹 / 版本握手)
9.2 封装层次
- 物理层:上游常数与公式——
tanh(v/1500)、spacing × 3滞后上限、弹簧 1500/1.0 与 400/0.5、Elastic.kt三重上限、ElasticStrength三档枚举(原文件零改动编入); - 内核层:
SpringSolver闭式积分 +ElasticEngine相位机、速度估计、偏移计算,纯 stdlib; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、事件接线、逐帧偏移应用、回弹循环与读数;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- "整库皆 Compose"的库也有可下沉资产,但要按语义拆而不是按文件拆:qrose 按文件二分(编码器 / Painter),KStore 按制品线整库编译,scroll-effects 只能抽出"公式 + 常数"这一层——拆分面由库的依赖形态决定,没有通用模板;
- 符号约定是跨平台滚动类的第一陷阱:Compose 与 ArkUI 对"前进滚动"的 delta 符号相反,照抄上游符号会把弹性行至错误一侧;内核测试必须包含方向断言,让约定差异在 JVM 上暴露而不是真机上;
- 帧循环类效果的桥协议要按"每帧往返次数"设计:状态 + 批量偏移两次调用是可接受上限;把状态留在 so、把偏移按 distances 批量取回,比每 item 每帧一次调用或把状态在 JSON 里往返都更稳。
9.4 适配成果
当前适配已完成:
- scroll-effects
0.2.0(上游 MIT)弹性滚动物理的双 ABI Kotlin/Native 编译(对上游源码的唯一修改是两处去@Immutable注解,ElasticStrength枚举零改动编入); SpringSolver+ElasticEngine纯 Kotlin 内核(闭式弹簧、LSQ 速度估计、Elastic.kt偏移公式,常数与上游逐字一致);- 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 双 Tab 验收页面(效果开关、三档强度、实时读数、32 卡片弹性列表、版本握手);
- 9 项 JVM 交叉验证全绿 + 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)