本文记录 wavy-slider 接入 OpenHarmony 的完整过程,覆盖纯 Compose 库的内核盘点、社区 CMP 鸿蒙发行版选型、波形数学抽取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI Canvas 真机页面、HAP 构建和真机验收。

wavy-slider 是 Compose Multiplatform 的动画"波浪滑块/进度条"库——模仿 Android 13 媒体控制中心的 squiggly slider,提供 Material 2 与 Material 3 两套 API:波浪形轨道随时间流动、可拖动 thumb、波长/波高/流速/线厚/轨道厚度/incremental 增量模式全部可调,waveHeight = 0 时退化为普通平进度条。它恰好是 KStore 的镜像情形:KStore 是"零 UI、纯引擎"的库,正确答案是真编译整库;wavy-slider 是"零引擎、纯 UI"的库——100% 代码站在 androidx.compose.* 上,而社区鸿蒙 CMP 发行版目前是 1.9.2 系、上游基于 CMP 1.9.3 的官方 Slider.kt fork 改写,版本对不齐。因此本次适配走的是 qrose / Vico / Reorderable 已过审的同一条路:不把 Compose UI 编进鸿蒙,把可复用内核(波形采样数学)抽到 commonMain 作为单一事实来源,ArkTS Canvas 负责绘制与交互——库算出来的波,ArkUI 画出来。

项目地址: AtomGit/oh-tpc/wavy-slider.git

上游仓库: mahozad/wavy-slider

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

wavy-slider 在 Compose 应用里当 Material Slider 用:两个包 material / material3 各一个 WavySlider composable,waveLength / waveHeight / waveVelocity / waveThickness / trackThickness / incremental 六个参数控制波形;默认值通过 SliderDefaults 扩展属性下发(波长 20dp、波高 6dp、流速 10dp+TAIL、M2 线厚 4dp、M3 轨道厚 16dp);波高变化 tween(300ms)、流速变化 tween(2000ms)、出场展开 tween(6000ms) 三套默认动画内置在 WaveAnimationSpecs 里。整个库没有一行平台代码——没有 expect/actual、没有 androidMain/iosMain,绘制全部走 Compose Canvas + Path。

上游构建(Kotlin 2.2.21 / Compose Multiplatform 1.9.3 / Gradle 9.2.0 / AGP 8.12.3)不会生成 OpenHarmony 变体;而把 Compose 编进鸿蒙,当前条件也不成立:

障碍具体问题
Compose 版本错位社区鸿蒙 CMP 发行版是 1.9.2-OH 系;上游两个 WavySlider.kt 是对 CMP 1.9.3 官方 Slider.kt 源码的 fork 改写(文件头注释自证),强行对齐等于重打一套 Compose 发行版。
已过审路线明确qrose / Vico / Reorderable 的审核结论:鸿蒙侧不跑 Compose UI,commonMain 保算法/数据模型,UI 隔离,ohosArm64/ohosX64 出 sharedLib,ArkTS 画 UI。
纯 UI 库没有"引擎"可编100% 代码依赖 androidx.compose.*,Path / DrawScope / AnimationSpec / Dp 都过不了 ohos 编译。
语言边界ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
动画语义跨不了桥withFrameNanos 帧循环、animateDpAsState 缓动是 Compose 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:把波形采样数学从 Core.kt 的 createWavyPath 里抽成纯 Kotlin 的 sampleWave(公式一比一复刻),编进 ohos KLIB 作为单一事实来源;两个 composable 与上游测试隔离到不编译的 composeMain / composeDesktopTest;ArkTS 只负责帧循环、缓动驱动、Canvas 绘制和拖拽交互。波形的每一个点都来自库本身,ArkTS 里没有第二份波形公式。

1.2 库提供的能力

Demo(三页 Tabs)覆盖桥接的全部入口,对应上游 showcase 的演示矩阵:

  • M2 细线滑块:默认参数(20/6/10dp、TAIL),出场 6000ms 展开动画启用;
  • M2 自定义滑块:上游 showcase 同款参数(波长 12dp、波高 24dp、流速 14dp、方向 RIGHT、线厚 1dp、轨道 5dp、增量模式、红色);
  • 压平波浪:waveHeight = 0,桥端直接返回两点中线——与上游 createFlatPath 语义一致;
  • M3 粗轨滑块:16dp 轨道 + 圆形 thumb + 6dp 间隙,以及 28dp 大 thumb 的自定义版本;
  • 模式与流向:增量模式对比、LEFT / RIGHT / TAIL / HEAD 四方向按钮(换向走 2000ms 缓动)、波高渐变 Toggle(300ms 缓动);
  • WavyDivider:静态波浪分隔线(流速 0、value=1),对应上游 FAQ 里的 divider 用法;
  • 默认值对账:说明页动态展示 WavySliderDefaults() 的真实返回值,不硬编码 20/6/10。

