本文记录 compose-progressIndicator-multiplatform 接入 OpenHarmony 的完整过程,覆盖 UI 组件库盘点、绘制几何下沉、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、软件光栅化与 PNG 通道、ArkUI 真机页面、HAP 构建和真机验收。

compose-progressIndicator-multiplatform 是基于 Compose Multiplatform 的自定义线性进度指示器库:在 Material LinearProgressIndicator 的基础上补齐圆角、末端 thumb 与进度变化弹簧动画,开发者可自定义 thumb 的大小、位置与颜色,Android / iOS / Desktop 三端共享同一份 commonMain 代码。本次适配与存储类库的"真编译整库"路线(KStore)有本质区别:上游的全部资产都是 @Composable 函数与 DrawScope 扩展,站在 androidx.compose 上,而社区鸿蒙 CMP 是 1.9.2、上游是 Compose 1.5.1 / Kotlin 1.9.10,版本对不齐——因此提取的是绘制几何模型(进度条宽度 = 画布宽 × progress、thumb 圆心 = 进度条宽 − thumbOffset、thumb 仅在圆心为正时绘制)与默认配色,编成 OpenHarmony KLIB;Compose 组件原样隔离在 src/composeMain 不参与鸿蒙编译;HarmonyOS 侧用同一几何做软件光栅化出透明 PNG,ArkUI 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/compose-progressIndicator-multiplatform.git

上游仓库: KevinnZou/compose-progressIndicator-multiplatform

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

