开源鸿蒙平台 CMP 三方库「compose-collapsing-toolbar」适配全流程
本文记录
compose-collapsing-toolbar接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 库的内核提取、折叠状态机逐条移植、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
compose-collapsing-toolbar是 Jetpack Compose 上的CollapsingToolbarLayout等价物:CollapsingToolbarScaffold把 toolbar 高度钳制在[minHeight, maxHeight](由子项最小 / 最大高度决定),body 通过嵌套滚动与 toolbar 分摊滚动手势,toolbar 子项以pin()钉住、road()双锚点插值移动、parallax(ratio)视差三种模式摆放,progress驱动 Compose 重组联动 UI。本次适配与 KStore 的"真编译整库"路线(社区 Maven 上有完整 HarmonyOS kotlinx 制品线)恰好是镜像场景:这个库一行 kotlinx 依赖都没有,全部源码 import 的是androidx.compose,后者永远不会出现在 ohos 制品线上——因此走的是社区早期已过审的零依赖内核提取路线(qrose / WheelPickerCompose / Flippable / swipe 同路):把 UI 下面的折叠算术(高度钳制、进度、亚像素累计器、三种ScrollStrategy的滚动分布、road / parallax / pin 摆放公式)移植为零依赖纯 Kotlin 内核,编译成 OpenHarmony 动态库;被提取的不是算法库,而是一台交互状态机,ArkUI 页面逐帧驱动它完成验收。
项目地址: AtomGit/oh-tpc/compose-collapsing-toolbar.git
上游仓库: onebone/compose-collapsing-toolbar
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-collapsing-toolbar 为 Compose 应用提供"详情页大图 toolbar 折叠成小标题栏"这一件事的完整答案:rememberCollapsingToolbarScaffoldState() 一行拿状态,scrollStrategy 三选一定义滚动分摊顺序,toolbar 子项用 Modifier.pin() / road() / parallax() 声明摆放语义,state.toolbarState.progress 让字号、透明度随折叠进度插值重组,expand() / collapse() 补间到端点。上游(me.onebone:toolbar-compose 2.3.5)是纯 Android 库:lib 模块 8 个 Kotlin 文件全部站在 androidx.compose 上(Layout、ParentDataModifier、nestedScroll、rememberSaveable),连 KMP 结构都没有。OpenHarmony 应用同样需要这套折叠语义,但上游构建(AGP 7.3.1 / Kotlin 1.7.20 / Compose Compiler 1.3.2)不会生成 OpenHarmony 变体。
如果只在 ArkTS 里手写一个折叠页头,页面可以滚动,却无法证明上游那台调过无数次的折叠状态机在 OpenHarmony ARM64 设备上完成编译、链接和执行——而那台状态机里藏着上游最值钱的东西:钳制的先后顺序、亚像素增量的累计与截断、三种策略下"谁先吃滚动量"的分布顺序、锚点按当前高度解析的 road 插值。适配需要同时验证内核数学、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 AGP 7.3.1 / Kotlin 1.7.20 与鸿蒙工具链无法共存于一个 Gradle 构建,根构建必须整体替换。 |
| 依赖矩阵 | 全库唯一依赖 androidx.compose,社区 Maven(eazytec)上不存在也不会出现其 ohos 制品——"真编译整库"路线在此前提判死。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交互语义跨不了桥 | 嵌套滚动的 pre/post 分发、fling 决策、补间动画都是逐帧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:折叠算术的内核提取与逐条移植、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。UI 原样留在 composeMain 隔离区等待社区 CMP 工具链对齐,ArkTS 只负责手势、动画时钟、渲染与调用 Native。
1.2 库提供的能力
Demo 提供一场真实的折叠会话,对应桥接的全部入口:
- 测量回报(layout):toolbar 子项的 min / max 高度进内核,触发与上游测量阶段完全相同的钳制副作用(先 min 后 max、初始
Int.MAX_VALUE被拉到展开高度); - 逐帧滚动(scroll):手势每帧的 dy 走 pre 分布,body 消费后余量走 post,返回 consumed / height / offset / progress;
- fling 决策(fling):松手速度进内核,按策略与阶段返回 settle 目标(expand / collapse / none)与 body 剩余速度;
- 补间动画(anim):
expand()/collapse()的 ~200ms 补间每帧经dispatchRawDelta落高度,与上游AnimationState.animateTo + scrollBy同路径; - 子项摆放(place):pin / road / parallax 三种模式在当前 progress 下的坐标由内核计算,ArkTS 只做渲染;
- 版本握手(info):返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
height / min / max | 当前高度与钳制边界(读数行的数据来源)。 |
offset | body 偏移(EnterAlways 系把 toolbar 推出屏幕的量)。 |
progress | 折叠进度 0…1(联动 UI 的插值源)。 |
consumed | 本帧被脚手架吃掉的滚动量,余量归 body。 |
target / targetHeight / body | fling 的 settle 决策与 body 剩余速度。 |
上游 UI 层能力(rememberSaveable 状态恢复、作用域 DSL、deprecated 的 AppBarContainer / ToolbarWithFabScaffold)属于 Compose 专有语义,隔离在 composeMain 不进内核;内核数学全部有 17 项 jvmTest 交叉验证用例钉死,证明 ohos .so 里执行的折叠逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 折叠算术从上游 UI 逐条提取移植(每条规则 KDoc 标注上游文件与行号),Compose UI 原样隔离进 lib/src/composeMain/,不删不改。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 Android 演示 app/ 留盘不进构建。 |
| 桥接稳定 | 使用 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 内核零第三方依赖(连 kotlinx 都不碰),commonMain 一行依赖坐标都不需要。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:三种策略、parallax / road / pin、enabled 开关、expand / collapse 补间、实时引擎读数。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
compose-collapsing-toolbar本身就是 UI 库,UI 层进不了 ohos 是这次适配的起点而不是缺陷;ArkTS Demo 复刻的是上游MainActivity与ParallaxActivity两个官方演示,骨架数字全部来自内核。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、判定制品线(结论:没有)、确定交付物
第 2 阶段:工具链钉版 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 + Gradle 8.14.1
第 3 阶段:内核提取与移植 ── 折叠状态机逐条搬运、jvmTest 交叉验证(含上游 androidTest 场景)
第 4 阶段:桥协议建模 ── 九 op 扁平 JSON 契约、符号约定、错误通道
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 手写嵌套滚动管线、四 Tab Demo、HAP、签名、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
lib/ 上游库模块(改造为纯 KMP:jvm + ohosArm64 + ohosX64)
lib/src/commonMain/ 新增:折叠内核三文件(引擎 + 摆放 + 测量)
lib/src/jvmTest/ 新增:17 项交叉验证
lib/src/composeMain/ 上游 Compose 源码 8 文件 + Manifest + 上下测试,隔离不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
app/ 上游 Android 演示,留盘不进 settings
img/ 上游动图(enter-always 等),留盘
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :lib 与 :example:nativeApp,rootProject.name = "ohos_collapsing_toolbar"。上游的 AGP classpath、buildscript 块随根构建替换整体剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 替换;2.2.21 旧线必须降 8.x) |
| 依赖 | commonMain 零依赖(无 kotlinx、无 androidx) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.collapsingtoolbar |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 上游文件 | 行数 | 依赖形态 | 处置 |
|---|---|---|---|
CollapsingToolbar.kt | 387 | androidx.compose UI + 动画 + 状态 | 隔离 composeMain;状态机数学提取进内核 |
CollapsingToolbarScaffold.kt | 201 | Layout / nestedScroll / rememberSaveable | 隔离;测量与摆放公式提取 |
ScrollStrategy.kt | 240 | NestedScrollConnection | 隔离;三策略分布数学逐分支提取 |
AppBarContainer.kt / ToolbarWithFabScaffold.kt / FabPlacement.kt / FabPosition.kt / Annotations.kt | — | deprecated 兼容层 / 注解 | 隔离,不入内核 |
expect / actual 声明全库为零——上游是纯 Android 库,没有平台源码集。这决定了内核提取的唯一工作是"把数学从 Compose 声明里剥出来",不需要补任何 actual。
1.4 页面边界
标题区 COMPOSE-COLLAPSING-TOOLBAR · OPENHARMONY / 版本握手行(fork 与 Kotlin 版本)
Tab 切换 ExitUntilCollapsed / EnterAlways / Parallax / Engine
脚手架区 toolbar 层(pin 背景 + road 标题或 parallax 头图,clip 随内核高度)+ body 列表层
开关区 Enable collapse/expand(Parallax Tab,对应上游 enabled 参数)
控制区 expand() / collapse() / Reset(Engine Tab,~200ms 补间)
读数行 h / min / max / offsetY / progress / bodyScroll 实时刷新(内核状态直读)
错误行 桥错误通道原文(⚠ 前缀,理论不该出现)
页面启动时先调 op:info 做握手,四个 Tab 各持一个引擎实例(id 1–4)。
第 2 阶段:工具链钉版
2.1 制品线检查(本适配的路线判定)
KStore 适配的第一步是查 eazytec Maven 上有没有 kotlinx 的 ohos 制品(结论是有,走真编译整库)。本项目做同一场检查,结论相反:compose-collapsing-toolbar 的依赖表里没有任何 kotlinx 坐标,全部重量压在 androidx.compose.* 上——后者是 Jetpack 体系,社区 Maven 不会也不该出现它的 ohos 变体。两条推论:其一,"真编译整库"路线在此判死;其二,正因为零依赖,内核提取后的 commonMain 连一条依赖坐标都不用改写,不存在 KStore 那种"版本主体逐字对齐"的选型工作——这是所有已适配 CMP 库里依赖面最小的一个。
2.2 工具链钉版(2.2.21 旧线,Gradle 必须降 8.14.1)
内核零依赖,工具链选择只需对齐社区已过审先例:qrose / WheelPickerCompose / Flippable / swipe 全部钉 HarmonyOS Kotlin 2.2.21-1.0.0,该线只支持 Gradle 8.x,wrapper 从上游 7.5.1 替换为 8.14.1。KStore 的 2.4.20 新线(Gradle 9.x)在这里没有收益——两条工具链线的经验不能互相套用。
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_collapsing_toolbar"
include(":lib")
include(":example:nativeApp")
2.3 改造库模块
lib/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
jvmTest.dependencies {
implementation(kotlin("test"))
}
}
}
上游源码 lib/src/main/java/** 整体挪进 lib/src/composeMain/kotlin/**(AndroidManifest.xml、androidTest、test 一并挪入 composeMain/ 留档)。composeMain 不是注册源集,任何目标都不编译它;KMP 的 jvm() 目标默认源集是 src/jvmMain,src/main/java(Android 约定路径)不会被动进编译,但显式挪进隔离区让"哪部分在构建里"一目了然。实测:
.\gradlew.bat :lib:compileKotlinJvm :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.4 测试任务的处置
根构建全局禁用 ohos 测试任务(交叉验证统一在 JVM 跑):
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
上游 androidTest 是 Compose 仪器测试(留在隔离区),ohos 侧只承担编译与链接;内核正确性由新写的 jvmTest 承担(第 3.4 节)。
第 3 阶段:内核提取与移植(技术难点)
3.1 内核三文件
| 新文件 | 移植自 | 内容 |
|---|---|---|
CollapsingToolbarEngine.kt | CollapsingToolbar.kt:57-186 + ScrollStrategy.kt 全部 | 状态钳制、progress、dispatchRawDelta 与亚像素累计、三策略 pre/post 分布、fling 决策 |
ToolbarPlacement.kt | CollapsingToolbar.kt:256-295 + Alignment 语义 | 偏置对齐模型(九预置)、pin / road / parallax 摆放公式 |
ScaffoldMeasure.kt | CollapsingToolbarScaffold.kt:122-181 | body maxHeight 约束、body / toolbar 摆放点 |
3.2 状态机数学:必须逐字节对齐的四条
钳制顺序(CollapsingToolbar.kt:71-94):minHeight setter 赋值后 if (height < value) height = value;maxHeight setter 是 if (value < height) height = value。测量回报必须先 min 后 max(上游 measure 顺序),初始 height = Int.MAX_VALUE 由 max 拉到展开高度。
亚像素累计器(CollapsingToolbar.kt:108-124):
fun dispatchRawDelta(value: Float): Float {
val consume = if (value < 0) max(minHeight.toFloat() - height, value)
else min(maxHeight.toFloat() - height, value)
val current = consume + deferredConsumption
val currentInt = current.toInt() // 向零截断
if (abs(current) > 0f) {
height += currentInt
deferredConsumption = current - currentInt
}
return consume
}
-0.6 单独一轮什么都不动,两轮才落 1px——这是上游防抖的隐藏细节,测试里用 −0.6×4 步逐轮钉死(100→100→99→99→98)。同样的机制在脚手架 offset 上还有一份(ScrollDelegate)。
三策略分布(ScrollStrategy.kt:80-239):
| 策略 | 阶段 | dy<0(折叠) | dy>0(展开) |
|---|---|---|---|
| EnterAlways | pre | toolbar 吃到 min → 余量进 offset,钳 offset ≥ −height(当前高度) | offset 先回 0 → 余量 toolbar 吃到 max |
| EnterAlwaysCollapsed | pre | 同上 | offset 先回 0(高度不动) |
| EnterAlwaysCollapsed | post | 不处理 | 全部给 toolbar(body 已到顶) |
| ExitUntilCollapsed | pre | 全部给 toolbar | 不处理 |
| ExitUntilCollapsed | post | 不处理 | 全部给 toolbar |
fling 净决策:上游把速度交给 Android 样条 FlingBehavior 衰减到端点;内核不模拟样条,改为回答"谁接管、走向哪个 bound、body 剩多少"——EnterAlways 在 pre 吃正向速度、EnterAlwaysCollapsed 在 post 吃 body 用剩的正向速度、ExitUntilCollapsed 在 pre 吃负向 / post 吃正向,且只在还有余地(canMove)时接管,否则速度全额还给 body。
3.3 摆放公式:锚点按当前高度解析
PlacementMode.ROAD -> {
val collapsed = whenCollapsed.align(childW, childH, toolbarW, toolbarHeight)
val expanded = whenExpanded.align(childW, childH, toolbarW, toolbarHeight)
IntOffset2D(
collapsed.x + ((expanded.x - collapsed.x) * progress).roundToInt(),
collapsed.y + ((expanded.y - collapsed.y) * progress).roundToInt(),
)
}
PlacementMode.PARALLAX -> IntOffset2D(0, -((maxHeight - minHeight) * (1 - progress) * ratio).roundToInt())
两个易错点:parallax 的取整在取负之内(−62.5 → −63,不是 −62);road 的两个锚点都按 IntSize(maxWidth, height)(当前高度,CollapsingToolbar.kt:271-281)解析,锚点本身随容器收缩而移动——这是 road 运动感的来源,也是移植时最容易想当然的地方(见 8.1 踩坑第 4 条)。align 复刻 androidx 公式:((space − size) / 2 · (1 + bias)).roundToInt(),偏置 −1/0/1 对应 start/center/end。
3.4 交叉验证:连移植者的误解都能拦
lib/src/jvmTest/.../CollapsingToolbarEngineTest.kt,17 项用例,期望值全部从上游源码推导:
| 断言域 | 用例数 | 钉住什么 |
|---|---|---|
| 钳制与进度 | 4 | 上游 androidTest testCollapse 场景(layout 后 progress1、height300;累计 −250 上滑后 height==50、` |
| dispatchRawDelta | 2 | 端点停摆(−400 只吃 −250)、亚像素 −0.6×4 步逐轮高度 |
| EnterAlways 分布 | 2 | 折叠 toolbar→offset、offset 只能到 −(height)、+200 实吃 120 |
| EnterAlwaysCollapsed | 2 | 折叠高度→offset、pre 展开只还 offset、post 展开进高度 |
| ExitUntilCollapsed | 1 | pre 只吃折叠、post 只吃展开 |
| fling 决策 | 2 | 3 策略 × pre/post × 方向的 target 矩阵、bound 处速度全额还 body |
| 摆放公式 | 3 | parallax 三点(含取整方向)、road 三点(50 高容器内 CenterStart 是 (0,5))、pin 与对齐解析 |
| 脚手架测量 | 1 | bodyMax 三策略 + 越界 coerce、body / toolbar 摆放点 |
一轮真实返工:初版 road 测试按"展开态容器 300 高"算锚点,两条用例当场红——翻回上游 271-281 行确认对齐空间是当前高度,修的是测试期望而不是内核。交叉验证的价值正在于此:它不仅拦内核漂移,连移植者自己的误解一起拦。
第 4 阶段:桥协议建模
4.1 JSON 契约
me.onebone.toolbar/**(内核:引擎 + 摆放 + 测量,commonMain 编译)
↓ 折叠状态机:钳制 / 分布 / 插值
Bridge.kt(@CName × 1,扁平 JSON 进出,id 键引擎注册表)
↓ UTF-8 JSON
N-API collapsingToolbarDispatch()
↓
ArkUI Index.ets(四 Tab,手写嵌套滚动管线)
请求与响应:
请求 : {"op":"scroll","id":1,"dy":-12.5,"phase":"pre"}
{"op":"layout","id":1,"min":50,"max":150}
{"op":"fling","id":1,"velocity":-900,"phase":"pre"}
{"op":"place","mode":"road","progress":0.5,"cw":220,"ch":56,"tw":360,"th":175,
"min":50,"max":150,"collapsed":"CenterStart","expanded":"BottomEnd"}
响应 : {"ok":true,"height":137,"offset":0,"progress":0.785,"min":50,"max":150,"consumed":-12.5}
{"ok":false,"error":"..."}
九个 op:info(握手)/ create / dispose(实例生命周期)/ layout(测量回报)/ scroll(pre|post 分布)/ anim(补间帧直进高度)/ fling(settle 决策)/ place(子项坐标)/ measure(body 约束)。
两个设计约定:符号沿用 Compose(dy > 0 展开、手指下移为正——ArkUI PanGesture 的 offsetY / velocityY 恰好同向,桥上不用翻转);单位统一 vp(引擎不关心单位,进度与比例无量纲)。
4.2 为什么是每帧一调用
qrose 是"一次调用一图"、WheelPickerCompose 是"一次 snap 一调用",本库是连续手势库,桥粒度必须到帧:上游嵌套滚动本来就是逐 delta 分发的(onPreScroll 每帧带 available),ArkTS 拿到 consumed 后把余量喂给自己的 body 钳制,再决定是否调 post——与 Compose 的 pre → child → post 顺序一一对应。折叠是 60fps 交互,但桥载荷刻意最小(scroll 一个 op 七个字段,往返微秒级),"扁平、单函数、异常不跨界"的社区契约不变。
4.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI,非法参数(缺 dy、未知策略、未知对齐名)统一走 {"ok":false,"error":"…"} 通道,ArkTS 侧渲染到 ⚠ 错误行。
第 5 阶段:原生桥接
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libcollapsingtoolbar_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoscollapsingtoolbar.so)
│ CollapsingToolbarDispatch
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制折叠语义,所以选择 C ABI + JSON。
5.2 Kotlin/Native 导出函数
@CName("CollapsingToolbarDispatch")
fun collapsingToolbarDispatch(request: String): String // 九 op 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
CollapsingToolbarDispatch | collapsingToolbarDispatch(req) | 引擎注册表 + 九 op 全部分发 |
info op 承担版本握手(返回 fork / Kotlin 版本与 op 清单),真机版本行的数据来源。
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"collapsingToolbarDispatch", nullptr, NapiCollapsingToolbarDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscollapsingtoolbar_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscollapsingtoolbar_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与一个业务符号),不拖整份生成头。
5.4 CMake 和类型声明
add_library(ohoscollapsingtoolbar SHARED IMPORTED)
set_target_properties(ohoscollapsingtoolbar PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(collapsingtoolbar_napi SHARED napi_init.cpp)
target_link_libraries(collapsingtoolbar_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoscollapsingtoolbar)
OHOS_ARCH 为空或 libohoscollapsingtoolbar.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const collapsingToolbarDispatch: (request: string) => string;
第 6 阶段:ArkUI 宿主
6.1 手写嵌套滚动管线(pre → body → post)
private applyDelta(composeDy: number): void {
const pre = JSON.parse(dispatchRaw({ op: 'scroll', id: this.paneId, dy: composeDy, phase: 'pre' })) as StateRes;
const remaining: number = composeDy - (pre.consumed ?? 0); // 余量归 body
const clamped: number = Math.min(Math.max(this.bodyScroll - remaining, 0), this.bodyMaxScroll());
const bodyConsumed: number = -(clamped - this.bodyScroll); // body 实吃(钳制后)
this.bodyScroll = clamped;
let leftover: number = remaining - bodyConsumed;
if (Math.abs(leftover) > 0.01) { // post:EAC / EUC 吃展开余量
const post = JSON.parse(dispatchRaw({ op: 'scroll', id: this.paneId, dy: leftover, phase: 'post' })) as StateRes;
this.syncState(post);
}
this.syncState(pre);
this.applyProgressExtras(); // road / parallax 坐标 + 字号插值
}
为什么 body 不用原生 List + onScrollFrameBegin:ArkUI 列表在顶边 EdgeEffect.None 时不再派发帧回调,展开 toolbar 的 post 通道会丢事件;手写管线把 pre → body → post 的顺序完整握在手里,与上游嵌套滚动分发严格一致(WheelPickerCompose 手写滚轮的同款取舍)。
6.2 释放、fling 与补间
松手 velocityY 进 op:fling(pre),target 非空则跑 ~200ms ease-out 补间到 targetHeight,每帧经 op:anim(dispatchRawDelta)落高度——与上游 expand() 用 AnimationState.animateTo + scrollBy 的路径同构,钳制与亚像素累计全程参与;target 为空则 body 以 0.92/帧衰减 fling,每帧仍走 applyDelta(嵌套语义连续),衰减殆尽且 body 在顶部仍有余速时补一发 fling(post)(onPostFling 孪生)。Engine Tab 的 expand() / collapse() 按钮直接触发同一条补间路径。
6.3 四 Tab 与上游 Demo 的对应
| Tab | 上游出处 | 要点 |
|---|---|---|
| ExitUntilCollapsed | MainActivity | 150vp 背景 pin + 50vp 隐形 spacer 定最小高度 + road(CenterStart→BottomEnd) 标题(字号 18+12·progress) |
| EnterAlways | 同款 toolbar 换策略 | toolbar 收到最小后被 offset 推出屏幕,下拉先回 toolbar |
| Parallax | ParallaxActivity | 50vp spacer + 300vp 头图 parallax(0.5) 且 alpha = progress,EnterAlwaysCollapsed,右下 Floating Button!,顶部 Enable collapse/expand 开关(enabled 参数,关掉后滚动直通 body) |
| Engine | expand() / collapse() API | 补间按钮 + 实时读数 + info 握手 |
每个 Tab 底部都有读数行(h=… min=… max=… offsetY=… progress=… bodyScroll=…),滚动时实时跳动——"数字来自内核"的直接验收面。
6.4 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoscollapsingtoolbar)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName + MiniJson + 引擎注册表(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.collapsingtoolbar、versionName 2.3.5-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/libohoscollapsingtoolbar_api.h
├── src/main/cpp/types/libcollapsingtoolbar_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.5 构建、签名和安装
.\gradlew.bat :lib:jvmTest # 17 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example\harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 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.collapsingtoolbar -m entry
四、完整代码对照
4.1 整体架构
me.onebone.toolbar/**(折叠内核,commonMain 新增三文件)
│ CollapsingToolbarEngine / ToolbarPlacement / ScaffoldMeasure
Bridge.kt + MiniJson
│ CollapsingToolbarDispatch(九 op)
libcollapsingtoolbar_napi.so
│ collapsingToolbarDispatch
Index.ets
│ 手写嵌套滚动管线 + JSON parse + @State 更新
▼
ArkUI 真机页面(四 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_collapsing_toolbar、app/ 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 钉版 |
build.gradle.kts(根) | 版本 2.3.5-ohos.1、全局禁用 Ohos*Test* 任务 |
lib/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
lib/src/commonMain/.../CollapsingToolbarEngine.kt | 状态钳制 + dispatchRawDelta + 三策略分布 + fling 决策 |
lib/src/commonMain/.../ToolbarPlacement.kt | 偏置对齐模型 + pin / road / parallax 公式 |
lib/src/commonMain/.../ScaffoldMeasure.kt | body 约束 + body / toolbar 摆放点 |
lib/src/jvmTest/.../CollapsingToolbarEngineTest.kt | 17 项交叉验证(含上游 androidTest 场景) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、九 op 分发、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 四 Tab 验收页面(嵌套滚动管线) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(18 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | layout(min, max) | 测量回报,含钳制副作用与先后顺序(上游 measure policy) |
| 引擎 | preScroll(dy) / postScroll(dy) | 三策略的滚动分布(上游三个 NestedScrollConnection) |
| 引擎 | dispatchRawDelta(v) | 高度直进 + 亚像素累计(上游 ScrollableState lambda) |
| 引擎 | fling(v, phase) | settle 决策 + body 剩余速度(上游 onPre/PostFling 净语义) |
| 引擎 | progress | (height−min)/(max−min),联动 UI 插值源 |
| 摆放 | ToolbarPlacement.place(mode, …) | pin / road / parallax 坐标(上游 placement pass) |
| 测量 | ScaffoldMeasure.bodyMaxHeight(…) | body 约束(ExitUntilCollapsed 扣 toolbarMin) |
| Native | CollapsingToolbarDispatch | C ABI JSON 导出 |
| N-API | collapsingToolbarDispatch(req) | 向 ArkTS 暴露能力 |
| ArkTS | applyDelta(dy) | pre → body → post 管线并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const pre = JSON.parse(collapsingToolbarDispatch(JSON.stringify({
op: 'scroll', id: 1, dy: -12, phase: 'pre',
}))) as StateRes;
toolbarStack.height(pre.height); // 折叠中的高度
bodyStack.position({ y: pre.height + pre.offset }); // body 摆放点
titleText.fontSize(18 + 12 * pre.progress); // progress 联动
fun preScroll(dy: Float): Float = when (strategy) {
StrategyKind.ENTER_ALWAYS -> enterAlwaysPreScroll(dy) // toolbar→offset→body
StrategyKind.ENTER_ALWAYS_COLLAPSED -> enterAlwaysCollapsedPreScroll(dy)
StrategyKind.EXIT_UNTIL_COLLAPSED -> exitUntilCollapsedPreScroll(dy)
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:内核提取而不是真编译整库
KStore 走真编译整库的前提是 eazytec 上有它依赖的完整 kotlinx ohos 制品线;本库的依赖表里只有 androidx.compose,这个前提永远不成立。但"没有制品"不等于"没有价值"——上游值钱的部分(钳制顺序、亚像素累计、三策略分布、锚点插值)是纯数学,与渲染无关,提取成本约 500 行零依赖 Kotlin。与 qrose / WheelPickerCompose / Flippable / swipe 构成社区内核路线的连续先例。
决策 2:工具链钉 2.2.21 旧线,Gradle 降 8.14.1
内核零依赖,不存在"由依赖制品线反推 Kotlin 版本"的约束,工具链直接对齐社区已过审的内核路线先例(qrose / WheelPickerCompose 全部是 2.2.21-1.0.0 + Gradle 8.14.1)。KStore 的 2.4.20 线(Gradle 9.x)是为 kotlinx 新制品线服务的,在这里没有收益——两条工具链线的经验不能互相套用。
决策 3:fling 用净决策而不是样条移植
上游样条衰减是 Android FlingBehavior 的平台实现细节。内核回答"谁接管、走向哪个 bound、body 剩多少"即可语义等价:ArkTS 拿到 target 后跑补间,每帧仍经 dispatchRawDelta 落高度,钳制与累计器全程参与——settle 的路径与上游 expand() 完全同构,只是时钟从 Android 动画框架换成了 ArkTS setInterval。
决策 4:每帧 JSON 桥 + 读数行
连续手势库的桥粒度必须到帧,但契约纪律(扁平、单函数、异常不跨界、DisposeString 归还)与 qrose 完全一致;每个 Tab 底部的读数行让 h / min / max / offsetY / progress 在真机上肉眼可对拍第 3.2 节的分布表,验收不依赖 hilog。
决策 5:签名材料不进仓
build-profile.json5 的 signingConfigs 留空数组(条目缺 material 字段会被 hvigor schema 拒绝),DevEco 勾自动签名回填本机;上游 Android 演示 app/ 与 Compose 隔离区留盘不进构建,作为对照与未来回接的底稿。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :lib:jvmTest
17 项用例全部通过(分域统计见 3.4 节表格)。与 KStore"上游原测试文件零改动"不同,本库上游只有 Compose 仪器测试(进不了 JVM),因此交叉验证是新写的——但其中最关键的一组直接移植自上游 androidTest 的 testCollapse 场景(300/50 双子项 → 折叠到 50),断言原文照抄(|progress| < 0.01)。
6.3 HAP 验证
:lib:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: CollapsingToolbarDispatch / libohoscollapsingtoolbar_symbols 均为全局 T 符号
BuildNativeWithNinja: finished(双 ABI 编出 libcollapsingtoolbar_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 6.8 MB,内含双 ABI 各三件套(libcollapsingtoolbar_napi.so / libohoscollapsingtoolbar.so / libc++_shared.so);HAP 内 strip 后 KN so arm64 约 1.9MB、x86_64 约 1.9MB(debugShared 未 strip 时分别约 3.69MB / 3.40MB,含 Kotlin/Native 运行时——本库没有 kotlinx 依赖,比 KStore 的 so 显著更小)。
6.4 功能用例
- 启动后标题下方版本行出现
fork 2.3.5-ohos.1 · Kotlin 2.2.21-1.0.0 · ops: info/create/…(来自真实 Native 调用,非静态文字); - ExitUntilCollapsed Tab 上滑:toolbar 从 150 收到 50 并钉住,road 标题从 CenterStart 走向 BottomStart 侧锚点、字号从 30 收到 18,读数行 progress 从 1.00 走到 0.00;
- EnterAlways Tab 继续上滑:toolbar 被 offset 推出屏幕(读数 offsetY 走负),body 顶到状态栏下;下拉时 offset 先归零、toolbar 再展开;
- Parallax Tab:头图以 0.5 系数视差且透明度随 progress 渐隐;关掉
Enable collapse/expand后滚动直通列表、toolbar 冻结(enabled参数语义); - 松手 fling:按策略与方向回弹到端点(ExitUntilCollapsed 上滑松手直接收到底),回弹是补间而非瞬移;
- Engine Tab 点
expand()/collapse():~200ms 缓动补间,读数行连续走字;Reset归位 body 与 toolbar; - 每帧滚动读数行实时刷新,
h/min/max与声明的子项高度一致(50/150、50/300); - 桥错误通道:理论不该出现,出现时 ⚠ 行显示原文、页面不崩溃。
6.5 验证结论
折叠内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四 Tab 手势与读数构成完整验收面(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-collapsing-toolbar.git
cd compose-collapsing-toolbar
export JAVA_HOME="/path/to/jdk-21"
./gradlew :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64 # klib
./gradlew :lib:jvmTest # 17 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.collapsingtoolbar -m entry
hdc shell hilog | grep CollapsingToolbar
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP/CMP 适配,必须让折叠状态机真正编译成
ohosArm64/ohosX64动态库,并由页面逐帧调用; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 制品线检查的结论可以是"没有":
androidx.compose不存在 ohos 制品是路线判定的输入而不是障碍——零依赖内核路线照样成立,且 commonMain 连依赖坐标都不用改写; - road 锚点按当前高度解析:初版按展开态容器算锚点,JVM 交叉验证当场打脸;上游 271-281 行的对齐空间是
IntSize(maxWidth, height),锚点随容器收缩移动——修测试期望,内核本来就对; - ArkUI
PanGesture的offsetY/velocityY与 Compose 的 dy / velocity 同向(手指下移为正 = 展开),桥上不用做符号翻转;但 body 滚动方向相反(dy<0 时 bodyScroll 增长),余量换算时别混; - ArkUI 原生列表顶边
EdgeEffect.None时不再派发帧回调,嵌套滚动的 post 通道会丢事件——body 必须手写钳制管线,不能依赖onScrollFrameBegin; hvigorw assembleHap的PackageHap需要 java:spawn java ENOENT时把JAVA_HOME指到 Temurin 21 再跑;- 根构建的
/build/与.idea/*是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的example/harmonyApp/.idea/与example/nativeApp/build/要单独追加 gitignore,否则混进提交; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝(报00303038 must have required property 'material')——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,提交前用git update-index --skip-worktree保护,证书绝不进入 AtomGit。
8.2 已知问题
- road 子项尺寸由声明常量给出(
TITLE_W × TITLE_H的定尺寸容器),不是动态文本测量——上游测量真实 child,ArkTS 侧视觉等价; - RTL /
layoutDirection未移植(上游placeRelative在 LTR 下与place等价,Demo 只跑 LTR),偏置模型已保留,接 RTL 时在 ArkTS 侧翻 x 即可; - deprecated 的
AppBarContainer/ToolbarWithFabScaffold/FabPlacement/FabPosition未入内核(上游迁移兼容层,行为与主 API 重复),隔离在composeMain; rememberSaveable的状态恢复语义不适用:HAP 侧无跨进程恢复场景,引擎随组件生命周期create/dispose;- body 列表非虚拟化(Demo 60 行 Text),业务接入应换
List或复用手写管线; - EnterAlways 折叠钳制用当前高度(
-height - offsetY)是上游行为不是 bug:toolbar 收缩后 offset 上限跟着变小; - HAP 安装必须使用与 bundle name
org.terminator.ohos.collapsingtoolbar匹配的签名。
8.3 未来优化方向
- 补真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- EnterAlwaysCollapsed 的
onPostFling触发细节增加专项真机用例(当前依赖帧管线 +fling(post)双通道); - 社区 CMP 鸿蒙线与上游 Compose 版本对齐后,把
composeMain重新挂回编译、内核退化为共享状态层。
九、总结
9.1 核心难点回顾
上游 compose-collapsing-toolbar(100% androidx.compose)
→ 制品线检查(结论:没有)→ 判定零依赖内核路线
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1 降版)
→ 根构建替换(cmp_qrose 模式)+ Compose 源码隔离 composeMain
→ 折叠状态机逐条提取(钳制顺序 / 亚像素累计 / 三策略分布 / 锚点插值)
→ ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
→ C ABI(@CName × 1,九 op)+ 每帧扁平 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS 手写嵌套滚动管线(pre → body → post)
→ ArkUI 真机四 Tab 验收(三策略 / parallax / road / 补间 / 读数)
9.2 封装层次
- 内核层:
CollapsingToolbarEngine(钳制、进度、累计器、分布、fling 决策)、ToolbarPlacement(对齐模型与三摆放公式)、ScaffoldMeasure(body 约束与摆放点); - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:手写嵌套滚动管线、手势与 fling 时钟、补间驱动、页面状态与读数;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 制品线检查的结论可以是"没有",这同样是路线判定的输入:纯 Compose UI 库没有 kotlinx 依赖可换线,正确答案是提取 UI 下面的数学——零依赖内核的
commonMain连一条依赖坐标都不用改,适配面反而最小;社区内核路线(qrose / WheelPickerCompose / Flippable / swipe)因此构成与 KStore 真编译路线互补的另一半地图; - 交互状态机的移植必须钉"顺序"而不只是"公式":钳制的先后(min 先 max 后)、分布的先后(pre 里 toolbar→offset)、取整的先后(parallax 取负之内、锚点按当前高度解析)——JVM 交叉验证连移植者自己的误解都能拦下来(本轮就拦了 road 锚点一处);
- 连续手势库的桥粒度到帧,契约纪律不变:一帧一个
scrollop、七个字段、往返微秒级,"扁平 JSON、单函数、异常不跨界、DisposeString 归还"与一图一回的 qrose 完全一致——变的是粒度,不变的是边界。
9.4 适配成果
当前适配已完成:
compose-collapsing-toolbar2.3.5 折叠内核的双 ABI Kotlin/Native 编译(上游 Compose UI 零删改隔离进composeMain,内核三文件每条规则标注上游出处);- 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 四 Tab 验收页面(三策略、parallax / road / pin、enabled 开关、expand / collapse 补间、实时引擎读数);
- 17 项 JVM 交叉验证全绿(含上游 androidTest 场景移植)+ HAP 构建(双 ABI 六件套 so,约 6.8MB)、签名、设备安装和真机验证;
- 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)