开源鸿蒙平台 CMP 三方库「Dropdown」适配全流程
本文记录
Dropdown接入 OpenHarmony 的完整过程,覆盖动画库盘点、Compose 依赖边界拆分、纯 Kotlin 帧引擎构建、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 四 Tab 页面、HAP 构建和设备验收。
Dropdown是基于 Compose Animation 的 Compose Multiplatform 级联下拉菜单库:DropdownComposable 包住 Material3 的DropdownMenu,提供 12 种入场动画、12 种退场动画、4 条缓动曲线与tween时间线(AnimationProp的 duration / delay / easing),子菜单推进/返回由AnimatedContent的togetherWith过渡承载。本次适配与存储类库的「真编译整库」路线(KStore,得益于 kotlinx ohos 制品线补齐)有本质区别:Dropdown 的全部价值在 Compose UI,而社区 CMP 工具链是 1.9.2 / HarmonyOS Kotlin2.2.21-1.0.0,上游是 Compose 1.10 / Kotlin 2.1.21,Composable 无法编进 ohos。因此走的是社区早期已过审的 qrose / Vico / Reorderable 路线:动画模型(枚举、时间线、缓动曲线、逐帧变换数学)留在commonMain原样编译成 OpenHarmony KLIB,Compose 部分隔离到composeMain不参与 ohos 构建,ArkTS 通过 C ABI 每帧取回 alpha / scale / offset 渲染 ArkUI 面板——动画语义 100% 由上游 Kotlin 决定,ArkTS 一行动画数学都不写。
项目地址: AtomGit/oh-tpc/Dropdown(harmony-adaptation 分支)
上游仓库: AndroidPoet/Dropdown
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Dropdown 为 Compose 应用提供「带级联与花式动画的下拉菜单」这一件事的完整答案:dropDownMenu<String> { item(...) { ... } } 一套 DSL 建树,Dropdown(isOpen, menu, enter, exit, easing, enterDuration, exitDuration) 一行挂到锚点上。入场可选 ElevationScale、SlideInHorizontally、ExpandIn 等 12 种,退场对称 12 种,缓动 4 条,时长毫秒级可调;点子菜单前进用所选 enter/exit 对过渡,点头部返回强制 FadeIn/FadeOut。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套菜单动效,但上游构建(Kotlin 2.1.21 / Compose 1.10.3 / AGP 8.13.2 / Gradle 8.14.5)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS 的 animateTo,页面可以弹出菜单,却无法证明上游动画语义在 OpenHarmony ARM64 设备上完成编译、链接和执行——ArkUI 自带的 springs/curves 与 Compose tween + CubicBezierEasing 不可换算,验收时「同一个 ElevationScale 在 Android 和鸿蒙长得不一样」无法解释。适配需要同时验证动画模型、缓动曲线数学、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Compose 1.10.3 / Kotlin 2.1.21 / AGP 8.13.2 与鸿蒙工具链无法共存于一个 Gradle 构建;Composable 依赖 androidx.compose.material3、AnimatedContent、ImageVector。 |
| 依赖矩阵 | 库本体 7 个 Kotlin 文件全部 import Compose;MenuColors 绑 Color、菜单 DSL 绑 ImageVector,连"纯模型"文件都不干净。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 动画语义跨不了桥 | EnterTransition / ExitTransition 是 Compose 类型,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:动画模型的 ohos 编译(枚举下沉 + 帧引擎)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。每帧的 alpha / scale / offset 完全由上游语义的 Kotlin 实现计算,ArkTS 只负责时间线驱动、面板渲染和结果展示。
1.2 库提供的能力
Demo 提供一个真实的菜单动效会话,对应桥接的全部入口:
- 目录握手(catalog):启动时
op=catalog从 Kotlin 拉取 12 入场 + 12 退场 + 4 缓动枚举、曲线控制点与上游默认值——页面选项卡是运行时生成的,不在 ArkTS 手写; - 入场播放(frame/enter):任选入场动画 × 缓动 × 时长,面板逐帧套用桥返回的 alpha / scale / offset,如
SlideInHorizontally从-width/2滑入、ScaleIn从 0 等比长出; - 退场播放(frame/exit):12 种退场对称验证,
SlideOut结束在(-width/2, height/2),Shrink 系只缩不淡; - 缓动采样(easing):Canvas 曲线 41 个采样点全部来自桥,附五点采样表与缓动小球演示;
- 级联导航:子菜单前进 = 所选 enter + exit 对(对应上游
togetherWith),返回 = 上游强制的 FadeIn/FadeOut。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / kind | 回显本次操作与方向(enter / exit)。 |
eased | 缓动后的进度,供状态行与断言。 |
alpha / scaleX / scaleY | 当帧透明度与等比缩放(上游 TransformOrigin.Center)。 |
offsetX / offsetY | 当帧平移,单位与调用方传参一致(vp)。 |
引擎侧完整能力(4 条三次贝塞尔缓动、tween 插值链、12+12 种动画到变换的映射、上游默认值)全部在 commonMain 里进入 .so,JVM 侧保留 12 项 jvmTest 交叉验证用例——其中一条直接把真实的 androidx.compose.animation.core(上游实际编译依赖的 1.10.3)与 fork 的曲线实现逐点比对,证明 ohos .so 里执行的动画数学没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 动画枚举、AnimationProp 原样保留;Compose 工厂与 Composable 原样隔离到 composeMain(零删除);新增 DropdownMotion 帧引擎,常量全部溯源上游 Compose 源码。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);app / baselineprofile / baselineprofile-app 留盘不进构建。 |
| 桥接稳定 | 单个 C ABI(DropdownMotion)+ UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 工具链换到 HarmonyOS Kotlin 2.2.21-1.0.0(社区已审钉法),Gradle 钉 8.14.1;上游 Compose 依赖仅在 jvmTest 测试范围出现,不进鸿蒙产物。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:入场、退场、缓动曲线、级联菜单,枚举目录运行时拉取。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
DropdownComposable 留在composeMain等社区compose.uiohos 制品与 CMP 1.10 对齐后接回;当前 ArkTS Demo 只是把桥暴露的动画闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 7 个源文件的 Compose 依赖边界、确定「枚举下沉 / 工厂隔离」拆分策略、交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前 + 仓库内容过滤、Kotlin 2.2.21-1.0.0 钉版
第 3 阶段:拆分与帧引擎 ── 枚举留 commonMain、工厂进 composeMain、DropdownMotion 帧求值(常量溯源 animation 1.10.3)
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、MiniJson 和错误通道
第 5 阶段:ArkUI 宿主 ── catalog 握手、四 Tab、16ms 帧循环、级联双面板过渡
第 6 阶段:示例与验证 ── JVM 交叉验证(真实 Compose 比对)、双 ABI so、HAP、签名、设备运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
dropdown/src/commonMain/ 动画引擎:枚举 + AnimationProp + MotionFrame + DropdownMotion(进 ohos 构建)
dropdown/src/composeMain/ 上游 Composable、Transition 工厂、MenuColors、菜单 DSL(ohos 不编,零删除)
dropdown/src/jvmTest/ 与真实 Compose 缓动逐点比对 + 帧数学断言
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
app/ baselineprofile/ buildSrc/ ... 上游 Android 工程遗留,留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :dropdown 与 :example:nativeApp,rootProject.name = "ohos_dropdown"。上游的 spotless / nexus / dokka / binary-compatibility 发布插件、AGP classpath、baseline profile 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,社区已审钉法) |
| JDK | 21(Temurin 21.0.12.1,org.gradle.java.home 写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 钉版;2.2.21 旧线经验,同 qrose) |
| 交叉验证依赖 | org.jetbrains.compose.animation:animation-core:1.10.3(仅 jvmTest 测试范围) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.dropdown |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
上游 dropdown 模块 commonMain 只有 7 个 Kotlin 文件,但全部 import Compose,拆分必须逐文件判断:
| 文件 | Compose 依赖形态 | 处置 |
|---|---|---|
AnimationProp.kt | 零依赖(仅引用同包 Easing 枚举) | 原样留在 commonMain |
Easing.kt | 枚举纯净;getEasing 返回 Compose Easing 实例 | 枚举留下,工厂挪 composeMain/EasingCompose.kt |
EnterAnimation.kt / ExitAnimation.kt | 枚举纯净;getEnterAnimation 等工厂构造 EnterTransition | 枚举留下,工厂挪 composeMain/*Compose.kt |
DropDown.kt | @Composable 本体,AnimatedContent + Material3 | 整文件挪 composeMain |
MenuColors.kt | Color / MaterialTheme | 整文件挪 composeMain |
DropDownMenuBuilder.kt | 菜单 DSL 绑 ImageVector | 整文件挪 composeMain |
desktopMain/Main.kt | compose.desktop 演示入口 | 挪 composeMain |
这和 qrose 挪 Code128Type(枚举写在 Painter 文件里、编码器却要引用)是同一类问题:类型定义跟 UI 文件绑在一起,拆的时候要手洗。composeMain 不是 Kotlin 源集,不会被任何目标编译——留在树里是为了对照上游 API,等 CMP 工具链对齐后整体搬回 commonMain。
1.4 页面边界
Tab 栏 入场动画 / 退场动画 / 缓动曲线 / 级联菜单
选项区 入场(或退场)芯片行、缓动芯片行、时长滑条(100–1500ms)
舞台区 锚点按钮 + 192vp 菜单面板(opacity/scale/translate 逐帧驱动)
状态行 最近一帧的 t / eased / α / s / dx / dy
缓动 Tab Canvas 曲线(41 采样点)+ 五点采样表 + 缓动小球
级联 Tab 文件/编辑/帮助三级树,前进双面板同屏过渡,返回 FadeIn/FadeOut
页面启动时先调 op=catalog 做握手,状态行出现「目录加载成功:入场×12 退场×12 缓动×4」——证明枚举目录来自 Kotlin 库而非 ArkTS 手写。
第 2 阶段:目标与依赖打通
2.1 根构建替换(cmp_qrose 模式)
settings.gradle.kts 把 eazytec 仓库写在最前,并做仓库内容过滤——这是本适配踩出来的关键一步:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/") {
// androidx.* metadata is proxied here but jars 404; route them to google().
content { excludeGroupByRegex("androidx\\..*") }
}
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos") {
content { excludeGroupByRegex("androidx\\..*") }
}
google {
content { includeGroupByRegex("androidx\\..*") }
}
mavenCentral()
}
}
rootProject.name = "ohos_dropdown"
include(":dropdown")
include(":example:nativeApp")
不加过滤会发生什么:jvmTest 引入 animation-core 1.10.3 后,其 JVM 变体传递依赖 androidx.compose.runtime:runtime-desktop:1.10.5;eazytec 代理返回了该模块的 .module 元数据但 jar 404,而 Gradle 认下元数据后就不再回退其它仓库,解析直接失败,报错只显示在 eazytec 里找过。内容过滤按 group 路由——androidx.* 只走 google()(那里 jar 齐全),HarmonyOS Kotlin(org.jetbrains.kotlin fork 组)照旧走 eazytec,互不干扰。
gradle.properties 钉 JDK 与版本:
org.gradle.java.home=C:\\Users\\<you>\\.jdks\\jdk-21
version=1.1.6-ohos.1
kotlin.native.ignoreDisabledTargets=true
根构建只剩版本声明与一条测试纪律:
plugins { alias(libs.plugins.kotlin.multiplatform) apply false }
allprojects {
group = "io.github.androidpoet"
version = providers.gradleProperty("version").orElse("1.1.6-ohos.1").get()
}
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
2.2 库模块收敛为纯 KMP
dropdown/build.gradle.kts 重写:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
val jvmTest by getting {
dependencies {
implementation(kotlin("test"))
// 上游交叉验证专用:真实 Compose 缓动与本 fork 曲线逐点比对。
// 仅测试范围,ohos 目标与 klib 完全不可见。
implementation("org.jetbrains.compose.animation:animation-core:1.10.3")
}
}
}
}
实测:
./gradlew :dropdown:compileKotlinJvm # BUILD SUCCESSFUL
Composable 不在 commonMain 里,ohos 编译器看不到 androidx.compose——这是拆分成立的直接证据。
2.3 wrapper 与 Gradle 版本
上游 wrapper 是 8.14.5,按社区 2.2.21 线的已审经验钉回 8.14.1(distributionUrl=gradle-8.14.1-bin.zip)。另有一个 Windows 环境细节:org.gradle.java.home 只管 Gradle 守护进程,不管 wrapper 启动 JVM——跑 gradlew.bat 前必须先 export JAVA_HOME(或 PowerShell $env:JAVA_HOME),否则报 JAVA_HOME is not set。
第 3 阶段:拆分与帧引擎(技术难点)
3.1 拆分四刀
第 1 刀 Easing.kt 枚举留 commonMain,getEasing → composeMain/EasingCompose.kt
第 2 刀 Enter/ExitAnimation 枚举留 commonMain,四个工厂 → composeMain/*Compose.kt
第 3 刀 DropDown/MenuColors/Builder/Main git mv 整文件进 composeMain(保留历史)
第 4 刀 新增 commonMain/MotionFrame.kt + DropdownMotion.kt(Compose-free 帧引擎)
工厂文件顶部加注释声明同步义务:composeMain/EnterAnimationCompose.kt 改映射时,DropdownMotion.enterFrame 必须同步——两份实现靠 jvmTest 约束一致。
3.2 常量溯源:不凭记忆写 Compose 默认值
帧引擎的唯一事实来源,是上游实际依赖的 animation / animation-core 1.10.3 源码(适配时下载 sources jar 逐条核对):
fadeIn(spec) alpha 0f → 1f (initialAlpha 默认 0f)
fadeOut(spec) alpha 1f → 0f (targetAlpha 默认 0f)
scaleIn(spec) scale 0f → 1f (initialScale 默认 0f!)
scaleOut(spec) scale 1f → 0f (targetScale 默认 0f)
slideInHorizontally(spec) x: -fullWidth/2 → 0 (initialOffsetX 默认 { -it / 2 })
slideInVertically(spec) y: -fullHeight/2 → 0
slideOutHorizontally(spec) x: 0 → -fullWidth/2
slideOutVertically(spec) y: 0 → -fullHeight/2
scaleIn/scaleOut 默认 TransformOrigin.Center → 等比,中心缩放
FastOutSlowInEasing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
LinearOutSlowInEasing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
FastOutLinearInEasing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
LinearEasing = 恒等
两个「想当然必错」的点,核对前连本仓最初写的测试预期都是错的:
scaleIn默认从 0 起步,不是 0.8 / 0.9(那是 material SharedAxis 的自定义值)。上游 Dropdown 没传 initialScale,鸿蒙侧 ScaleIn / Expand 系就是从 0 长出来;LinearOutSlowInEasing是 (0, 0, 0.2, 1),网上流传的 (0, 0, 0.05, 1) 是 CSS 惯用值,不是 Compose 的。
连带一个反直觉事实:FastOutSlowInEasing 并不关于对角线对称——它是前载曲线,x=0.5 处 y≈0.776(x=0.5 处 FastOutLinearIn≈0.325、LinearOutSlowIn≈0.839),这条数值后来直接进了 Demo 缓动 Tab 的验收口径。
3.3 帧引擎实现
tween 的求值链条(与 Compose 运行时语义一致):
linearProgress = (now - start) / duration // ArkTS 时间线职责
eased = easing.transform(linearProgress) // 三次贝塞尔 y(x),端点恒等
value = start + (end - start) * eased // 库职责,逐属性
DropdownMotion 对外暴露三个纯函数:
public object DropdownMotion {
public fun sampleEasing(easing: Easing, progress: Float): Float
public fun enterFrame(animation: EnterAnimation, easing: Easing,
linearProgress: Float, width: Int, height: Int): MotionFrame
public fun exitFrame(animation: ExitAnimation, easing: Easing,
linearProgress: Float, width: Int, height: Int): MotionFrame
}
enterFrame 按上游 getEnterAnimation 的映射逐动画实现:fade 族只动 alpha;Z 轴 / ElevationScale 是 fadeIn + scaleIn 叠加;slide 族按 Compose 默认距离;scale / expand / shrink 族只动等比 scale、不带 alpha。上游把 ExpandHorizontally 映射成普通 scaleIn、ShrinkVertically 映射成普通 scaleOut——这些「名字方向性、实际等比」的上游行为原样保留,不在鸿蒙侧「顺手修正」。
贝塞尔求解用 Newton–Raphson(8 步)+ 二分(24 步)兜底,与 Compose 的代数求根数值等价——这一点由 jvmTest 的逐点比对背书(见 6.2)。
第 4 阶段:原生桥接
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libdropdown_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosdropdown.so)
│ DropdownMotion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;让页面复制动画数学又会让语义漂移,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("DropdownMotion")
fun dropdownMotion(request: String): String // frame / easing / catalog 一口进
请求 op | 入参 | 返回 |
|---|---|---|
frame | kind(enter/exit)、animation、easing、progress(线性 0…1)、width / height | alpha / scaleX / scaleY / offsetX / offsetY / eased |
easing | easing、progress | value(采样值) |
catalog | — | enter[12] / exit[12] / easings[4] / curves / 默认值 / menuMaxWidth |
单 C ABI 是刻意设计:以后字段再变,只动 JSON,const char* -> const char* 签名永不动,ArkTS 侧 dropdownMotion(string): string 永不改。
4.3 错误通道
@CName("DropdownMotion")
fun dropdownMotion(request: String): String = try {
/* op 分发 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
未知动画名、非法 JSON、越界参数(width / height 会 clamp 到 1…4096)全部走同一个 ok:false 通道,ArkTS 侧统一渲染,Kotlin 异常永远不跨 FFI。
4.4 C++ N-API 与 CMake
napi_property_descriptor desc[] = {
{"dropdownMotion", nullptr, NapiDropdownMotion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosdropdown_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosdropdown_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号)。
add_library(ohosdropdown SHARED IMPORTED)
set_target_properties(ohosdropdown PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(dropdown_napi SHARED napi_init.cpp)
target_link_libraries(dropdown_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosdropdown)
OHOS_ARCH 为空或 libohosdropdown.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。API 26 的 NAPI 头把 napi_get_value_string_utf8 缓冲区标成非 const——先量长度、开 std::vector<char> 拷贝再构造 std::string,直接传 std::string::data() 编不过。
4.5 MiniJson
桥的入参是扁平 JSON,没有嵌套对象和数组,手写 MiniJson(按逗号切顶层字段、剥引号、认 true/数字,字符串内逗号靠引号状态机跳过)即可,不拉 kotlinx-serialization 进 KN——qrose 同款实现,社区已审。
第 5 阶段:ArkUI 宿主
5.1 catalog 握手与帧循环
aboutToAppear(): void {
const raw = dropdownMotion(JSON.stringify({ op: 'catalog' }));
const catalog = JSON.parse(raw) as CatalogResult;
if (catalog.ok) {
this.enterList = catalog.enter ?? FALLBACK_ENTER; // 12 项,来自 Kotlin 枚举
this.exitList = catalog.exit ?? FALLBACK_EXIT;
this.easingList = catalog.easings ?? FALLBACK_EASINGS;
}
}
private playEnterExit(kind: string, animation: string, duration: number, onDone: () => void): void {
const width = 192; // 上游 MAX_WIDTH = 192.dp
const height = this.menuHeight(false);
const start = Date.now();
this.timerId = setInterval(() => {
const progress = Math.min(1, (Date.now() - start) / duration);
const res = this.callFrame(kind, animation, progress, width, height);
if (!res.ok) { /* 停表 + 状态行报错 */ return; }
this.frame = new FrameValues(res.alpha ?? 1, res.scaleX ?? 1,
res.offsetX ?? 0, res.offsetY ?? 0);
if (progress >= 1) { clearInterval(this.timerId); onDone(); }
}, 16);
}
渲染三件套直接套在面板上:
.opacity(this.frame.alpha)
.scale({ x: this.frame.scale, y: this.frame.scale, centerX: '50%', centerY: '50%' })
.translate({ x: this.frame.offsetX, y: this.frame.offsetY })
centerX/centerY: '50%' 不能省——上游 TransformOrigin.Center,等比缩放必须以面板中心为原点,否则 ScaleIn 会「往右下长」。
5.2 页面交互
四个 Tab 自上而下:选项区(芯片行 + 时长滑条)、舞台区(锚点按钮 + 192vp 面板)、状态行(enter SlideInHorizontally t=0.42 eased=0.35 α=1.00 s=1.00 dx=-62.4 dy=0.0 逐帧刷新,dx = -96 × (1 - eased))。缓动 Tab 用 Canvas 画曲线:41 个采样点全部来自桥,蓝色实线是采样曲线、灰色虚线是 Linear 参照、黑色圆点是五点采样标记;「播放缓动小球」用同一缓动驱动小球横移,加速曲线先慢后快一眼可辨。级联 Tab 是文件/编辑/帮助三级树:前进时旧面板按所选退场动画离开、新面板按所选入场动画进入(双面板同屏,对应上游 togetherWith);返回强制 FadeIn/FadeOut——这是上游对 Backward 方向的固定行为,不随所选动画变。
5.3 错误边界
ok:false 时错误原文进状态行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。切换 Tab / 重复点击会先 clearInterval 停掉旧帧循环,不叠加定时器。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosdropdown)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName("DropdownMotion") + MiniJson
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.dropdown、versionName 1.1.6-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/libohosdropdown_api.h
├── src/main/cpp/types/libdropdown_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :dropdown:jvmTest # 12 项交叉验证
./gradlew :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 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。
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.dropdown -m entry
四、完整代码对照
4.1 整体架构
io.androidpoet.dropdown(上游动画模型,commonMain 原样 + 新增帧引擎)
│ EnterAnimation ×12 / ExitAnimation ×12 / Easing ×4 / AnimationProp
│ DropdownMotion.sampleEasing / enterFrame / exitFrame(常量溯源 animation 1.10.3)
dropdown/src/composeMain(上游 Compose 部分,ohos 不编,零删除)
│ Dropdown Composable / Transition 工厂 / MenuColors / 菜单 DSL
Bridge.kt + MiniJson
│ DropdownMotion(const char* -> const char*)
libdropdown_napi.so
│ dropdownMotion(request)
Index.ets
│ setInterval 时间线 + JSON parse + opacity/scale/translate
▼
ArkUI 设备页面(四 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec 置前 + androidx 内容过滤、ohos_dropdown |
gradle/libs.versions.toml | 只剩 Kotlin 2.2.21-1.0.0 一个插件别名 |
build.gradle.kts(根) | 版本 1.1.6-ohos.1、全局禁用 Ohos*Test* 任务 |
dropdown/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64;jvmTest 依赖 animation-core(仅测试) |
dropdown/src/commonMain/.../DropdownMotion.kt | 帧引擎:曲线控制点、贝塞尔求解、12+12 映射 |
dropdown/src/commonMain/.../MotionFrame.kt | 帧数据类(alpha/scaleX/scaleY/offsetX/offsetY) |
dropdown/src/composeMain/** | 上游 Composable 与工厂,git mv 保留历史 |
dropdown/src/jvmTest/.../DropdownMotionTest.kt | 与真实 Compose 逐点比对 + 帧数学断言 |
example/nativeApp/.../Bridge.kt | 单 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 | 鸿蒙适配主文档(24 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 模型 | EnterAnimation / ExitAnimation / Easing | 上游枚举,ohos 直接编译 |
| 模型 | AnimationProp | tween 时间线参数(duration / delay / easing / initialAlpha) |
| 引擎 | DropdownMotion.sampleEasing | 三次贝塞尔采样,端点恒等 |
| 引擎 | DropdownMotion.enterFrame / exitFrame | 动画 × 缓动 × 线性进度 × 尺寸 → 当帧变换 |
| 上游 Compose | getEnterAnimation / getExitAnimation(composeMain) | Compose 工厂,与帧引擎互为镜像 |
| Native | DropdownMotion | C ABI JSON 导出(frame / easing / catalog) |
| N-API | dropdownMotion(request) | 向 ArkTS 暴露能力 |
| ArkTS | playEnterExit / callFrame | 时间线驱动与帧应用 |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(dropdownMotion(JSON.stringify({
op: 'frame', kind: 'enter', animation: 'SlideInHorizontally',
easing: 'FastOutLinearInEasing', progress: 0.5, width: 192, height: 240
})));
// res.alpha=1, res.scaleX=1, res.offsetX ≈ -64.8(加速曲线前半程,剩余偏移大于线性的一半)
val t = DropdownMotion.sampleEasing(Easing.FastOutLinearInEasing, 0.5f) // ≈ 0.325
val frame = DropdownMotion.enterFrame(
EnterAnimation.SlideInHorizontally, Easing.FastOutLinearInEasing, 0.5f, 192, 240)
// frame.offsetX = -192 * 0.5f * (1 - t) ≈ -64.8(vp,与 ArkTS 侧一致)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:隔离 Compose 而不是硬编或 ArkTS 重写
上游 Compose 1.10 与社区 CMP 1.9.2 工具链对不齐,硬编等于再打一套 Compose 发行版;ArkTS 重写动画则曲线控制点、起止值迟早漂移。动画模型(枚举 + 时间线 + 数学)恰好是平台无关的那一层——留在 commonMain 原样编译,Compose 部分进 composeMain 零删除,等社区 compose.ui ohos 制品出现可整体搬回。这是 qrose / Vico / Reorderable 已过审的同一套路。
决策 2:帧引擎常量全部溯源,不凭记忆
scaleIn 默认 0f 起步、LinearOutSlowInEasing 是 (0,0,0.2,1)——两个「凭印象必错」的点,靠下载 animation 1.10.3 sources jar 逐条核对才落定。本仓最初按错误记忆写的测试预期(曲线关于对角线对称)当场失败,反过来证明了核对流程的必要性。适配移植里「数学对不对」永远比「代码跑不跑」优先。
决策 3:根构建替换(cmp_qrose 模式),上游 Android 工程留盘
上游根构建(spotless / nexus / dokka / baseline profile / AGP)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。app / baselineprofile / baselineprofile-app / buildSrc 留盘不进 settings.gradle.kts——它们的存在方式只有这一种。
决策 4:单 C ABI + catalog 动态枚举,ArkTS 零手写语义
op=catalog 把枚举、曲线控制点、上游默认值(enter=SlideInHorizontally、exit=SlideOutHorizontally、easing=FastOutLinearInEasing、500ms、192dp)一次性下发给页面,选项卡运行时生成。凡是上游语义(枚举名、默认值、前缀)只允许 Kotlin 一份——和 qrose「载荷前缀必须来自 QrData.kt,不要在 ArkTS 手写」同一条纪律。C ABI 保持 const char* -> const char*,以后字段再变只动 JSON 不动签名。
决策 5:jvmTest 直 depend 真实 animation-core
交叉验证的对照物不是「另一份自己写的参考实现」,而是上游实际编译依赖的 androidx.compose.animation.core:1.10.3 本尊——199 个采样点 × 4 条曲线逐点比对,2e-4 容差。该依赖仅在 jvmTest 测试范围,ohos 目标与 klib 完全不可见,不会把 Compose 带进鸿蒙产物。测试对照物选得比实现本身还讲究,「引擎没漂」这句话才有分量。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12.1)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙设备(arm64 真机 / x86_64 模拟器)。
6.2 JVM 交叉验证
./gradlew :dropdown:jvmTest
12 项用例全部通过:
| 测试 | 钉住什么 |
|---|---|
curvesMatchUpstreamComposeExactly | 核心:真实 Compose 四条缓动 vs sampleEasing,0.005 步进扫 (0,1),2e-4 容差 |
easingEndpointsAreExact / linearEasingIsIdentity | 端点恒等与恒等曲线 |
curveFamiliesLeanTheRightWay | 前载 / 加速 / 减速曲线在对角线正确的侧别 |
easingsAreMonotonic / newtonSolverMatchesReferenceBisection | 单调性与求解器对独立二分参考的一致性 |
fadeInFamilyEntersFromZeroAlpha / elevationAndZAxisCombineFadeAndScale | fade 族与 Z 轴 / Elevation 组合的起止帧 |
slidesUseComposeDefaultMagnitudes | slide 族默认距离(-width/2、(w/2,h/2) 起、(-w/2,h/2) 止) |
scaleFamilyStartsFromZeroWithoutFading | Scale/Expand/Shrink 从 0 起步且不淡 |
tweenAppliesEasingToLinearProgress | easing 作用于线性进度的方向性 |
everyAnimationHasAFrameAtBothEnds | 24 种动画端到端可求值 |
「同一输入产出同一帧」是鸿蒙侧的验收底线——只看到菜单能动不算验收,能动只证明没崩,不证明和上游逐帧一致。
6.3 产物验证
:dropdown:compileKotlinJvm: BUILD SUCCESSFUL
:dropdown:jvmTest: BUILD SUCCESSFUL(12 项)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: DropdownMotion 为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libdropdown_napi.so)
CompileArkTS / PackageHap: finished
install bundle successfully / start ability successfully
| 产物 | 位置 | 大小 |
|---|---|---|
libohosdropdown.so(arm64,debugShared 未 strip) | entry/libs/arm64-v8a/ | ≈3.49 MB |
libohosdropdown.so(x86_64) | entry/libs/x86_64/ | ≈3.21 MB |
libc++_shared.so ×2 | 各 ABI 目录 | ≈1.7 MB |
libdropdown_napi.so | CMake 产物 | <0.3 MB |
链接期仅有一条 CAdapter 已知告警(api.cpp:448: non-void function does not return a value in all control paths,HarmonyOS Kotlin 生成的 napi 胶水,与 qrose 仓记录一致),so 正常产出。
6.4 功能用例
- 启动后状态行出现「目录加载成功:入场×12 退场×12 缓动×4」——枚举目录来自真实 Native 调用,非静态文字;
- 入场 Tab 选
SlideInHorizontally+FastOutLinearInEasing+ 500ms 点「打开菜单」,面板从左侧 96vp(width/2)滑入,状态行dx从-96.0单调变化到0.0; - 切
ScaleIn:面板从 0 等比长出,α全程 1.00;切ElevationScale:淡入与长出同时发生; - 退场 Tab 点「关闭(播放退场)」:
SlideOutHorizontally末帧dx=-96.0;SlideOut末帧dx=-96.0, dy=+h/2;Shrink 系α全程 1.00、s收到 0.00; - 缓动 Tab:FastOutSlowIn 的蓝色曲线明显前载(x=0.5 处 y≈0.776),FastOutLinearIn 后载(≈0.325),LinearOutSlowIn 起步最陡(≈0.839);「播放缓动小球」加速曲线先慢后快;
- 级联 Tab:文件 → 导出 → PDF 逐级进入,旧面板按所选退场、新面板按所选入场,双面板同屏;点头部返回强制 FadeIn/FadeOut;
- 每步操作状态行都有
t / eased / α / s / dx / dy五个量,起止帧与第 4.3 节表格精确一致。
6.5 验证结论
动画模型、缓动曲线数学、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙设备上成功安装并进入前台,页面展示了真实的菜单动效会话:目录握手、12+12 种动画逐帧播放、四条缓动曲线采样与级联导航闭环(见下节效果图)。
七、运行效果
7.1 设备截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/Dropdown.git
cd Dropdown
git checkout harmony-adaptation # 鸿蒙适配在此分支,master 仍为上游原始代码
export JAVA_HOME="/path/to/jdk-21"
./gradlew :dropdown:compileKotlinJvm # 动画模型编译
./gradlew :dropdown:jvmTest # 12 项交叉验证(含真实 Compose 比对)
./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.dropdown -m entry
hdc shell hilog | grep Dropdown
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让动画模型真正编译成
ohosArm64/ohosX64动态库,且每帧变换来自这份编译产物; - eazytec 代理对
androidx.*返回元数据但 jar 404,Gradle 认下元数据后不再回退其它仓库——settings.gradle.kts必须做仓库内容过滤(androidx 只走google()),否则 jvmTest 的 Compose 交叉验证依赖解析卡死; org.gradle.java.home管不了 wrapper 启动 JVM,Windows 上跑gradlew.bat前必须先设JAVA_HOME环境变量;- 适配者凭记忆写上游常量是最常见的翻车点:
scaleIn默认 initialScale 是 0f(不是 0.8/0.9),LinearOutSlowInEasing是 (0,0,0.2,1)(不是流传的 (0,0,0.05,1))——本次两个错误预期都是被 jvmTest 当场打回来的; FastOutSlowInEasing不关于对角线对称(x=0.5 → y≈0.776),「对称性」直觉不能当断言写;- hvigor 报
00304056 Can not find build config file build-profile.json5 at 'entry'——DevEco 工程除了根级build-profile.json5,entry 模块还必须有自己的build-profile.json5(含externalNativeOptions与双 ABIabiFilters),缺了 CMake 无从挂载; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须arm64-v8a+x86_64双写,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - DevEco 打开工程后生成的
example/harmonyApp/.idea/(含 clangd 缓存数据库、compile_commands.json)与.clang-tidy/.clangd会混进提交——根.gitignore的.idea是根锚定模式盖不住嵌套目录,要单独追加; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();API 26 头文件的非 const 缓冲区要用std::vector<char>中转; - ArkTS 侧
scale忘写centerX/centerY: '50%'会让中心缩放动画变成向右下生长——对应上游TransformOrigin.Center,不是可选项; - 桥的 width/height 必须与渲染侧同单位:偏移 = 尺寸 × 比例,Demo 统一用 vp,业务用 px 时要先换算,否则动画幅度翻倍/减半;
- 签名材料由 DevEco 自动写入
build-profile.json5,仓库内保持signingConfigs空数组,证书绝不进入 AtomGit。
8.2 已知问题
DropdownComposable / 菜单 DSL /MenuColors留在composeMain不参与 ohos 编译,等社区compose.uiohos 制品与 CMP 1.10 对齐后接回;- 菜单树在 ArkTS 侧维护:
MenuItemDSL 绑ImageVector,图标由业务在 ArkUI 侧自配,桥只传帧不传树; AnimationProp.initialAlpha上游 transitions 从不读取(疑似遗留字段),fork 原样保留但不参与帧数学;- 帧驱动是 16ms
setInterval,与 Compose 的 vsync 帧对齐有细微差别(时长本身按时间戳计算,精确); MenuPlacement/DpOffset锚定逻辑未过桥,属于 Composable 职责,ArkUI 侧自行锚位;- 上游
ExpandIn系名义是方向性动画、实际映射为等比scaleIn,ShrinkHorizontally/Vertically实际是等比scaleOut——fork 保留上游行为,未「增强」; - android / ios / js / wasmJs 目标剥离(源文件留在树里),发布、spotless、dokka、baseline profile 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.dropdown匹配的签名。
8.3 未来优化方向
- 补 uitest + hdc 自动化截图脚本,把四 Tab 几十个开关的验收脚本化(参照 qrose 的 capture.py 路径);
- 真机(arm64)与模拟器(x86_64)各补一轮全量截图存档;
- 若社区 CMP 1.10 /
compose.uiohos 制品出现,把composeMain搬回commonMain,DropdownMotion降级为对照测试; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查(
llvm-nm验DropdownMotion为 T 符号)和 HAP 构建。
九、总结
9.1 核心难点回顾
上游 dropdown(commonMain 全量 Compose 依赖)
→ 源码依赖度盘点:枚举纯净 / 工厂绑 Transition / Composable 绑 Material3
→ 拆分:枚举下沉 commonMain,工厂与 Composable 隔离 composeMain(零删除)
→ 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0(Gradle 8.14.1)
→ 根构建替换(cmp_qrose 模式 + androidx 仓库内容过滤)
→ DropdownMotion 帧引擎(常量溯源 animation 1.10.3 sources)
→ ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
→ C ABI(@CName × 1)+ frame/easing/catalog JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS 16ms 帧循环 + catalog 动态枚举
→ ArkUI 四 Tab 设备验收(入场 / 退场 / 缓动曲线 / 级联)
9.2 封装层次
- 模型层:上游原版
EnterAnimation/ExitAnimation/Easing/AnimationProp; - 引擎层:
DropdownMotion缓动采样与 12+12 种动画到帧变换的映射(常量逐条溯源 Compose 源码); - Native 层:双 ABI 动态库,单 C ABI 输出 JSON;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:时间线驱动、catalog 握手、页面状态、级联树与错误文本;
- DevEco 层:CMake、HAP、签名、安装和设备运行。
9.3 三条经验
- 上游常量必须溯源,不能凭记忆:两个「想当然」(scaleIn 起步值、LinearOutSlowIn 控制点)全部被交叉验证打回;移植的数学正确性优先于代码能跑,对照物要选上游实际编译依赖的那份源码,而不是网传值;
- 凡是上游语义,只允许 Kotlin 一份:枚举目录、曲线控制点、默认值全部经
op=catalog下发,ArkTS 连选项卡列表都不手写——两份实现必然漂移,漂移只是时间问题; - 交叉验证要验到「真实上游实现」级别:jvmTest 直接 depend
animation-core:1.10.3,让本 fork 的曲线与 Compose 本尊逐点比对,而不是自证清白;UI 类库的验收重点是「同一参数逐帧一致」,这比「菜单能动」严一个量级。
9.4 适配成果
当前适配已完成:
- Dropdown
1.1.6动画模型的双 ABI Kotlin/Native 编译(枚举与时间线零改动,composeMain零删除); DropdownMotion帧引擎与MotionFrame数据模型(常量溯源 animation / animation-core 1.10.3);- 单 C ABI 符号与 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 四 Tab 验收页面(目录握手、12+12 动画、缓动曲线画布、级联导航);
- 12 项 JVM 交叉验证全绿(含与真实 Compose 缓动 199 点 × 4 曲线逐点比对)+ 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)