compose-progressIndicator-multiplatform 为 Compose Multiplatform 应用提供"一条进度指示器"这一件事的完整答案:SimpleProgressIndicator 一行出条,progress / progressBarColor / cornerRadius / trackColor / thumbRadius / thumbColor / thumbOffset 七个参数覆盖全部视觉自由度,SimpleProgressIndicatorWithAnim 在此之上用 animateFloatAsState + 可替换的 AnimationSpec 让进度变化带上弹簧动画。实现层只有三个文件:两个 @Composable 入口、一个默认 AnimationSpec 对象、三个 internal 的 DrawScope 扩展(轨道圆角矩形、进度圆角矩形、thumb 圆),纯 Canvas 绘制、零 expect/actual、零平台 API。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套指示器能力,但上游构建(Kotlin 1.9.10 / Gradle 8.2.1 / AGP 8.0.2 / Compose 1.5.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示进度条,却无法证明公共绘制模型在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证几何模型、光栅化输出、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 1.9.10 / Gradle 8.2.1 / AGP 8.0.2 与鸿蒙工具链无法共存于一个 Gradle 构建;targetHierarchy.default() 加手写 iosMain dependsOn 的 1.9 时代写法在鸿蒙 Kotlin 上直接报错。
Compose 依赖对不齐全部三个源文件 import androidx.compose,社区鸿蒙 CMP 1.9.2 与上游 Compose 1.5.1 的编译器/运行时无法混编,sync 阶段就会失败。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
动画语义跨不了桥animateFloatAsState / AnimationSpec 是 Compose runtime 概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:绘制几何与默认值的 ohos 编译(下沉为纯 Kotlin)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。绘制结果与 Android/iOS/Desktop 逐像素一致由"几何公式逐行取自上游 Canvas 实现"保证,ArkTS 只负责调用 Native、解码 PNG 和展示结果。

1.2 库提供的能力

Demo 提供一个真实的进度指示器会话,对应桥接的全部入口:

  • 渲染(render):ProgressIndicatorRender 把一份像素参数(画布宽高、圆角、三组颜色、thumb 半径与偏移)经几何模型计算后光栅化成透明背景 PNG——轨道全尺寸圆角矩形、进度条按比例宽的圆角矩形、thumb 圆心在进度条末端回退一个 thumbOffset 处,绘制顺序与上游 Canvas 完全一致;
  • 参数默认值:不传参时对齐上游 Compose API 缺省(progress=0.7、红条 #FF0000、轨道 #FBE8E8、白 thumb、thumbOffset = thumbRadius);
  • 越界钳制:progress 越界钳回 0…1,cornerRadius 按 Skia 语义钳到短边一半(所以圆角 ≥ 高度一半时自然呈胶囊形,与上游一致);
  • 版本与握手:kind 字段回显 "progress",响应携带真实渲染的宽高与 PNG 字节数。

响应包含:

字段作用
ok渲染是否成功,失败时 error 带原文。
kind固定 "progress",标识渲染通道。
width / height本次光栅化画布的像素尺寸。
bytesPNG 字节数(真机状态行的数据来源)。
pngBase64透明背景 RGBA PNG 的 Base64 文本。

上游几何语义(barWidth、thumbCenterX、thumbCenterY、shouldDrawThumb、进度钳制、三组默认色)全部随 ProgressIndicatorGeometry / ProgressIndicatorDefaults 编进 .so,JVM 侧保留 5 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的绘制几何没有漂移。

1.3 实现适配

维度要求
代码复用绘制几何与默认值从上游 SimpleProgressIndicator.kt 逐行下沉为纯 Kotlin(commonMain 唯一源文件);三个 Compose 文件 git mv 到 src/composeMain 原样保留(100% rename,零内容改动)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);sample/*(Android/iOS/Desktop 示例)留盘不进构建。
桥接稳定使用 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
渲染一致光栅化的全部几何量来自 KLIB 内的 ProgressIndicatorGeometry,光栅化器不私藏公式;透明背景、像素中心采样、半径钳制对齐 Skia 语义。
UI 完整单页覆盖全部参数面:四行指示器对照(原生 Progress / 无动画 / 两种弹簧动画)、Slider、Decrease / Increase / 随机按钮与状态行。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。SimpleProgressIndicator / SimpleProgressIndicatorWithAnim 在鸿蒙侧不运行;ArkTS Demo 只是把桥暴露的渲染闭环画出来,动画由 ArkTS 侧的弹簧数值积分驱动。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点源码依赖度(3 文件全 Compose)、确定"几何下沉"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:桥协议建模     ── 像素参数 JSON 契约、PNG-Base64 响应、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 四行指示器、弹簧数值积分、Slider 与按钮、状态行
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
progressIndicator/                    上游进度指示器库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
progressIndicator/src/commonMain/     新增:ProgressIndicatorGeometry + ProgressIndicatorDefaults(几何核心,唯一 klib 源码)
progressIndicator/src/composeMain/    上游三个 Compose 文件 git mv 而来(不注册源集,鸿蒙不编译)
progressIndicator/src/commonTest/     新增:5 项几何交叉验证
example/nativeApp/                    Kotlin/Native 动态库(Bridge + Rasterizer + PngEncoder)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
sample/                               上游 Android/iOS/Desktop 示例,留盘不进 settings

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :progressIndicator 与 :example:nativeApp,rootProject.name = "compose-progressIndicator-multiplatform"。上游的 AGP / Compose / vanniktech 发布插件、foojay resolver、JetBrains Space dev 仓库全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 钉死;2.2.21 旧线要求的稳定版本)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.progressindicator
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
文件依赖形态处置
SimpleProgressIndicator.kt@Composable × 2 + Canvas + animateFloatAsState + progressSemantics整体挪 composeMain(上游原件,零改动)
SimpleProgressIndicatorDefaults.ktAnimationSpec / spring整体挪 composeMain
DrawScopeExt.ktDrawScope / drawRoundRect / drawCircle整体挪 composeMain
(新)ProgressIndicatorGeometry.kt纯 Kotlin,零依赖下沉进 commonMain,唯一 klib 源码

expect/actual 声明全库为零,androidMain 只有一个空 Manifest——上游是 100% common 代码,这对适配是好消息:没有平台缝隙要补,难点全部集中在"Compose 依赖怎么隔离"。答案是拆成两半:公式进 commonMain,调用公式的 Compose 壳进 composeMain。下沉的核心长这样:

// progressIndicator/src/commonMain/.../ProgressIndicatorGeometry.kt
public object ProgressIndicatorGeometry {
    public fun coerceProgress(progress: Float): Float = progress.coerceIn(0f, 1f)
    public fun barWidth(canvasWidth: Float, progress: Float): Float =
        canvasWidth * coerceProgress(progress)
    public fun thumbCenterX(canvasWidth: Float, progress: Float, thumbOffsetPx: Float): Float =
        barWidth(canvasWidth, progress) - thumbOffsetPx
    public fun thumbCenterY(canvasHeight: Float): Float = canvasHeight / 2f
    public fun shouldDrawThumb(canvasWidth: Float, progress: Float, thumbOffsetPx: Float): Boolean =
        thumbCenterX(canvasWidth, progress, thumbOffsetPx) > 0f
}

每一条都逐行对应上游 SimpleProgressIndicator 的 Canvas 绘制序列——val progressWidth = size.width * progress、thumbCenter = progressWidth - thumbOffset.toPx()、if (thumbCenter > 0) drawThumb(...),几何语义没有发明任何新东西。

1.4 页面边界
标题区          PROGRESSINDICATOR · OPENHARMONY(CMP 进度指示器库 · Kotlin/Native ohosArm64/ohosX64)
对照行          ArkUI 原生 Progress(Linear)——对应上游 sample 里的 Material LinearProgressIndicator 参照行
指示器行 1      SimpleProgressIndicator(无动画 · 默认参数)——跟随 Slider 即时跳变
指示器行 2      WithAnim(自定义色 · StiffnessVeryLow 弹簧)——黄条 / 紫红 thumb / 15vp 圆角
指示器行 3      WithAnim(默认色 · StiffnessLow 弹簧)——红条 / 白 thumb / 15vp 圆角
控制区          progress Slider(0..100)+ Decrease / Increase / 随机 三按钮
状态行          当前 progress 值 + 本次 PNG 尺寸与字节数

页面启动时按屏幕宽度注入画布像素宽(display.getDefaultDisplaySync().width - 边距),高度 / 圆角 / thumb 半径经 vp2px 换算成物理像素。

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

2.1 工具链钉版(为什么是 2.2.21-1.0.0)

本库没有 kotlinx 依赖矩阵要迁就(不依赖 coroutines / serialization / io),适配钉版直接采用社区标准钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同款),对应 Gradle 钉 8.14.1。上游的 Gradle 8.2.1 wrapper 连 jar 带脚本一起替换。这里与 KStore 的 2.4.20 线是两条经验:有 kotlinx 制品依赖时版本跟着依赖走,纯 UI 库直接用社区基准钉法。

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 = "compose-progressIndicator-multiplatform"

include(":progressIndicator")
include(":example:nativeApp")

新建 gradle/libs.versions.toml,全仓只留一个插件别名:

[versions]
kotlin = "2.2.21-1.0.0"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.2 编译几何核心

progressIndicator/build.gradle.kts 从 87 行(四类目标 + Android 配置 + 发布段)收敛为纯 KMP 模块:

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

kotlin {
    applyDefaultHierarchyTemplate()      // 取代 1.9 时代的 targetHierarchy.default() + 手写 iosMain 链
    jvmToolchain(21)
    jvm()                                // 交叉验证用
    ohosArm64()
    ohosX64()

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

实测:

./gradlew :progressIndicator:compileKotlinJvm     # BUILD SUCCESSFUL
./gradlew :progressIndicator:jvmTest              # 5 项交叉验证全绿

上游的 Compose 插件、compose.runtime/foundation/material 依赖、iOS framework、vanniktech 发布段全部随重写剥离;根构建收敛为 13 行(group / version + 全局禁用 Ohos*Test* 任务)。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.multiplatform.progressindicator/**(几何核心 + 默认值,原样编译)
        ↓ ProgressIndicatorGeometry / ProgressIndicatorDefaults
Bridge.kt(@CName × 1,扁平 JSON 进,PNG-Base64 出)
        ↓ Rasterizer(IntArray 画布)→ PngEncoder(stored zlib)
N-API progressindicatorRender()
        ↓
ArkUI Index.ets(Base64 → PixelMap → Image)

请求与响应:

请求 : {"progress":0.5,"width":1080,"height":42,"cornerRadius":21,
        "progressBarColor":"#FFFF0000","trackColor":"#FFFBE8E8",
        "thumbRadius":18,"thumbColor":"#FFFFFFFF","thumbOffset":18}
响应 : {"ok":true,"kind":"progress","width":1080,"height":42,"bytes":N,"pngBase64":"..."}
       {"ok":false,"error":"..."}

全部字段可选:缺省值就是上游 Compose API 的默认值,thumbOffset 缺省取 thumbRadius(复刻上游 thumbOffset: Dp = thumbRadius 的默认引用语义)。颜色走 #RRGGBB / #AARRGGBB 文本,6 位自动补不透明 alpha。

3.2 渲染载荷为什么固定 PNG

桥不把画布、路径或像素缓冲传给 ArkTS——那需要在 C ABI 上发明一套生命周期协议。一张透明背景 RGBA PNG(Base64 文本)就是渲染结果的完整、自描述、无状态快照:ArkTS 侧 util.Base64Helper + image.createImageSource + createPixelMap 三行解码,Image 组件直接显示。透明背景对齐 Compose Canvas 的默认行为,叠在任意底色上与上游一致。PNG 用 stored zlib 块(未压缩)输出:Kotlin/Native 没有 java.util.zip,进度条图片只有百 KB 级,用字节换依赖简单,桥内自带 CRC32 / Adler32 实现,零平台依赖。

3.3 错误通道

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

@CName("ProgressIndicatorRender")
fun progressIndicatorRender(request: String): String = try {
    /* 参数钳制 → Rasterizer.bar(...) → ok(image) */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、颜色串不合法、尺寸越界(宽 8…4096、高 2…512、圆角钳到短边一半)全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libprogressindicator_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosprogressindicator.so)
  │ ProgressIndicatorRender
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复刻几何公式(两份实现必漂移),所以选择 C ABI + JSON + PNG。