采样响应包含:

字段作用
ok采样是否成功,失败时 error 带原文。
count点数(= points 长度 / 2)。
points扁平数组 [x0, y0, x1, y1, ...],moveTo 起点精确到浮点,其后每整数像素步进一点——与 createWavyPath 的步进完全一致。

上游 composable 的完整能力(M2/M3 两套 API、SliderState 重载、自定义 thumb/track、六参数、三套默认动画语义)保留在 composeMain 源码里随仓库交付;JVM 侧保留 8 项 commonTest 交叉验证用例,证明 ohos .so 里执行的波形数学没有漂移。

1.3 实现适配

维度要求
代码复用波形公式不重写:sampleWave 对 createWavyPath 逐行复刻(同 Double 精度、同整数步进、同平波退化);WaveDirection / WavySliderDefaults 同步下沉 commonMain。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);showcase 各平台 app 与 :demo 留盘不进构建。
桥接稳定3 个 C ABI 和 UTF-8 JSON,全部入参用像素(密度换算在调用方),不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律零第三方依赖:库与桥只用 kotlin stdlib,JSON 手写 MiniJson(继承 qrose 审核模式)。
UI 完整三页覆盖全部桥接入口:三组滑块共享 value、四方向切换、模式对比、静态分隔线、默认值对账。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。wavy-slider 本身只输出 UI,适配面天然最大;ArkTS Demo 复刻的是"波怎么动、怎么拖",而"波长什么样"每帧都由库计算。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、调研社区 CMP 鸿蒙发行版、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:内核抽取       ── createWavyPath 公式下沉 commonMain、Compose 代码隔离 composeMain
第 4 阶段:原生桥接       ── @CName C ABI × 3、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── Canvas 逐帧采样、DisplaySync 帧循环、缓动复刻、拖拽交互
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
library/                              库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
library/src/commonMain/               新增:WaveMath.kt(纯 Kotlin 波形采样,唯一新增源文件)
library/src/commonTest/               新增:WaveMathTest.kt(8 项交叉验证)
library/src/composeMain/              上游两个 WavySlider composable + Core.kt + AOSP.kt(隔离,不编译)
library/src/composeDesktopTest/       上游截图/视觉测试与 15 张基准图(隔离,不编译)
example/nativeApp/                    Kotlin/Native 动态库(WaveBridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
showcase/ 与 asset/demo               上游各平台示例 app 与录屏工程,留盘不进 settings
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library(改名 wavy-slider,保留上游发布名语义)与 :example:nativeApp,rootProject.name = "ohos_wavy_slider"。上游的 binary-compatibility-validator / compose / compose-compiler / AGP / vanniktech 发布 / dokka 插件全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 C:\Users\Administrator\.jdks\jdk-21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 9.2.0 降级;2.2.21 线只支持 Gradle 8.x)
Compose不引入。社区 1.9.2-OH 系与上游 1.9.3 fork 不对齐,UI 走 ArkTS
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.wavyslider
$env:JAVA_HOME = "C:\Users\Administrator\.jdks\jdk-21"
java -version
1.3 源码依赖度盘点
文件依赖形态处置
Core.kt@Composable animate* + DrawScope.createWavyPath + Path/Dp/AnimationSpec公式抽到 commonMain,文件平移 composeMain
material/WavySlider.kt(684 行)M2 composable + createFlatPath + drawTrackInactivePart平移 composeMain,平波语义在 sampleWave 里镜像
material3/WavySlider.kt(1041 行)M3 composable + SliderState 重载平移 composeMain
AOSP.kt(239 行)AOSP SquigglyProgress 移植参考平移 composeMain
desktopTest/compose.desktop.uiTestJUnit4 截图/视觉测试 + 15 张基准图改名 composeDesktopTest,不进构建

