本文记录 Dropdown 接入 OpenHarmony 的完整过程,覆盖动画库盘点、Compose 依赖边界拆分、纯 Kotlin 帧引擎构建、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 四 Tab 页面、HAP 构建和设备验收。

Dropdown 是基于 Compose Animation 的 Compose Multiplatform 级联下拉菜单库:Dropdown Composable 包住 Material3 的 DropdownMenu,提供 12 种入场动画、12 种退场动画、4 条缓动曲线与 tween 时间线(AnimationProp 的 duration / delay / easing),子菜单推进/返回由 AnimatedContent 的 togetherWith 过渡承载。本次适配与存储类库的「真编译整库」路线(KStore,得益于 kotlinx ohos 制品线补齐)有本质区别:Dropdown 的全部价值在 Compose UI,而社区 CMP 工具链是 1.9.2 / HarmonyOS Kotlin 2.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 实现。Dropdown Composable 留在 composeMain 等社区 compose.ui ohos 制品与 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,社区已审钉法)
JDK21(Temurin 21.0.12.1,org.gradle.java.home 写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(wrapper 钉版;2.2.21 旧线经验,同 qrose)
交叉验证依赖org.jetbrains.compose.animation:animation-core:1.10.3(仅 jvmTest 测试范围)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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.ktColor / MaterialTheme整文件挪 composeMain
DropDownMenuBuilder.kt菜单 DSL 绑 ImageVector整文件挪 composeMain
desktopMain/Main.ktcompose.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           = 恒等

两个「想当然必错」的点,核对前连本仓最初写的测试预期都是错的:

  1. scaleIn 默认从 0 起步,不是 0.8 / 0.9(那是 material SharedAxis 的自定义值)。上游 Dropdown 没传 initialScale,鸿蒙侧 ScaleIn / Expand 系就是从 0 长出来;
  2. 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入参返回
framekind(enter/exit)、animation、easing、progress(线性 0…1)、width / heightalpha / scaleX / scaleY / offsetX / offsetY / eased
easingeasing、progressvalue(采样值)
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.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(24 节)

4.3 关键 API 对照

层次API作用
模型EnterAnimation / ExitAnimation / Easing上游枚举,ohos 直接编译
模型AnimationProptween 时间线参数(duration / delay / easing / initialAlpha)
引擎DropdownMotion.sampleEasing三次贝塞尔采样,端点恒等
引擎DropdownMotion.enterFrame / exitFrame动画 × 缓动 × 线性进度 × 尺寸 → 当帧变换
上游 ComposegetEnterAnimation / getExitAnimation(composeMain)Compose 工厂,与帧引擎互为镜像
NativeDropdownMotionC ABI JSON 导出(frame / easing / catalog)
N-APIdropdownMotion(request)向 ArkTS 暴露能力
ArkTSplayEnterExit / 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 / elevationAndZAxisCombineFadeAndScalefade 族与 Z 轴 / Elevation 组合的起止帧
slidesUseComposeDefaultMagnitudesslide 族默认距离(-width/2、(w/2,h/2) 起、(-w/2,h/2) 止)
scaleFamilyStartsFromZeroWithoutFadingScale/Expand/Shrink 从 0 起步且不淡
tweenAppliesEasingToLinearProgresseasing 作用于线性进度的方向性
everyAnimationHasAFrameAtBothEnds24 种动画端到端可求值

「同一输入产出同一帧」是鸿蒙侧的验收底线——只看到菜单能动不算验收,能动只证明没崩,不证明和上游逐帧一致。

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.soCMake 产物<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 功能用例

  1. 启动后状态行出现「目录加载成功:入场×12 退场×12 缓动×4」——枚举目录来自真实 Native 调用,非静态文字;
  2. 入场 Tab 选 SlideInHorizontally + FastOutLinearInEasing + 500ms 点「打开菜单」,面板从左侧 96vp(width/2)滑入,状态行 dx 从 -96.0 单调变化到 0.0;
  3. 切 ScaleIn:面板从 0 等比长出,α 全程 1.00;切 ElevationScale:淡入与长出同时发生;
  4. 退场 Tab 点「关闭(播放退场)」:SlideOutHorizontally 末帧 dx=-96.0;SlideOut 末帧 dx=-96.0, dy=+h/2;Shrink 系 α 全程 1.00、s 收到 0.00;
  5. 缓动 Tab:FastOutSlowIn 的蓝色曲线明显前载(x=0.5 处 y≈0.776),FastOutLinearIn 后载(≈0.325),LinearOutSlowIn 起步最陡(≈0.839);「播放缓动小球」加速曲线先慢后快;
  6. 级联 Tab:文件 → 导出 → PDF 逐级进入,旧面板按所选退场、新面板按所选入场,双面板同屏;点头部返回强制 FadeIn/FadeOut;
  7. 每步操作状态行都有 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 踩坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让动画模型真正编译成 ohosArm64 / ohosX64 动态库,且每帧变换来自这份编译产物;
  2. eazytec 代理对 androidx.* 返回元数据但 jar 404,Gradle 认下元数据后不再回退其它仓库——settings.gradle.kts 必须做仓库内容过滤(androidx 只走 google()),否则 jvmTest 的 Compose 交叉验证依赖解析卡死;
  3. org.gradle.java.home 管不了 wrapper 启动 JVM,Windows 上跑 gradlew.bat 前必须先设 JAVA_HOME 环境变量;
  4. 适配者凭记忆写上游常量是最常见的翻车点:scaleIn 默认 initialScale 是 0f(不是 0.8/0.9),LinearOutSlowInEasing 是 (0,0,0.2,1)(不是流传的 (0,0,0.05,1))——本次两个错误预期都是被 jvmTest 当场打回来的;
  5. FastOutSlowInEasing 不关于对角线对称(x=0.5 → y≈0.776),「对称性」直觉不能当断言写;
  6. hvigor 报 00304056 Can not find build config file build-profile.json5 at 'entry'——DevEco 工程除了根级 build-profile.json5,entry 模块还必须有自己的 build-profile.json5(含 externalNativeOptions 与双 ABI abiFilters),缺了 CMake 无从挂载;
  7. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须 arm64-v8a + x86_64 双写,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  8. DevEco 打开工程后生成的 example/harmonyApp/.idea/(含 clangd 缓存数据库、compile_commands.json)与 .clang-tidy / .clangd 会混进提交——根 .gitignore 的 .idea 是根锚定模式盖不住嵌套目录,要单独追加;
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();API 26 头文件的非 const 缓冲区要用 std::vector<char> 中转;
  10. ArkTS 侧 scale 忘写 centerX/centerY: '50%' 会让中心缩放动画变成向右下生长——对应上游 TransformOrigin.Center,不是可选项;
  11. 桥的 width/height 必须与渲染侧同单位:偏移 = 尺寸 × 比例,Demo 统一用 vp,业务用 px 时要先换算,否则动画幅度翻倍/减半;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内保持 signingConfigs 空数组,证书绝不进入 AtomGit。

8.2 已知问题

  • Dropdown Composable / 菜单 DSL / MenuColors 留在 composeMain 不参与 ohos 编译,等社区 compose.ui ohos 制品与 CMP 1.10 对齐后接回;
  • 菜单树在 ArkTS 侧维护:MenuItem DSL 绑 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.ui ohos 制品出现,把 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 三条经验

  1. 上游常量必须溯源,不能凭记忆:两个「想当然」(scaleIn 起步值、LinearOutSlowIn 控制点)全部被交叉验证打回;移植的数学正确性优先于代码能跑,对照物要选上游实际编译依赖的那份源码,而不是网传值;
  2. 凡是上游语义,只允许 Kotlin 一份:枚举目录、曲线控制点、默认值全部经 op=catalog 下发,ArkTS 连选项卡列表都不手写——两份实现必然漂移,漂移只是时间问题;
  3. 交叉验证要验到「真实上游实现」级别: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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