4.2 Kotlin/Native 导出函数
@CName("ProgressIndicatorRender")
fun progressIndicatorRender(request: String): String   // 像素参数进,PNG-Base64 JSON 出
C ABI 符号ArkTS 方法覆盖内核
ProgressIndicatorRenderprogressindicatorRender(req)参数钳制 + ProgressIndicatorGeometry 全部几何 + 光栅化 + PNG
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"progressindicatorRender", nullptr, NapiProgressIndicatorRender, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

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

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

add_library(progressindicator_napi SHARED napi_init.cpp)
target_link_libraries(progressindicator_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosprogressindicator)

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

export const progressindicatorRender: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 弹簧动画的数值积分

上游 SimpleProgressIndicatorWithAnim 的默认动画是 spring(dampingRatio = DampingRatioNoBouncy, stiffness = StiffnessLow),示例里另一个用 StiffnessVeryLow。animateFloatAsState 依赖 Compose runtime,跨不了桥——ArkTS 侧用等价的临界阻尼弹簧积分复刻:

// a = -k(x - target) - 2*sqrt(k)*v,NoBouncy 即阻尼比 1
class Spring {
  step(dt: number, stiffness: number): boolean {
    const damping = 2 * Math.sqrt(stiffness);
    const a = -stiffness * (this.value - this.target) - damping * this.velocity;
    this.velocity += a * dt;
    this.value += this.velocity * dt;
    return Math.abs(this.target - this.value) < 0.0005 &&
           Math.abs(this.velocity) < 0.0005;   // 收敛即停
  }
}