全库 expect/actual 为零,这是"纯 UI 库"的直接证据。抽取物是新文件 WaveMath.kt,四样东西:

  • sampleWave(startX, endX, bandHeightPx, waveLengthPx, waveHeightPx, waveSpread, waveShiftPx, incremental): FloatArray——公式逐行对应 createWavyPath:y = (sin(radians) * heightFactor * waveHeightPx + bandHeight) / 2、radians = waveSpread * (x - first + waveShiftPx) / waveLengthPx * 2π,Double 精度计算、逐点转 Float、起点精确浮点、其后整数步进、incremental 时 heightFactor 从 0 向 1 线性增长,全部与上游一致;
  • 平波退化:waveLengthPx <= 0 || waveHeightPx == 0 返回两点中线,镜像 createFlatPath;
  • WaveDirection 枚举——上游的 factor(LayoutDirection) 换成 factor(isLtr: Boolean),LEFT/RIGHT/TAIL/HEAD 四个系数原样保留(这是"类型定义跟 UI 绑在一起,拆的时候要手洗"的典型一刀);
  • WavySliderDefaults——20/6/10dp、TAIL、4/4/16dp、incremental=false、300/2000/6000ms,11 个常量对齐上游默认值,ArkTS 侧永远从桥拿默认,不硬编码。
1.4 页面边界
标题区          wavy-slider · OpenHarmony / value = 0.67(所有滑块共享,可拖动)
Tab 切换        滑块 / M3 与模式 / 分隔线与说明
滑块页          M2 默认(出场动画)/ M2 自定义(红色增量)/ 压平波浪(waveHeight=0)
M3 页           M3 粗轨+圆 thumb / M3 大 thumb / 增量模式 / 四方向按钮 / 波高渐变 Toggle
说明页          WavyDivider ×2(静态波)/ 工作原理 / 桥接默认值对账 / 仓库与社区链接

三个滑块通过 @Link value 共享同一个 0…1 值,拖任何一个,其余两个同步走——对应上游 showcase 的 SliderState(0.67f) 共享状态写法。

第 2 阶段:目标与依赖打通

2.1 发行版调研(本适配的路线分岔口)

先查社区 Maven(https://maven.eazytec-cloud.com)上 CMP 鸿蒙发行版的真实状态。用 Nexus REST 逐个验证 .module 元数据:

org.jetbrains.compose:compose-gradle-plugin
  ohos 线版本:1.9.2-0.2.x / 1.9.2-OH.0.1.2-xx(最新 -37)
org.jetbrains.compose.material3:material3:1.9.2-OH.0.1.2-37
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published ✓
org.jetbrains.compose.material:material / foundation:同上 ✓

社区发行版存在且制品齐全——但它停在 1.9.2 系,而上游 wavy-slider 编译目标是 CMP 1.9.3,两个 WavySlider.kt 是对 1.9.3 官方 Slider.kt 源码的 fork。把一个对官方源码做过手术的组件库对齐到低一档的发行版,等于把手术重做一遍。已过审仓库(qrose / Vico / Reorderable)在同样的分岔口都选择了同一条路:鸿蒙不跑 Compose UI。等 compose.ui 的 ohos 制品与上游版本对齐,composeMain 平移回 commonMain 即可,这条退路在 WaveMath.kt 的 KDoc 里写明了。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

库零第三方依赖,没有 kotlinx 制品线的元数据反推问题,直接沿用社区已批准的钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose 同款)。代价是 Gradle:上游 wrapper 是 9.2.0,2.2.21 线的 Kotlin 插件只支持 Gradle 8.x,wrapper 降级 8.14.1(qrose 同款)。这与 KStore 的选择恰好相反(KStore 因 coroutines 制品线升到 Kotlin 2.4.20、Gradle 沿用 9.6.1)——工具链跟着依赖走,两条线的经验不能互相套用。

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_wavy_slider"

include(":library")
project(":library").name = "wavy-slider"
include(":example:nativeApp")

gradle.properties 钉 JDK 21、版本 2.2.0-ohos.1,并加两条 KN 开关:kotlin.native.ignoreDisabledTargets=true 与 kotlin.native.distribution.downloadFromMaven=true(后者让 ohos 交叉编译工具链从 eazytec Maven 拉取)。

2.3 编译纯数学内核

library/build.gradle.kts 收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

gradle/libs.versions.toml 只剩一个插件别名(kotlin = "2.2.21-1.0.0")。实测:

.\gradlew.bat :wavy-slider:compileKotlinJvm :wavy-slider:compileKotlinOhosArm64   # BUILD SUCCESSFUL

上游的 compose / compose-compiler / android-library / maven-publish / dokka / apiCheck 插件全部不再引用;library/api/ 的二进制兼容快照留盘不消费。

2.4 上游测试的处置:截图测试跑不了,交叉验证另起炉灶

