开源鸿蒙平台 CMP 三方库「compose-progressIndicator-multiplatform」适配全流程
本文记录
compose-progressIndicator-multiplatform接入 OpenHarmony 的完整过程,覆盖 UI 组件库盘点、绘制几何下沉、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、软件光栅化与 PNG 通道、ArkUI 真机页面、HAP 构建和真机验收。
compose-progressIndicator-multiplatform是基于 Compose Multiplatform 的自定义线性进度指示器库:在 MaterialLinearProgressIndicator的基础上补齐圆角、末端 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 | 本次光栅化画布的像素尺寸。 |
bytes | PNG 字节数(真机状态行的数据来源)。 |
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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向本机 JDK 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 钉死;2.2.21 旧线要求的稳定版本) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.progressindicator |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 文件 | 依赖形态 | 处置 |
|---|---|---|
SimpleProgressIndicator.kt | @Composable × 2 + Canvas + animateFloatAsState + progressSemantics | 整体挪 composeMain(上游原件,零改动) |
SimpleProgressIndicatorDefaults.kt | AnimationSpec / spring | 整体挪 composeMain |
DrawScopeExt.kt | DrawScope / 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 方法 | 覆盖内核 |
|---|---|---|
ProgressIndicatorRender | progressindicatorRender(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.toml | Kotlin 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.kt | 1 个 C ABI、MiniJson、错误通道 |
nativeApp/.../Rasterizer.kt | 软件光栅化(调用 KLIB 几何) |
nativeApp/.../PngEncoder.kt | stored-zlib PNG |
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 验收页面(含 Spring 数值积分) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(21 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 几何 | ProgressIndicatorGeometry.barWidth | 进度条宽 = 画布宽 × progress(钳制后) |
| 几何 | ProgressIndicatorGeometry.thumbCenterX/Y | thumb 圆心(回退 thumbOffset / 垂直居中) |
| 几何 | ProgressIndicatorGeometry.shouldDrawThumb | 圆心为正才画(上游同款判定) |
| 几何 | ProgressIndicatorDefaults | progress 0.7 / 红条 / 0xFFFBE8E8 轨道 / 白 thumb |
| 上游 Compose | SimpleProgressIndicator(WithAnim) | 隔离在 composeMain,鸿蒙不编译(对照面) |
| 光栅化 | Rasterizer.bar(...) | track → bar → thumb 三步绘制到 IntArray |
| Native | ProgressIndicatorRender | C ABI JSON 导出 |
| N-API | progressindicatorRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | Spring.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 |
thumbCenterRetreatsByOffset | thumb 圆心 = 进度条宽 − thumbOffset(1000×0.5−6=494) |
thumbHiddenWhenCenterNotPositive | progress=0 时圆心为负、不画 thumb |
thumbIsVerticallyCentered | thumb 垂直居中(高 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 功能用例
- 启动后四行指示器全部出图:ArkUI 原生 Progress 对照行、无动画默认参数行、自定义色 VeryLow 弹簧行、默认色 Low 弹簧行;
- 状态行显示
progress=0.50,信息行显示png=宽x高 bytes=N(来自真实 Native 渲染,非静态文字); - 拖动 Slider:无动画行即时跳变,两根动画行弹簧追踪;
- Decrease / Increase:步进 0.1,钳在 0…1;
- 点随机:一次大跨度跳变,VeryLow 弹簧明显"慢而稳"(约 1s 收敛)、Low 弹簧更快到位;
- thumb 圆心始终在进度条末端回退一个
thumbOffset处;progress=0时无 thumb(圆心非正不画); cornerRadius = 15vp大于高度一半(6vp)时,进度条呈完整胶囊形——与上游 Skia 钳制语义一致;- 弹簧收敛后动画定时器停止(无残留后台调用)。
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 踎坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让
:progressIndicator真正编译成ohosArm64/ohosX64——哪怕是"只剩几何"的 UI 库也要有 KLIB 产物,否则渲染公式就是两份; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游 1.9 时代的
targetHierarchy.default()+ 手写iosMain dependsOn链在鸿蒙 Kotlin 2.2.21 上报错,必须换applyDefaultHierarchyTemplate(); src/composeMain是"停车位"不是源集:只要它不出现在任何 build 脚本里,ohos 编译期就永远碰不到androidx.compose;一旦手痒把它注册回去,sync 立刻失败;- 本机没有 Adoptium JDK 21 时,
org.gradle.java.home可以直接指向 Gradle 工具链下载的 JBR 21(~/.gradle/jdks/...),但gradlew.bat启动仍需先export JAVA_HOME——wrapper 启动 Java 与 Gradle 运行 Java 是两回事; - DevEco 打开
example/harmonyApp后生成的.idea/、.clangd、clangd 缓存数据库会混进提交——根.gitignore的.idea是根锚定模式盖不住嵌套目录,必须单独追加example/harmonyApp/.idea/等条目; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,模拟器是 x86_64,linkDebugSharedOhosX64不能省,abiFilters保持双 ABI; - ArkTS 不能
import 'libohosprogressindicator.so'——HarmonyOS CAdapter 不会把@CName业务符号挂给 ArkTS,必须走 CMake/NAPI 包装层; - IMPORTED so 的 NEEDED 会记录绝对路径(Windows 盘符),设备上 rpath 解析失败——
IMPORTED_NO_SONAME TRUE+$ORIGINrpath + POST_BUILD 拷 so 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 弹簧动画逐帧调桥时,画布像素宽度要按真机物理像素注入(
display+vp2px),否则 PNG 会被Image拉伸模糊、thumb 位置失真; - 生成 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,桥退化为演示路径; - 增加
ohosArm64release 裁剪与 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 三条经验
- UI 库适配的下沉物是"模型"而不是"代码":进度指示器没有编码器、没有存储引擎,可下沉的最大公约数是几何公式与默认值。判断标准很简单——把
@Composable壳剥掉后还剩什么,剩的就是 KLIB 的内容;什么都剩不下时才考虑纯 ArkTS 复刻路线; - "两份几何"是 UI 库适配最大的漂移风险:光栅化器必须调用 KLIB 里的同一份
ProgressIndicatorGeometry,而不是自己再写一遍公式——桥只做参数钳制与像素填充,公式永远只有一个出处,交叉验证才有意义; - 动画语义用数值积分复刻,验收看特征等价:
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。
参考文档
更多推荐

所有评论(0)