两根指示器各挂一个 Spring(StiffnessVeryLow = 50 / StiffnessLow = 200,即 Compose Spring 常量),进度变化时 setInterval 30 fps 步进,每帧对两根动画条各调一次 progressindicatorRender 重出 PNG——@State 的 PixelMap 更新驱动界面刷新,收敛即 clearInterval。30 fps 对进度条足够顺滑,且把每帧的 JSON+PNG 跨界流量减半。

5.2 页面交互

单页自上而下:标题区、ArkUI Progress 对照行、三行指示器(Image(PixelMap) 显示 PNG)、progress Slider(0…100)、Decrease / Increase / 随机三按钮、状态行与信息行。Decrease / Increase 步进 0.1 并钳在 0…1(对齐上游示例按钮逻辑);随机按钮一次大跨度跳变,两根弹簧同时收敛,动画差异肉眼可辨。每行指示器下方标注对应的上游 API 与弹簧参数,验收时可对照。

5.3 像素换算与错误边界

画布宽度用 display.getDefaultDisplaySync().width - vp2px(32) 注入真机物理像素,高度 / 圆角 / thumb 半径经 vp2px 换算(生成 vp2px(12) 高、显示 .height(12),1:1 映射不缩放)。ok:false 时错误原文进状态行;Native 调用本身抛错同样被 try/catch 收进状态行,动画定时器在 aboutToDisappear 清理。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosprogressindicator)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       ├── Bridge.kt                         @CName × 1 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
│       ├── Rasterizer.kt                     进度条软件光栅化(几何全部来自 KLIB)
│       └── PngEncoder.kt                     stored-zlib PNG(RGBA,自带 CRC/Adler)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.progressindicator、versionName 1.3.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/libohosprogressindicator_api.h
        ├── src/main/cpp/types/libprogressindicator_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :progressIndicator:jvmTest                                     # 5 项交叉验证