上游 desktopTest 是 278 行截图测试 + 893 行视觉测试,依赖 compose.desktop.uiTestJUnit4 与 15 张基准图——Compose 依赖剥掉后不可能运行。处置是把整个目录平移到 composeDesktopTest(不是 Kotlin 源集,不参与构建,源码与基准图原样保留);交叉验证用新写的 commonTest(8 项用例,见 6.2)钉住抽取公式的语义。上游测试零删改,鸿蒙侧另起验证——比"改上游测试迁就构建"诚实。

第 3 阶段:桥协议建模

3.1 JSON 契约
ir.mahozad.multiplatform.wavyslider/**(WaveMath,commonMain 原样编译)
        ↓ sampleWave / WaveDirection / WavySliderDefaults
WaveBridge.kt(@CName × 3,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API wavySliderSample() / wavySliderDefaults() / wavySliderDirectionFactor()
        ↓
ArkUI Index.ets(Canvas 逐帧采样绘制)

请求与响应:

请求 : {"startX":24.0,"endX":240.5,"bandHeight":16.0,"waveLength":40.0,
        "waveHeight":12.0,"spread":1.0,"shift":0.0,"incremental":false}
响应 : {"ok":true,"count":N,"points":[x0,y0,x1,y1,...]}
       {"ok":false,"error":"..."}

三个设计决定:其一,全部入参用像素——库默认值是 dp,密度换算(vp2px)放在调用方,桥不做任何隐式缩放,waveLength / waveHeight 因此必填(有 px 语义的缺省值是谎言);其二,每帧全量采样——shift(累计位移)由 ArkTS 帧循环累积,波形的每个点始终出自库,ArkTS 不持有任何波形公式;其三,平波不报错——waveLength <= 0 或 waveHeight == 0 走 sampleWave 内置的两点中线分支,语义与上游 createFlatPath 对齐。

3.2 三导出的分工
导出签名覆盖内核
WavySliderSample(request: String): StringsampleWave 全参数采样 + 校验(必填字段、有限数、endX >= startX)
WavySliderDefaults(): StringWavySliderDefaults 11 项默认值(dp 值 + 300/2000/6000ms)+ TAIL 方向
WavySliderDirectionFactor(direction: String): IntWaveDirection.factor(isLtr = true):LEFT→1、RIGHT→-1、TAIL→1、HEAD→-1、未知→0

WavySliderDefaults 的存在让 ArkTS 侧零硬编码:波长 20dp、三段动画时长全部启动时从桥读取,桥失败才走注释标明的应急兜底。WavySliderDirectionFactor 把方向语义留在库侧——ArkTS 只拿一个带符号系数乘进速度。

桥内浮点序列化是手写的 %.4f 定点:Kotlin/Native 没有 String.format,裸 Float.toString 会切科学计数法,4 位小数远低于屏幕像素精度,饱和取整保证极端值仍是合法 JSON。

3.3 错误通道

主导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("WavySliderSample")
fun wavySliderSample(request: String): String {
    return try {
        val json = MiniJson.parse(request)
        /* 校验 + sampleWave + 序列化 */
    } catch (t: Throwable) {
        """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
    }
}

非法 JSON、缺必填字段、NaN / Infinity、endX < startX,全部走同一个 ok:false 通道;ArkTS 侧 hilog 打点(去重)并跳过该帧。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libwavy_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoswavyslider.so)
  │ WavySliderSample / WavySliderDefaults / WavySliderDirectionFactor
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让 ArkTS 复制波形公式,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("WavySliderSample")
fun wavySliderSample(request: String): String          // 一帧一次,全量采样

@CName("WavySliderDefaults")
fun wavySliderDefaults(): String                        // 启动握手一次

@CName("WavySliderDirectionFactor")
fun wavySliderDirectionFactor(direction: String): Int   // 方向切换时

example/nativeApp/build.gradle.kts 声明双 ABI sharedLib { baseName = "ohoswavyslider" },依赖 project(":wavy-slider")——sharedLib 只放 nativeApp,库模块不开,否则每个模块带一份 KN 运行时。copyKnSoArm64 / copyKnSoX64 两个 Copy 任务 finalizedBy 在 link 任务后,把 libohoswavyslider.so 与匹配 triple 的 libc++_shared.so 拷进 example/harmonyApp/entry/libs/<abi>/。实测链接后 llvm-nm --defined-only:

T WavySliderDefaults
T WavySliderDirectionFactor
T WavySliderSample
T libohoswavyslider_symbols

四个符号全部全局导出,与 KN 自动生成的 libohoswavyslider_api.h、手写头三方一致。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"wavySliderSample", nullptr, NapiWavySliderSample, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"wavySliderDefaults", nullptr, NapiWavySliderDefaults, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"wavySliderDirectionFactor", nullptr, NapiWavySliderDirectionFactor, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、两段式 napi_get_value_string_utf8 取串、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoswavyslider_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoswavyslider_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与三个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohoswavyslider SHARED IMPORTED)
set_target_properties(ohoswavyslider PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(wavy_napi SHARED napi_init.cpp)
target_link_libraries(wavy_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoswavyslider)

OHOS_ARCH 为空或 libohoswavyslider.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const wavySliderSample: (request: string) => string;
export const wavySliderDefaults: () => string;
export const wavySliderDirectionFactor: (direction: string) => number;

ArkTS 只 import libwavy_napi.so,绝不 import libohoswavyslider.so——HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS 模块上。

第 5 阶段:ArkUI 宿主

5.1 帧循环与动画驱动(把 Core.kt 的语义搬过来)

上游动画三件套在 ArkTS 侧逐条复刻,时长全部从 wavySliderDefaults() 读取:

上游语义(Core.kt)ArkTS 复刻
animateWaveShift:withFrameNanos 逐帧累积 shift += amount * dtDisplaySync on('frame', cb) 帧时间戳驱动,shift += amount·dt;DisplaySync 不可用兜底 setInterval(16ms)
流速变化 tween(2000ms, LinearOutSlowInEasing)自实现 CubicBezier 同曲线,amount 向新目标(速度×方向系数)缓动 2000ms,时间基切换时重定基
波高变化 tween(300ms, FastOutSlowInEasing)同款 300ms 缓动,波高渐变 Toggle 演示
出场 animateWaveSpread tween(6000ms, EaseOutQuad)spread 从 0 到 1 的 6000ms 缓动,首个滑块启用
WaveDirection.factorwavySliderDirectionFactor() 返回带符号系数乘进速度

每帧把参数打包成 JSON 调 wavySliderSample,points 用 moveTo + 逐点 lineTo + stroke(圆头圆角、lineWidth = 线厚 px)画出激活段;未激活段按上游 drawTrackInactivePart 的画法补圆头平线;thumb 按模式画带阴影圆形。绘制侧保证 endX >= startX(活跃段不足 1px 不发起调用)。

5.2 页面交互

三页 Tabs:"滑块"页 M2 默认 / 自定义 / 压平;"M3 与模式"页粗轨 + 大 thumb、增量对比、四方向按钮、波高 Toggle;"分隔线与说明"页两条静态 WavyDivider、工作原理、默认值对账与社区链接。拖拽走 onTouch(Down/Move),value = clamp((x - thumbR) / span),三个滑块 @Link 共享联动。

5.3 对比度和错误边界

激活段品牌紫 / 未激活段半透明、自定义滑块红色系、方向按钮选中态深底白字;ok:false 时 hilog 打点(去重)并跳帧,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoswavyslider)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/WaveBridge.kt     @CName × 3 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.wavyslider、versionName 2.2.0-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohoswavyslider_api.h
        ├── src/main/cpp/types/libwavy_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(三页 Tabs + WavySliderView 组件)
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Users\Administrator\.jdks\jdk-21"
.\gradlew.bat :wavy-slider:jvmTest                                              # 8 项交叉验证
.\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 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

hdc list targets -v
hdc -t <设备序列号> install -r \
  entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.wavyslider -m entry

四、完整代码对照

4.1 整体架构

ir.mahozad.multiplatform.wavyslider/**(WaveMath 内核,commonMain)
    │ sampleWave / WaveDirection / WavySliderDefaults
WaveBridge.kt + MiniJson
    │ WavySliderSample / WavySliderDefaults / WavySliderDirectionFactor
libwavy_napi.so
    │ wavySliderSample / wavySliderDefaults / wavySliderDirectionFactor
WavySliderView(@Component)
    │ DisplaySync 帧循环 + 缓动 + Canvas stroke
    ▼
ArkUI 真机页面(三页 Tabs,滑块可拖)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_wavy_slider、showcase/:demo 移出
gradle/libs.versions.toml只剩 Kotlin 2.2.21-1.0.0 + multiplatform 插件别名
build.gradle.kts(根)版本 2.2.0-ohos.1、全局禁用 Ohos*Test* 任务
gradle.propertiesJDK 21 路径、KN 双开关(downloadFromMaven)
library/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,commonTest 挂 kotlin-test
library/src/commonMain/.../WaveMath.kt波形采样内核(唯一新增库源文件)
library/src/commonTest/.../WaveMathTest.kt8 项交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../WaveBridge.kt3 个 C ABI、定点序列化、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(WavySliderView 组件 + 三页)
README.OpenHarmony_CN.md鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
内核sampleWave(...)一比一复刻 DrawScope.createWavyPath,返回扁平 (x,y) 数组
内核WaveDirection.factor(isLtr)四流向系数(上游 factor(LayoutDirection) 的纯 Kotlin 形态)
内核WavySliderDefaults20/6/10dp、TAIL、4/4/16dp、300/2000/6000ms
上游保留WavySlider(M2/M3)composable,隔离在 composeMain 等工具链对齐
NativeWavySliderSample / WavySliderDefaults / WavySliderDirectionFactorC ABI JSON 导出
N-APIwavySliderSample(req) 等向 ArkTS 暴露能力
ArkTSWavySliderView帧循环采样 + Canvas 绘制 + 拖拽的复用组件

4.4 ArkTS 与 Kotlin 的边界

const request = new SampleRequest(startX, activeEnd, bandHeightPx,
  waveLengthPx, waveHeightPx, this.spread, this.shiftPx, this.incremental);
const res = JSON.parse(wavySliderSample(JSON.stringify(request))) as SampleResponse;
// res.ok === true;res.points = [x0, y0, x1, y1, ...] 逐点 lineTo
val points = sampleWave(
    startX = startX.toFloat(), endX = endX.toFloat(),
    bandHeightPx = bandHeight.toFloat(),
    waveLengthPx = waveLength.toFloat(), waveHeightPx = waveHeight.toFloat(),
    waveSpread = spread.toFloat(), waveShiftPx = shift.toFloat(),
    incremental = incremental,
)

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;波形公式的唯一副本在 Kotlin 侧。

五、关键决策说明

决策 1:不把 Compose 编进鸿蒙,抽数学内核

社区 CMP 鸿蒙发行版停在 1.9.2 系,上游是 1.9.3 的 Slider.kt fork——强行对齐等于重打发行版且重做手术。波浪滑块的可复用内核是波形采样数学(不是 composable),把它抽成 sampleWave 作为单一事实来源,ArkTS 只做绘制与交互。这与 KStore 的"真编译整库"是同一原则在两种库形态上的投影:编得进去的(引擎)整库编,编不进去的(UI)抽内核。

决策 2:工具链钉 2.2.21-1.0.0,Gradle 降 8.14.1

库零第三方依赖,没有元数据反推问题,直接沿用 qrose 已批准的钉法。代价是 Gradle 从上游 9.2.0 降到 8.14.1(2.2.21 线只支持 8.x)——KStore 升到 2.4.20 留 9.x 是因为 coroutines 制品线,工具链版本跟着依赖走,不跟着习惯走。

决策 3:根构建替换(cmp_qrose 模式),showcase 留盘

上游根构建(compose / AGP / vanniktech 发布 / dokka / apiCheck)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。showcase 五个平台 app 与 :demo 录屏工程留盘不进 settings.gradle.kts——它们是上游演示资产,鸿蒙对应物是 example/harmonyApp。

决策 4:入参全像素 + 三导出,动画驱动放 ArkTS

桥不做隐式 dp→px 换算(密度在调用方),waveLength/waveHeight 必填;WavySliderDefaults 单独导出让 ArkTS 零硬编码;WavySliderDirectionFactor 把方向语义留在库侧。帧循环与缓动是 UI 时序,归 ArkTS;波形每点归 Kotlin——数据过桥,交互原生,对应 Vico"图表数据交给 ArkTS 画 Canvas"的同款切分。

决策 5:上游截图测试隔离,交叉验证另起

上游 desktopTest 深度绑定 Compose 桌面测试栈,剥离 Compose 后不可能运行;整体平移 composeDesktopTest(源码 + 15 张基准图零删改),另写 8 项 commonTest 钉住抽取公式。测试文件一个字节不动,新验证的独立性才成立。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21(Temurin)、DevEco Studio 26.0(命令行 hvigor + Node 工具链)、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

.\gradlew.bat :wavy-slider:jvmTest

8 项用例全部通过(内核在 commonMain,JVM 与 ohos 跑同一份代码):

用例钉住什么
平波退化(波高 0)返回两点、y = band/2,镜像 createFlatPath
平波退化(波长 ≤ 0)同上,不除零
零 spread所有 y 恒等于中线(出场动画起点语义)
波形带内sin 有界 → y 始终在 [center - h/2, center + h/2]
平移一个波长复现波形radians 对 x 的周期性(公式结构没漂)
增量包络`
方向系数LEFT/RIGHT/TAIL/HEAD × isLtr 八种组合,对齐 WaveDirection.factor
默认值对账20/6/10dp、TAIL、4/4/16dp、300/2000/6000ms 共 11 项

6.3 HAP 验证

:wavy-slider:compileKotlinJvm / compileKotlinOhosArm64: BUILD SUCCESSFUL
:wavy-slider:jvmTest: BUILD SUCCESSFUL(8 项)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm --defined-only: WavySliderSample / WavySliderDefaults / WavySliderDirectionFactor / libohoswavyslider_symbols 均为全局 T 符号
CompileArkTS: finished
BuildNativeWithNinja: finished(双 ABI 编出 libwavy_napi.so,约 290KB / 270KB)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 6.7 MB,内含双 ABI 各三件套(libwavy_napi.so / libohoswavyslider.so / libc++_shared.so);KN so 为 debugShared 未 strip 口径,arm64 约 3.6 MB、x86_64 约 3.3 MB(含 Kotlin/Native 运行时)。

6.4 功能用例

  1. 启动即握手 wavySliderDefaults(),说明页默认值对账显示真实桥数据(20/6/10dp、TAIL、4/4/16dp、300/2000/6000ms);
  2. 首页三个滑块共享 value = 0.67,拖动任意一个,其余两个同步联动(@Link);
  3. 波形随时间持续流动(默认 TAIL 方向,流速 10dp/s);
  4. 切换 LEFT / RIGHT / TAIL / HEAD,波在 2000ms 缓动内换向,不跳变;
  5. 波高渐变 Toggle 触发 300ms 缓动,波形平滑压扁 / 抬起;
  6. waveHeight = 0 的滑块呈平线(桥端直接返回两点中线,不报错);
  7. 增量模式滑块的波高从起点 0 向 thumb 逐渐长大;
  8. WavyDivider 两条静态波稳定不动(流速 0);
  9. 首个滑块出场时 6000ms 展开动画可见(spread 0→1)。

6.5 验证结论

波形数学内核、Native 动态库、C ABI、N-API、ArkTS Canvas 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,三个滑块实时联动、波形持续流动、模式与方向切换全部符合预期(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/wavy-slider.git
cd wavy-slider
export JAVA_HOME="/path/to/jdk-21"
./gradlew.bat :wavy-slider:compileKotlinOhosArm64                       # klib
./gradlew.bat :wavy-slider:jvmTest                                      # 8 项交叉验证
./gradlew.bat :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.wavyslider -m entry
hdc shell hilog | grep -E "WavySliderPage|WavyNapi"

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :wavy-slider 真正编译成 ohosArm64 / ohosX64 产物——哪怕库里剩下的只是数学;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 纯 Compose 库没有"非 Compose 内核"可直接编译,抽取是必经步骤:公式(createWavyPath)、类型(WaveDirection 的 factor(LayoutDirection) 要改成 factor(isLtr))、默认值(WavySliderDefaults)三样都要手洗,漏一样 ArkTS 就得复制一份语义;
  4. 上游 Slider.kt fork 的版本锚点要留证:两个 WavySlider.kt 文件头注释写明基于 CMP 1.9.3,这是"为什么不编 Compose"的依据,文档里要能对上;
  5. import { displaySync } from '@kit.GraphicsKit' 在 API 18 SDK 报 “Kit has no corresponding config file”——displaySync 属于 ArkGraphics2D 线,且该 SDK 下 kit 桶不可用,改为直接 import displaySync from '@ohos.graphics.displaySync';
  6. 直接模块导入后,回调参数类型是 displaySync.IntervalInfo 而不是 kit 时代的 FrameInfo——照 kit 文档抄会报 “has no exported member”;
  7. @Prop direction / @Prop enabled 与 CustomComponent 内置属性重名,编译报 “not assignable to the same property in base type”——改 flowDirection / interactive;
  8. ctx.lineCap = LineCapStyle.Round 类型不匹配:Canvas 上下文要的是 CanvasLineCap 字符串联合,写 ctx.lineCap = 'round'(lineJoin 同理);
  9. onAreaChange 的第二参数是 AreaChangeOptions,把回调拆成两个箭头函数传会类型错误——正确签名 (oldValue: Area, newValue: Area) => void;
  10. 根构建的 /build/ 与 .idea 是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/.idea/、hvigor 装进 entry/oh_modules/ 的 hypium 都要单独追加 gitignore,否则混进提交;
  11. Kotlin/Native 没有 String.format,Float.toString 会切科学计数法,JSON 数字要手写定点序列化;
  12. isFinite 在 Kotlin/Native 是函数不是属性,value.isFinite 编不过,要 value.isFinite();
  13. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  14. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();签名材料由 DevEco 自动写入 build-profile.json5,提交前保护,证书绝不进入 AtomGit。

8.2 已知问题

  • 两个 WavySlider composable(M2/M3)不编进鸿蒙,composeMain 保留源码,等社区 compose.ui ohos 制品与上游 CMP 版本对齐后移回 commonMain,WaveMath.kt 随之退役;
  • 动画语义(tween 时长 / 缓动曲线 / shift 累积)在 ArkTS 侧复刻,库侧只提供默认时长与方向系数——缓动曲线是自实现 CubicBezier,与 Compose 同参数但非同代码;
  • Demo 按 LTR 布局;RTL 需按 WaveDirection.factor(isLtr = false) 语义交换 LEFT/RIGHT 系数;
  • android / js / wasmJs / apple 目标剥离(源文件留在树里),发布、dokka、apiCheck 一并剥离,本 fork 不发 Maven Central;
  • vp2px 在新 SDK 有 deprecation 警告(功能正常,后续可换 display 密度自算);
  • HAP 安装必须使用与 bundle name org.terminator.ohos.wavyslider 匹配的签名。

8.3 未来优化方向

  • 把 ArkTS 侧的 WavySliderView 组件整理成可分发的 HAR,带上 sampleWave 的 so 三件套,让鸿蒙工程一行依赖用上波浪滑块;
  • 缓动曲线改从桥下发(库侧导出 bezier 参数),进一步压缩 ArkTS 侧的动画实现;
  • 跟踪社区 CMP 鸿蒙发行版版本,对齐后把 composeMain 平移回 commonMain,ArkTS Demo 降级为 fallback;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 wavy-slider(100% commonMain 纯 Compose)
    → 调研社区 CMP 鸿蒙发行版(1.9.2-OH 系存在,与上游 1.9.3 Slider fork 不对齐)
    → 走已过审路线:不编 Compose UI
    → 内核抽取(createWavyPath → sampleWave,公式 / 类型 / 默认值三件手洗)
    → 工具链钉 Kotlin 2.2.21-1.0.0、Gradle 降 8.14.1(cmp_qrose 模式根构建替换)
    → ohosArm64 / ohosX64 KLIB + 双 ABI sharedLib
    → C ABI(@CName × 3,全像素 JSON)
    → C++ N-API(DisposeString 纪律)
    → ArkTS Canvas + DisplaySync 帧循环 + 缓动复刻
    → ArkUI 真机三页验收(联动拖拽 / 流动 / 换向 / 退化 / 增量 / 分隔线)

9.2 封装层次

  • 内核层:sampleWave 波形采样、WaveDirection 流向系数、WavySliderDefaults 默认值(公式与上游一比一);
  • 隔离层:上游 M2/M3 composable、Core.kt、AOSP.kt、截图测试原样保留在 composeMain / composeDesktopTest,不参与构建;
  • Native 层:生成双 ABI 动态库并通过 3 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理帧循环、三段缓动、Canvas 绘制、拖拽与页面状态;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 适配路线跟着库形态走:KStore 零 UI,正确答案是真编译整库 + 8 行 actual;wavy-slider 零引擎,正确答案是抽数学内核 + 隔离 UI。共同原则是"编得进去的整库编,编不进去的抽内核"——判断依据是社区制品线与上游依赖的当天的对齐程度,不是历史结论;
  2. 纯 UI 库的"内核"要靠抽取而不是翻译:sampleWave 若在 ArkTS 里重写一遍,适配就失去了意义(波形漂移无人把关)。抽取物必须用交叉验证钉住——8 项 commonTest 钉住平波退化、周期性、增量包络与默认值,ohos .so 与 JVM 跑同一份代码;
  3. 交互归原生,算法归 Kotlin:波浪滑块的价值在"波长得像上游",而不在"ArkTS 里也有一个滑块"。帧循环、缓动、手势是 UI 时序,放 ArkTS 不损失保真度;波形每个点过桥计算,才保证 Demo 展示的是库的行为而不是重写的行为。

9.4 适配成果

当前适配已完成:

  • wavy-slider 2.2.0 波形数学内核的双 ABI Kotlin/Native 编译(sampleWave 一比一复刻上游公式,新增 commonTest 8 项交叉验证全绿);
  • 3 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 三页验收 Demo(WavySliderView 组件:Canvas 绘制、DisplaySync 帧循环、三段缓动、四方向切换、拖拽联动、平波退化、增量模式、静态分隔线、默认值对账);
  • HAP 构建(双 ABI 六件套 so,约 6.7MB)、签名、设备安装和真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