开源鸿蒙平台 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 两个官方演示,骨架数字全部来自内核。

二、实现路线图

text 复制代码
第 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 工程边界
text 复制代码
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
powershell 复制代码
$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 页面边界
text 复制代码
标题区          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 仓库写在最前:

kotlin 复制代码
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 模块:

kotlin 复制代码
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 约定路径)不会被动进编译,但显式挪进隔离区让"哪部分在构建里"一目了然。实测:

powershell 复制代码
.\gradlew.bat :lib:compileKotlinJvm :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64   # BUILD SUCCESSFUL
2.4 测试任务的处置

根构建全局禁用 ohos 测试任务(交叉验证统一在 JVM 跑):

kotlin 复制代码
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):

kotlin 复制代码
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 摆放公式:锚点按当前高度解析
kotlin 复制代码
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 契约
text 复制代码
me.onebone.toolbar/**(内核:引擎 + 摆放 + 测量,commonMain 编译)
        ↓ 折叠状态机:钳制 / 分布 / 插值
Bridge.kt(@CName × 1,扁平 JSON 进出,id 键引擎注册表)
        ↓ UTF-8 JSON
N-API collapsingToolbarDispatch()
        ↓
ArkUI Index.ets(四 Tab,手写嵌套滚动管线)

请求与响应:

text 复制代码
请求 : {"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 三层桥接
text 复制代码
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 导出函数
kotlin 复制代码
@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 方法分发
cpp 复制代码
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 和类型声明
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 时互相可见。

typescript 复制代码
export const collapsingToolbarDispatch: (request: string) => string;

第 6 阶段:ArkUI 宿主

6.1 手写嵌套滚动管线(pre → body → post)
typescript 复制代码
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 工程结构
text 复制代码
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 构建、签名和安装
powershell 复制代码
.\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 的完整配置)。

bash 复制代码
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 整体架构

text 复制代码
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 的边界

typescript 复制代码
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 联动
kotlin 复制代码
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 交叉验证

powershell 复制代码
.\gradlew.bat :lib:jvmTest

17 项用例全部通过(分域统计见 3.4 节表格)。与 KStore"上游原测试文件零改动"不同,本库上游只有 Compose 仪器测试(进不了 JVM),因此交叉验证是新写的------但其中最关键的一组直接移植自上游 androidTest 的 testCollapse 场景(300/50 双子项 → 折叠到 50),断言原文照抄(|progress| < 0.01)。

6.3 HAP 验证

text 复制代码
: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 功能用例

  1. 启动后标题下方版本行出现 fork 2.3.5-ohos.1 · Kotlin 2.2.21-1.0.0 · ops: info/create/...(来自真实 Native 调用,非静态文字);
  2. ExitUntilCollapsed Tab 上滑:toolbar 从 150 收到 50 并钉住,road 标题从 CenterStart 走向 BottomStart 侧锚点、字号从 30 收到 18,读数行 progress 从 1.00 走到 0.00;
  3. EnterAlways Tab 继续上滑:toolbar 被 offset 推出屏幕(读数 offsetY 走负),body 顶到状态栏下;下拉时 offset 先归零、toolbar 再展开;
  4. Parallax Tab :头图以 0.5 系数视差且透明度随 progress 渐隐;关掉 Enable collapse/expand 后滚动直通列表、toolbar 冻结(enabled 参数语义);
  5. 松手 fling:按策略与方向回弹到端点(ExitUntilCollapsed 上滑松手直接收到底),回弹是补间而非瞬移;
  6. Engine Tab 点 expand() / collapse():~200ms 缓动补间,读数行连续走字;Reset 归位 body 与 toolbar;
  7. 每帧滚动读数行实时刷新,h/min/max 与声明的子项高度一致(50/150、50/300);
  8. 桥错误通道:理论不该出现,出现时 ⚠ 行显示原文、页面不崩溃。

6.5 验证结论

折叠内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四 Tab 手势与读数构成完整验收面(见下节效果图)。

七、运行效果

7.1 真机截图

7.2 命令速查

bash 复制代码
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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP/CMP 适配,必须让折叠状态机真正编译成 ohosArm64 / ohosX64 动态库,并由页面逐帧调用;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 制品线检查的结论可以是"没有":androidx.compose 不存在 ohos 制品是路线判定的输入而不是障碍------零依赖内核路线照样成立,且 commonMain 连依赖坐标都不用改写;
  4. road 锚点按当前高度解析 :初版按展开态容器算锚点,JVM 交叉验证当场打脸;上游 271-281 行的对齐空间是 IntSize(maxWidth, height),锚点随容器收缩移动------修测试期望,内核本来就对;
  5. ArkUI PanGesture 的 offsetY / velocityY 与 Compose 的 dy / velocity 同向(手指下移为正 = 展开),桥上不用做符号翻转;但 body 滚动方向相反(dy<0 时 bodyScroll 增长),余量换算时别混;
  6. ArkUI 原生列表顶边 EdgeEffect.None 时不再派发帧回调,嵌套滚动的 post 通道会丢事件------body 必须手写钳制管线,不能依赖 onScrollFrameBegin;
  7. hvigorw assembleHap 的 PackageHap 需要 java:spawn java ENOENT 时把 JAVA_HOME 指到 Temurin 21 再跑;
  8. 根构建的 /build/ 与 .idea/* 是根锚定模式,盖不住嵌套目录------DevEco 打开工程后生成的 example/harmonyApp/.idea/ 与 example/nativeApp/build/ 要单独追加 gitignore,否则混进提交;
  9. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(报 00303038 must have required property 'material')------仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 签名材料由 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 核心难点回顾

text 复制代码
上游 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 三条经验

  1. 制品线检查的结论可以是"没有",这同样是路线判定的输入:纯 Compose UI 库没有 kotlinx 依赖可换线,正确答案是提取 UI 下面的数学------零依赖内核的 commonMain 连一条依赖坐标都不用改,适配面反而最小;社区内核路线(qrose / WheelPickerCompose / Flippable / swipe)因此构成与 KStore 真编译路线互补的另一半地图;
  2. 交互状态机的移植必须钉"顺序"而不只是"公式":钳制的先后(min 先 max 后)、分布的先后(pre 里 toolbar→offset)、取整的先后(parallax 取负之内、锚点按当前高度解析)------JVM 交叉验证连移植者自己的误解都能拦下来(本轮就拦了 road 锚点一处);
  3. 连续手势库的桥粒度到帧,契约纪律不变:一帧一个 scroll op、七个字段、往返微秒级,"扁平 JSON、单函数、异常不跨界、DisposeString 归还"与一图一回的 qrose 完全一致------变的是粒度,不变的是边界。

9.4 适配成果

当前适配已完成:

  • compose-collapsing-toolbar 2.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。

参考文档

相关推荐
xnqys1 小时前
一款MIT协议开源的项目工程管理系统,内置投标管理、项目进度、成本控制等核心功能
开源
狂师1 小时前
GitHub上开源真机测试工具mobile-mcp,说句话就能让AI在手机上跑用例!
人工智能·开源·测试
sbjdhjd2 小时前
从污点追踪断点到 PHP-FPM 请求队列:HIDS PHP WebShell 样本复盘
开发语言·网络安全·数据挖掘·渗透测试·开源·php·shell
路由侠内网穿透.2 小时前
本地部署开源 WSGI HTTP 服务器 Gunicorn 并实现外部访问
运维·服务器·网络·网络协议·http·开源
2401_882351522 小时前
开源鸿蒙平台 CMP 三方库「SegmentedControl」适配全流程
华为·开源·harmonyos
miofly2 小时前
GitHub 日榜趋势速报 | 2026-10-30
开源·github
2401_882351523 小时前
开源鸿蒙平台 CMP 三方库「LlmTypewriter」适配全流程
华为·开源·harmonyos
wflynn11 小时前
GitHub 日榜趋势速报 | 2026-10-28
开源·github
孙琦Ray12 小时前
Meta 把 AI 网络 RDMA 传输层开源了
meta·开源·rdma·ai 网络·ocp 全球峰会·数据中心交换机