./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.progressindicator -m entry

四、完整代码对照

4.1 整体架构

com.multiplatform.progressindicator/**(几何核心原样编译)
    │ ProgressIndicatorGeometry / ProgressIndicatorDefaults
Bridge.kt + MiniJson
    │ 参数钳制 + parseColor
Rasterizer.kt
    │ IntArray 画布:track 圆角矩形 → bar 圆角矩形 → thumb 圆
PngEncoder.kt
    │ stored zlib PNG(RGBA 透明背景)
libprogressindicator_napi.so
    │ progressindicatorRender
Index.ets
    │ Base64 → PixelMap + Spring 数值积分
    ▼
ArkUI 真机页面(单页滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、sample/* 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 钉版 + 唯一插件别名
build.gradle.kts(根)版本 1.3.0-ohos.1、全局禁用 Ohos*Test* 任务
progressIndicator/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64
progressIndicator/src/commonMain/.../ProgressIndicatorGeometry.kt唯一 klib 源码(几何 + 默认值)
progressIndicator/src/composeMain/...上游三个 Compose 文件(git mv,100% rename)
progressIndicator/src/commonTest/...5 项几何交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、MiniJson、错误通道
nativeApp/.../Rasterizer.kt软件光栅化(调用 KLIB 几何)
nativeApp/.../PngEncoder.ktstored-zlib PNG
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(含 Spring 数值积分)
README.OpenHarmony_CN.md鸿蒙适配主文档(21 节)

4.3 关键 API 对照

层次API作用
几何ProgressIndicatorGeometry.barWidth进度条宽 = 画布宽 × progress(钳制后)
几何ProgressIndicatorGeometry.thumbCenterX/Ythumb 圆心(回退 thumbOffset / 垂直居中)
几何ProgressIndicatorGeometry.shouldDrawThumb圆心为正才画(上游同款判定)
几何ProgressIndicatorDefaultsprogress 0.7 / 红条 / 0xFFFBE8E8 轨道 / 白 thumb
上游 ComposeSimpleProgressIndicator(WithAnim)隔离在 composeMain,鸿蒙不编译(对照面)
光栅化Rasterizer.bar(...)track → bar → thumb 三步绘制到 IntArray
NativeProgressIndicatorRenderC ABI JSON 导出
N-APIprogressindicatorRender(req)向 ArkTS 暴露能力
ArkTSSpring.step(dt, stiffness)NoBouncy 弹簧数值积分(30 fps)

4.4 ArkTS 与 Kotlin 的边界

const raw = progressindicatorRender(JSON.stringify({
  progress: 0.7, width: screenWidthPx, height: Math.round(vp2px(12)),
  cornerRadius: Math.round(vp2px(15)),
  progressBarColor: '#FFFFFF00', trackColor: '#FFFBE8E8',
  thumbRadius: Math.round(vp2px(6)), thumbColor: '#FFFF00FF'
}));
const res = JSON.parse(raw);
// res.ok === true;res.pngBase64 → Base64Helper → createImageSource → PixelMap → Image
val image = Rasterizer.bar(
    width = width, height = height, progress = progress,
    cornerRadius = cornerRadius,                      // 已钳到 min(w,h)/2
    progressBarColor = progressBarColor, trackColor = trackColor,
    thumbRadius = thumbRadius, thumbColor = thumbColor, thumbOffset = thumbOffset,
)
ok(image)                                             // {"ok":true,...,"pngBase64":"..."}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象、画布状态或未校验的动态结构。

五、关键决策说明

决策 1:几何下沉而不是整库编译

上游三个文件全部站在 androidx.compose 上,没有一行可以原样编进 ohos KLIB——这与 KStore"真编译整库"的前提(纯 Kotlin 引擎 + kotlinx 制品线)根本不同。能复用的最大公约数是绘制几何模型与默认值,于是把它抽成 commonMain 纯 Kotlin,其余整体隔离进 composeMain。公式逐行取自上游 Canvas 实现,光栅化器只调用不下场,"两份几何"的风险被压到零。

决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1(社区基准钉法)

本库零 kotlinx 依赖,不需要为制品线反推版本,直接采用社区已多次过审的钉法(qrose / haze 同款)。Gradle 从上游 8.2.1 升到 8.14.1 是 2.2.21 线的配套要求,wrapper 连 jar 带脚本一起替换。

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

上游根构建(AGP / Compose / vanniktech 发布 / foojay / Space dev 仓库)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample/androidApp、sample/desktopApp、sample/shared、sample/iosApp 是上游遗留,留盘不进 settings.gradle.kts——鸿蒙 Demo 的宿主是 example/harmonyApp。

决策 4:PNG 通道而不是直接操作 PixelMap

Kotlin 侧保持零鸿蒙 SDK 依赖(KN 只用标准库 + libc),桥协议稳定为 string → string,引擎差异全部隔离在 NAPI 层之外;PNG 是自描述快照,ArkTS 三行解码。代价是每帧一次跨界传输,用 30 fps 与百 KB 级画布把代价压在可接受范围——这是 UI 库 Demo 的正确性价比,不是高性能渲染方案。

决策 5:jvmTest 交叉验证 + 动画 ArkTS 数值积分

几何逻辑在 commonMain,JVM 与 ohos 跑同一份代码;5 项 jvmTest 断言钉住"公式没漂"。动画不属于几何,AnimationSpec 无法下沉,ArkTS 用同参数(NoBouncy / 50 / 200)的临界阻尼积分复刻运动特征——验收看的是动画语义等价,不是把 Compose 动画系统搬过来。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :progressIndicator:jvmTest

5 项用例全部通过(新增测试文件,断言值手算自上游公式):

测试用例钉住什么
barWidthScalesWithProgress进度条宽随 progress 线性缩放(1000×0.25=250)
progressIsCoerced越界进度钳回 0…1
thumbCenterRetreatsByOffsetthumb 圆心 = 进度条宽 − thumbOffset(1000×0.5−6=494)
thumbHiddenWhenCenterNotPositiveprogress=0 时圆心为负、不画 thumb
thumbIsVerticallyCenteredthumb 垂直居中(高 48 → y=24)

这些断言同时是光栅化器的行为契约:ohos .so 里执行的几何与上游 Compose 实现一致。

6.3 HAP 验证

:progressIndicator:compileKotlinJvm: BUILD SUCCESSFUL
:progressIndicator:jvmTest: 5 项全绿
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ProgressIndicatorRender / libohosprogressindicator_symbols 均为全局 T 符号
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/<abi>/
BuildNativeWithCmake: finished(双 ABI 编出 libprogressindicator_napi.so)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

debugShared 的 KN so arm64 约 3.7 MB、x86_64 约 3.4 MB(含 Kotlin/Native 运行时),配 libc++_shared.so(约 1.85 / 1.73 MB)各三件套经 copyKnSo* 回填 entry/libs/<abi>/。

6.4 功能用例

  1. 启动后四行指示器全部出图:ArkUI 原生 Progress 对照行、无动画默认参数行、自定义色 VeryLow 弹簧行、默认色 Low 弹簧行;
  2. 状态行显示 progress=0.50,信息行显示 png=宽x高 bytes=N(来自真实 Native 渲染,非静态文字);
  3. 拖动 Slider:无动画行即时跳变,两根动画行弹簧追踪;
  4. Decrease / Increase:步进 0.1,钳在 0…1;
  5. 点随机:一次大跨度跳变,VeryLow 弹簧明显"慢而稳"(约 1s 收敛)、Low 弹簧更快到位;
  6. thumb 圆心始终在进度条末端回退一个 thumbOffset 处;progress=0 时无 thumb(圆心非正不画);
  7. cornerRadius = 15vp 大于高度一半(6vp)时,进度条呈完整胶囊形——与上游 Skia 钳制语义一致;
  8. 弹簧收敛后动画定时器停止(无残留后台调用)。

6.5 验证结论

绘制几何模型、光栅化输出、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的渲染会话:四行指示器对照、参数默认值对齐、弹簧动画收敛与交互闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-progressIndicator-multiplatform.git
cd compose-progressIndicator-multiplatform
export JAVA_HOME="/path/to/jdk-21"
./gradlew :progressIndicator:compileKotlinJvm                              # klib(JVM 交叉验证)
./gradlew :progressIndicator:jvmTest                                      # 5 项几何断言
./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.progressindicator -m entry
hdc shell hilog | grep ProgressIndicator

八、遗留问题与改进方向

8.1 踎坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让 :progressIndicator 真正编译成 ohosArm64 / ohosX64——哪怕是"只剩几何"的 UI 库也要有 KLIB 产物,否则渲染公式就是两份;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 1.9 时代的 targetHierarchy.default() + 手写 iosMain dependsOn 链在鸿蒙 Kotlin 2.2.21 上报错,必须换 applyDefaultHierarchyTemplate();
  4. src/composeMain 是"停车位"不是源集:只要它不出现在任何 build 脚本里,ohos 编译期就永远碰不到 androidx.compose;一旦手痒把它注册回去,sync 立刻失败;
  5. 本机没有 Adoptium JDK 21 时,org.gradle.java.home 可以直接指向 Gradle 工具链下载的 JBR 21(~/.gradle/jdks/...),但 gradlew.bat 启动仍需先 export JAVA_HOME——wrapper 启动 Java 与 Gradle 运行 Java 是两回事;
  6. DevEco 打开 example/harmonyApp 后生成的 .idea/、.clangd、clangd 缓存数据库会混进提交——根 .gitignore 的 .idea 是根锚定模式盖不住嵌套目录,必须单独追加 example/harmonyApp/.idea/ 等条目;
  7. HAP 缺当前 ABI 的 so 会报安装失败 9568347,模拟器是 x86_64,linkDebugSharedOhosX64 不能省,abiFilters 保持双 ABI;
  8. ArkTS 不能 import 'libohosprogressindicator.so'——HarmonyOS CAdapter 不会把 @CName 业务符号挂给 ArkTS,必须走 CMake/NAPI 包装层;
  9. IMPORTED so 的 NEEDED 会记录绝对路径(Windows 盘符),设备上 rpath 解析失败——IMPORTED_NO_SONAME TRUE + $ORIGIN rpath + POST_BUILD 拷 so 三件套缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. 弹簧动画逐帧调桥时,画布像素宽度要按真机物理像素注入(display + vp2px),否则 PNG 会被 Image 拉伸模糊、thumb 位置失真;
  12. 生成 PNG 高度用 vp2px(12)、显示 Image 高度用 12(vp),1:1 映射不缩放——两边单位搞混是 Demo 高度异常的常见原因。

8.2 已知问题

  • Compose 组件本体(@Composable)在鸿蒙不可运行,消费路径是 PNG 桥或 KLIB 几何;
  • 一次调用一张 PNG,不适合高频大画布场景(60 fps 的超宽画布不是目标);
  • animationSpec 自定义弹簧 / 关键帧只在 Demo 里以数值积分近似(NoBouncy 临界阻尼),visibilityThreshold 等细节未逐参数对齐;
  • 光栅化是像素中心采样的无抗锯齿实现,极小半径 thumb 的边缘与 Skia 抗锯齿渲染可能有 1px 级差异;
  • progressSemantics(无障碍语义)没有对应物,桥只管视觉;
  • android / ios / desktop 目标剥离(源文件留在树里),Maven Central 发布剥离,本 fork 不发坐标;
  • debug 版 so 未裁剪体积,release strip 未开启。

8.3 未来优化方向

  • 等社区 CMP 工具链对齐后,把 composeMain 挪回 commonMain,桥退化为演示路径;
  • 增加 ohosArm64 release 裁剪与 so 瘦身;
  • 评估 .har 形态发布(NAPI so + d.ts 打包),让 ArkTS 工程直接依赖;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 progressIndicator(3 文件全 Compose)
    → 源码依赖度盘点:几何可下沉、Compose 必须隔离
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1,社区基准钉法)
    → 根构建替换(cmp_qrose 模式,sample 留盘)
    → 几何下沉 commonMain + Compose 文件 git mv 到 composeMain
    → ohosArm64 / ohosX64 KLIB + 5 项 jvmTest 断言
    → C ABI(@CName × 1)+ PNG-Base64 载荷
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + 弹簧数值积分
    → ArkUI 真机单页验收(四行对照 / Slider / 按钮 / 动画收敛)

9.2 封装层次

  • 几何层:上游原版绘制公式(barWidth / thumbCenter / 显隐判定)与默认配色,纯 Kotlin;
  • 光栅化层:IntArray 画布上的三步绘制(track → bar → thumb),透明背景、Skia 语义半径钳制;
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON+PNG;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、Spring 数值积分、Base64 → PixelMap 解码与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. UI 库适配的下沉物是"模型"而不是"代码":进度指示器没有编码器、没有存储引擎,可下沉的最大公约数是几何公式与默认值。判断标准很简单——把 @Composable 壳剥掉后还剩什么,剩的就是 KLIB 的内容;什么都剩不下时才考虑纯 ArkTS 复刻路线;
  2. "两份几何"是 UI 库适配最大的漂移风险:光栅化器必须调用 KLIB 里的同一份 ProgressIndicatorGeometry,而不是自己再写一遍公式——桥只做参数钳制与像素填充,公式永远只有一个出处,交叉验证才有意义;
  3. 动画语义用数值积分复刻,验收看特征等价:AnimationSpec 跨不了 C ABI,但 NoBouncy + stiffness 的运动方程是纯数学,ArkTS 侧同参数积分即可得到同款收敛特征。UI 库 Demo 的验收重点落在"对照行 + 参数默认值 + 动画特征"三件套上,与存储库的"重启后数据还在"互为镜像。

9.4 适配成果

当前适配已完成:

  • compose-progressIndicator-multiplatform 1.3.0 绘制几何的双 ABI Kotlin/Native 编译(上游 Compose 文件零改动隔离,几何公式逐行下沉);
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • 软件光栅化 + stored-zlib PNG 通道(透明背景,与 Compose Canvas 行为一致);
  • DevEco Stage ArkUI 单页验收页面(四行指示器对照、Slider、Decrease / Increase / 随机、弹簧动画收敛、状态行);
  • 5 项 JVM 交叉验证全绿 + 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