开源鸿蒙平台 CMP 三方库「colorpicker-compose」适配全流程
本文记录
colorpicker-compose接入 OpenHarmony 的完整过程,覆盖 CMP 取色器库盘点、ohosArm64/ohosX64双目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建、签名和真机验收。
colorpicker-compose是以 ComposeCanvas/Brush为绘制核心的 Compose Multiplatform 取色器库。本次适配把它的 HSV 颜色模型与色轮几何抽成纯 Kotlin 公共实现进入 OpenHarmony ARM64 运行时,ArkUI 页面作为真机验收宿主,光栅化色轮与四类滑条全部由 Kotlin/Native 侧按上游渐变语义生成 PNG,ArkTS 只负责手势、解码和展示。
项目地址: AtomGit/oh-tpc/colorpicker-compose
上游仓库: skydoves/colorpicker-compose
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
colorpicker-compose(上游 skydoves/colorpicker-compose 1.2.0,Apache-2.0)为 CMP 应用提供 HsvColorPicker 色轮、ImageColorPicker 图像取色、Hue/Saturation/Brightness/Alpha 四种滑条和 AlphaTile 透明度棋盘格,回调统一走 ColorEnvelope(颜色 + hexCode + fromUser)。这些 Composable 在 Android、iOS、桌面、Web 上共享同一份 commonMain 实现。
上游工具链是 Compose 1.12 / Kotlin 2.4.20 / AGP 9.4 / Gradle 9.7.1;而 OpenHarmony 侧社区 CMP 栈是 CMP 1.9.2 + HarmonyOS Kotlin 2.2.21-1.0.0,两套工具链无法混编——HsvColorPicker 这类 @Composable 函数不可能直接编进 ohosArm64。但一个取色器真正值钱的是颜色模型与渐变语义:Color.toHSV() 的转换公式、Color.hexCode 的 AARRGGBB 格式、色轮的 sweep/radial 渐变 stops、滑条的渐变色表。把这些完整带到 OpenHarmony,才是「适配」而不是「重写」。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64()/ohosX64() 才能生成 OpenHarmony KLIB 与动态库。 |
| 工具链不一致 | 上游 Compose 1.12/Kotlin 2.4 与社区 HarmonyOS Kotlin 2.2.21 对不齐,配置期即失败。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 绘制语义复刻 | 色轮/滑条是 Compose Brush 渐变,鸿蒙侧必须按同一 stops 逐通道复刻,不能凭感觉重画。 |
| 源码集混杂 | 上游 23 个文件全部依赖 androidx.compose,模型与 UI 没有分层,必须拆源码集。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在 KMP 目标配置、纯模型抽取、KN 光栅化桥接和 ArkUI 验收层。颜色语义仍由公共 Kotlin 实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
适配后桥接层提供六个入口,与上游组件一一对应:
| 入口 | 对应上游 API | 作用 |
|---|---|---|
convert | Color.hsv / Color.toHSV / hexCode | HSV↔ARGB 互转、hex 解析与格式化。 |
coordToHsv | HsvColorPicker 手势 lambda | 色轮触点 → (hue, saturation),圆外钳位。 |
hsvToCoord | hsvToCoord 扩展 | (hue, saturation) → 色轮坐标,min(cx,cy) 半径语义。 |
wheelPng | drawHsvColorGradient | 光栅化 HSV 色轮为 PNG(sweep + radial 白色饱和层)。 |
sliderPng | HueSlider 等四类滑条渐变 | 光栅化四类滑条背景,支持竖直方向与棋盘格底。 |
tilePng | AlphaTile / alphaTilePaint | 光栅化 2×2 透明度棋盘格。 |
所有渐变 stops 与色表逐一对齐上游源码:色轮 sweep 是 0.000/0.166/0.333/0.499/0.666/0.833/0.999 七档(红/品红/蓝/青/绿/黄/红),径向层是中心不透明白到边缘透明白;Hue 滑条是 [红,黄,绿,青,蓝,品红,红];Brightness 是 [黑, hsv(h,s,1)];Saturation 是 [hsv(h,0,v), hsv(h,1,v)];Alpha 是基色 alpha 0→1。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | HSV 转换、hex 格式、色轮几何公式逐行移植自上游 ColorExtensions.kt/GeometryExtensions.kt,留在 KMP commonMain。 |
| 平台目标 | 库与桥接模块增加 ohosArm64 + ohosX64,生成双 ABI 动态库。 |
| 桥接稳定 | 单个 @CName C ABI + UTF-8 JSON,不传递 Kotlin 对象地址。 |
| CMP 兼容 | composeMain 完整保留上游 23 个 Composable,等社区 compose.ui ohos 制品出现即可接回。 |
| UI 完整 | ArkUI 四 Tab 覆盖全部桥入口:色轮/滑条/棋盘格/图像取色。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。CMP 工程可以复用同一份颜色模型;当前光栅化路径是社区已过审的 Vico / Reorderable / qrose 同款交付形态。
二、实现路线图
第 1 阶段:项目初始化 ── 查重、盘点 23 个 Compose 文件与交付边界
第 2 阶段:工具链与目标打通 ── eazytec 仓库、Kotlin 2.2.21-1.0.0、ohos 双目标
第 3 阶段:颜色模型抽取 ── HsvColor / PickerGeometry 逐行移植 + JVM 交叉验证
第 4 阶段:原生桥接 ── @CName 单入口 JSON 桥、光栅化、PNG、CMake N-API
第 5 阶段:ArkUI 宿主 ── 四 Tab Demo、PixelMap 图像取色
第 6 阶段:构建与真机验证 ── 双 ABI 链接、符号验收、HAP、签名、真机效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
colorpicker-compose/ 纯颜色模型 + ohosArm64/ohosX64 目标
src/commonMain HsvColor.kt / PickerGeometry.kt
src/commonTest 模型交叉验证(jvmTest 跑)
src/composeMain 上游 23 个 Compose 文件(ohos 不编)
src/composeTest 上游 19 个测试文件(随隔离)
example/nativeApp/ Kotlin/Native 动态库(libohoscolorpicker.so)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab
demo/ benchmark/ wasmApp/ iosApp/ 上游遗留,未进 settings.gradle.kts
上游遗留目录保留在仓库中但脱离构建,buildSrc 因会被 Gradle 自动构建而移除。DevEco 工程不是根项目的 Kotlin 子模块,Gradle 和 Hvigor 可以分开执行。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| Gradle / JDK | 8.14.1 / 21 |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留对照验证) |
| DevEco product | 5.1.0(18),runtimeOS HarmonyOS |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.colorpicker |
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()
}
}
ohosArm64()/ohosX64() 只存在于 HarmonyOS Kotlin 插件,插件仓库必须先写 eazytec 再写官方源。
1.3 页面边界
标题区 colorpicker-compose · OpenHarmony
Tab 区 色轮 / 滑条 / AlphaTile / 图像取色
状态行 最近一次桥调用(kind、尺寸、PNG 字节数或错误)
内容区 色轮+指示器 / 四滑条 / 棋盘格 / 调色板图像
信息卡 #HEX 大字 + ARGB 分量 + H/S/V/A 数值 + 透明度小方块
页面启动即预生成 560px 色轮并选中红色初始位,后续全部按需生成。
第 2 阶段:工具链与目标打通
2.1 库模块只保留三目标
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm()
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
上游的 android/ios/macos/js/wasmJs 目标与 Compose 依赖全部随 composeMain 隔离拆除;sharedLib 只放 example/nativeApp,避免每个库模块各吐一份带运行时的 so。
2.2 构建双 ABI 动态库
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
:example:nativeApp:linkDebugSharedOhosX64
copyKnSoArm64/copyKnSoX64 由链接任务 finalizedBy 触发,把 libohoscolorpicker.so 与同 triple 的 libc++_shared.so 自动拷入:
example/harmonyApp/entry/libs/arm64-v8a/libohoscolorpicker.so ≈8.4 MB
example/harmonyApp/entry/libs/arm64-v8a/libc++_shared.so
example/harmonyApp/entry/libs/x86_64/libohoscolorpicker.so ≈7.6 MB
example/harmonyApp/entry/libs/x86_64/libc++_shared.so
2.3 源码集迁移
23 个上游文件 git mv 到 src/composeMain(内容零改动,保历史),上游测试 19 个迁到 src/composeTest。composeMain 不是 Kotlin 源集,ohos 编译器看不到 androidx.compose;保留它是为了对照上游 API,也是社区 CMP 工具链对齐后的接回点。
第 3 阶段:颜色模型抽取(核心)
3.1 逐行移植,不是重写
HsvColor.kt 的三块公式分别来自:
// 1) Color.toHSV() —— 上游 ColorExtensions.kt 原样搬过来,只是把 Compose Color 换成 ARGB Int
val h = if (diff == 0f) 0.0f
else if (cmax == r) (60 * ((g - b) / diff) + 360f) % 360f
else if (cmax == g) (60 * ((b - r) / diff) + 120f) % 360f
else (60 * ((r - g) / diff) + 240f) % 360f
// 2) Color.hsv() 的扇区算法 —— 与 androidx.compose.ui.graphics 同式
val c = value * saturation
val x = c * (1f - abs(hue / 60f % 2f - 1f))
val m = value - c
// 3) hexCode —— AARRGGBB 小写,通道 (channel * 255).toInt() 截断,与上游逐字节一致
fun Int.hex() = toString(16).padStart(2, '0')
PickerGeometry.kt 移植 angleToHue(-angle°+360)%360、hueToAngle(-hue rad)、hsvToCoord(半径=min(cx,cy)) 和色轮手势的 sat=min(len/radius,1) 钳位。
3.2 JVM 交叉验证
commonTest 10 个用例在 jvm() 目标跑:六个主色断言(hsvToArgb(0/120/240/300/60/180,1,1) 精确等于 Compose Red/Green/Blue/Magenta/Yellow/Cyan 的 ARGB)、fromArgb 对照上游 toHSV 语义、HSV↔ARGB 往返、hexCode 格式、fromHex 双长度解析与非法分量拒绝、几何往返。
判定底线:同一输入,JVM 侧 HsvColor 与鸿蒙桥 convert 输出必须一致——模型是同一份代码编两个目标。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ HsvColor 模型 + Rasterizer
▼
UTF-8 JSON(PNG 为 Base64)+ explicit free
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制颜色语义,所以选择 C ABI + JSON,与社区 Vico/Reorderable/qrose 路径一致。
4.2 Kotlin/Native 导出函数
@CName("ColorPickerBridge")
fun colorPickerBridge(request: String): String {
return try {
val json = MiniJson.parse(request)
when (json.str("kind")) {
"convert" -> convert(json)
"coordToHsv" -> coordToHsv(json)
"hsvToCoord" -> hsvToCoord(json)
"wheelPng" -> wheelPng(json)
"sliderPng" -> sliderPng(json)
"tilePng" -> tilePng(json)
else -> fail("unknown kind: '${json.str("kind")}'")
}
} catch (t: Throwable) {
fail(t.message ?: t.toString())
}
}
任何异常都变成 {"ok":false,"error":"..."},不让 HAP 崩。convert 出参覆盖 argb/hex/r/g/b/a/hue/sat/value,即上游 ColorEnvelope 的数据面。
4.3 光栅化:渐变语义复刻
sweep 渐变必须按 stop 做逐通道 sRGB 插值,不能按 hue 线性——否则红→品红段会从黄绿方向绕过去,与 Compose Brush.sweepGradient 完全不同:
private val sweepStops = listOf(
0.000f to 0xFFFF0000.toInt(), // Red
0.166f to 0xFFFF00FF.toInt(), // Magenta
0.333f to 0xFF0000FF.toInt(), // Blue
0.499f to 0xFF00FFFF.toInt(), // Cyan
0.666f to 0xFF00FF00.toInt(), // Green
0.833f to 0xFFFFFF00.toInt(), // Yellow
0.999f to 0xFFFF0000.toInt(), // Red
)
fun sweepArgb(dx: Double, dy: Double): Int {
// 屏幕 y 向下,atan2 天然是顺时针扫角
val fraction = ((atan2(dy, dx) / (2.0 * PI)) + 1.0) % 1.0
// …相邻 stop 间按 offset 线性 t 插值 RGB 通道
}
饱和度层是 radialGradient(白不透明 → 白透明) src-over 叠加,round(base*(1-a) + 255*a);2×2 超采样做边缘抗锯齿,圆外全透明。PNG 用 zlib stored 块零依赖编码,不链系统 zlib。
4.4 C++ N-API 与 CMake
napi_property_descriptor desc[] = {
{"colorPickerBridge", nullptr, NapiColorPickerBridge, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};
KN 返回的字符串用 libohoscolorpicker_symbols()->DisposeString 释放,不让 ArkTS 管这块内存。CMake 把 KN so 作为 IMPORTED 库包一层 libcolorpicker_napi.so,$ORIGIN rpath 保证 dlopen 就近解析,缺 so 时 FATAL_ERROR 提示先跑链接任务:
add_library(ohoscolorpicker SHARED IMPORTED)
set_target_properties(ohoscolorpicker PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
target_link_libraries(colorpicker_napi PUBLIC
libace_napi.z.so libhilog_ndk.z.so ohoscolorpicker)
ArkTS 只 import { colorPickerBridge } from 'libcolorpicker_napi.so'——HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS,直接 import KN so 是拿不到任何业务函数的。
第 5 阶段:ArkUI 宿主
5.1 调桥与解码
const req: ConvertHsvReq = { kind: 'convert', h: hue, s: sat, v: value, a: alpha };
const res = JSON.parse(this.bridge(req)) as ConvertResult;
const bytes = util.Base64Helper().decodeSync(res.pngBase64);
const source = image.createImageSource(bytes);
const pm = await source.createPixelMap(); // Image(pm) 直接显示,旧的 release
四个 Tab 的全部视觉资产都来自桥:色轮 wheelPng(560)、四类滑条 sliderPng(依赖基色的滑条随当前色重建)、棋盘格 tilePng,PNG 按 key 缓存避免拖动时每帧重生成。
5.2 四 Tab 交互
- 色轮:按下/拖动走
coordToHsv,松手触发一次fromUser=false事件,对应上游onColorPickingFinished语义;指示器三层圆环按drawColorIndicator简化复刻; - 滑条:四根横滑条 + 一根竖直 Alpha 滑条,thumb 可拖,任一滑条联动重建依赖基色的其它滑条背景;
- AlphaTile:大棋盘格 + 带透明度的当前色覆盖,透明度滑条实时变;
- 图像取色:内置 720×240 调色板图,
readPixelsToBuffer一次读全图(ARGB_8888 buffer 是 BGRA 字节序),点击按显示/像素比例换算索引,convert(argb)出 hex——即上游ImageColorPicker的语义。
5.3 严格 ArkTS
所有请求字面量都对应显式 interface(arkts-no-untyped-obj-literals 是编译错误不是警告),回调显式类型,无 any。
第 6 阶段:构建与真机验证
6.1 构建链
export JAVA_HOME="/path/to/jdk-21" # Gradle 8.14.1 起 DevEco 的 JBR 25 拉不起来
./gradlew :colorpicker-compose:jvmTest \
:example:nativeApp:linkDebugSharedOhosArm64 \
:example:nativeApp:linkDebugSharedOhosX64
符号层用 llvm-nm -D 验收:ColorPickerBridge 在双 ABI 都是全局 T 符号,DT_NEEDED 只含短名(libc++_shared.so 等),无绝对路径。
6.2 HAP、签名和安装
DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录),build-profile.json5 已预置 "signingConfig": "default",自动签名后:
CompileArkTS: finished
PackageHap: finished
SignHap: finished
BUILD SUCCESSFUL
hdc install entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.colorpicker -m entry
四、完整代码对照
4.1 整体架构
HsvColor.kt / PickerGeometry.kt
│ HSV↔ARGB / hexCode / 色轮几何(上游公式逐行移植)
▼
Bridge.kt
│ @CName("ColorPickerBridge"),6 个 kind 的 JSON 分发
▼
libcolorpicker_napi.so
│ colorPickerBridge(request): string
▼
Index.ets
│ JSON parse + Base64 → PixelMap + 手势
▼
ArkUI 四 Tab 真机页面
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | eazytec 双仓库、模块裁剪(:colorpicker-compose + :example:nativeApp) |
gradle/libs.versions.toml | 只剩 Kotlin Multiplatform 插件,版本钉 2.2.21-1.0.0 |
colorpicker-compose/build.gradle.kts | jvm() + ohosArm64() + ohosX64() 三目标 |
src/commonMain/.../HsvColor.kt | 纯颜色模型:hsvToArgb/fromArgb/hexCode/fromHex |
src/commonMain/.../PickerGeometry.kt | angleToHue/hueToAngle/hsvToCoord/coordToHsv |
src/commonTest/ | 10 个交叉验证用例(jvmTest) |
src/composeMain/ | 上游 23 个 Compose 文件原样隔离 |
example/nativeApp/.../Bridge.kt | C ABI、JSON 分发和错误兜底 |
example/nativeApp/.../Rasterizer.kt | 色轮/滑条/棋盘格光栅化 |
example/nativeApp/.../PngEncoder.kt | zlib stored PNG 零依赖编码 |
example/harmonyApp/.../napi_init.cpp | N-API 导出、字符串转换和 DisposeString |
example/harmonyApp/.../Index.ets | ArkUI 四 Tab 运行页面 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| KMP | HsvColor.hsvToArgb | 扇区算法 HSV→ARGB,对齐 Compose Color.hsv |
| KMP | HsvColor.fromArgb / hexCode | 对齐上游 Color.toHSV / Color.hexCode |
| KMP | PickerGeometry.hsvToCoord / coordToHsv | 色轮坐标↔HSV,min(cx,cy) 半径与圆外钳位 |
| Native | ColorPickerBridge | 单 C ABI 入口,JSON in / JSON out |
| N-API | colorPickerBridge | 向 ArkTS 暴露唯一函数 |
| ArkTS | wheelPng / sliderPng / tilePng 调用 | 生成视觉资产并转 PixelMap |
4.4 ArkTS 与 Kotlin 的边界
const req: ConvertHsvReq = { kind: 'convert', h: hue, s: sat, v: value, a: alpha };
const res = JSON.parse(this.bridge(req)) as ConvertResult;
json.has("h") -> HsvColor(
hue = num(json, "h").coerceIn(0.0, 360.0).toFloat(),
saturation = optDouble(json, "s", 1.0).coerceIn(0.0, 1.0).toFloat(),
value = optDouble(json, "v", 1.0).coerceIn(0.0, 1.0).toFloat(),
alpha = optDouble(json, "a", 1.0).coerceIn(0.0, 1.0).toFloat(),
)
两者之间只传输数字和 JSON 字符串,数值在进模型前全部 coerceIn 钳位,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:把 ohosArm64 + ohosX64 加入库构建约定
真机是 arm64、DevEco 模拟器是 x86_64,只出一侧 so 会在另一侧装机时报 9568347。双 ABI 都链接、都进 HAP,才能两侧验收。
决策 2:模型上移、Composable 隔离,而不是 ArkTS 重写取色算法
在 ArkTS 里重写 HSV 转换等于抛弃上游全部可测实现;把 ColorExtensions/GeometryExtensions 的公式逐行搬进 commonMain,上游 Composable 原样进 composeMain,是当前工具链版本下唯一可验收、未来可接回的分层。
决策 3:光栅化放 Kotlin/Native,不放在 ArkTS
色轮的球与饱和层、滑条渐变、棋盘格都是上游样式模型的一部分。若只把布尔矩阵或数值丢过桥、让 ArkTS 自己画,等于把上游渐变语义在 TS 里重抄一遍。PNG 由 KN 生成,ArkTS 只解码显示,样式语义单一来源。
决策 4:JSON 作为跨语言数据契约
六个 kind 覆盖全部能力,C 签名永远是 const char* -> const char*。以后加字段(如自定义码点/滑条圆角)只改 JSON 分支,不动 ABI。
决策 5:JVM 目标保留做交叉验证
同输入下 JVM HsvColor 输出与鸿蒙桥输出必须一致(同一份代码两个目标),jvmTest 10/10 绿是「模型 100% 复用」的验收底线,防止「能出图」被误当成「没改坏算法」。
六、测试与验证
6.1 测试环境
本次验证使用 macOS、JDK 21(Gradle 侧)、DevEco Studio 5.1.0(18) SDK、HarmonyOS Kotlin 2.2.21-1.0.0 工具链与 OpenHarmony 真机(arm64-v8a)。Gradle 构建注意 JAVA_HOME 必须指向 JDK 21,DevEco 自带 JBR 25 无法启动 Gradle 8.14.1。
6.2 模型测试
./gradlew :colorpicker-compose:jvmTest
10 个用例全绿:主色六断言、toHSV 语义对照、HSV↔ARGB 往返(含 8-bit 量化容差与 hue 360↔0 环绕)、hexCode 格式、fromHex 解析与非法分量拒绝、几何往返。
6.3 HAP 验证
CompileArkTS: finished(0 错误 0 警告)
PackageHap: finished(entry-default-unsigned.hap ≈7.97 MB,双 ABI 三件套 so 齐全)
SignHap: finished(自动签名)
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
6.4 功能用例
- 启动自动生成 560px 色轮并选中初始红色,状态行显示
wheelPng字节数; - 色轮拖动指示器按 HSV 几何走位,中心近白、边缘纯色,松手触发一次
onColorPickingFinished语义(fromUser=false); - 四类滑条联动:hue 改变后 saturation/brightness/alpha 背景随基色重建;
- AlphaTile 棋盘格上实时叠加带透明度的当前色;
- 图像取色点击调色板命中像素,信息卡刷新 hex/ARGB/HSV;
- 非法输入(如错误 hex)返回
ok:false,页面显示错误,HAP 不崩。
6.5 验证结论
双 ABI 动态库、C ABI、N-API、ArkUI 页面和签名 HAP 已完成链路验证。应用在 OpenHarmony 真机(arm64)上成功安装并进入前台,色轮、滑条、棋盘格、图像取色四个 Tab 全部可用,选中色 #FF8654FF 的事件流与 PNG 生成字节数实时可见。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/colorpicker-compose.git
cd colorpicker-compose
export JAVA_HOME="/path/to/jdk-21"
./gradlew :colorpicker-compose:jvmTest \
:example:nativeApp:linkDebugSharedOhosArm64 \
:example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,自动签名后构建 HAP
hdc install entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.colorpicker -m entry
八、遗留问题与改进方向
8.1 踩坑复盘
const val初始化必须是常量表达式:PI.toFloat()不行,写死3.141592653589793f字面量;git mv目标目录不存在时会静默失败,上游测试残留在commonTest会挡compileTestKotlinJvm,迁移后必须find复核;arkts-no-untyped-obj-literals是编译错误不是警告,桥请求一律显式interface;PixelMap的 API 名是readPixelsToBuffer,且 ARGB_8888 buffer 字节序是 BGRA;- sweep 渐变必须按 stop 逐通道 sRGB 插值,按 hue 线性会让红→品红段走错方向;
- Gradle 8.14.1 需要 JDK 21,DevEco 自带 JBR 25 连版本号都打印不出来;
- KN so 的
DT_NEEDED含libc++_shared.so,拷 so 必须带上同 triple 的 libc++,否则运行期dlopen失败。
8.2 已知问题
- 鸿蒙侧没有 Compose Composable:
HsvColorPicker等仍在composeMain,等社区compose.uiohos 制品与 CMP 1.12 对齐后接回; - 光栅化是软件近似:无 Skia shader 精度,PNG 用 zlib stored 未压缩,560×560 色轮约 1.2 MB;
ImageColorPicker的像素读取在 ArkTS 侧用PixelMap完成,KN 不管图像解码;ColorPickerController的 debounce/Flow 语义在 Demo 里简化为状态机;- 上游
explicitApi与发布链未保留。
8.3 未来优化方向
- 把 Demo 验收脚本化(
uitest dumpLayout+ 自动截图),补全四 Tab 全接口效果图矩阵; - 等
compose.uiohos 制品出现后,将composeMain接回commonMain,光栅化降级为 fallback; - 桥增加自定义滑条圆角/边框、色相环分辨率等字段,C 签名不变;
- 在 CI 中加入 Native 链接、ELF 符号检查和 HAP 构建;
- 补充不同设备 ABI、API 版本的验收矩阵。
九、总结
9.1 核心难点回顾
上游 commonMain 全是 Compose
→ 拆源码集:模型进 commonMain,Composable 进 composeMain
→ ohosArm64 / ohosX64 Kotlin/Native
→ @CName("ColorPickerBridge") C ABI
→ C++ N-API(libcolorpicker_napi.so)
→ ArkTS JSON 解析 + PixelMap
→ ArkUI 四 Tab 真机页面
9.2 封装层次
- KMP 层:提供 HSV↔ARGB、hexCode 与色轮几何,公式与上游逐行一致;
- Native 层:双 ABI 动态库,单 C ABI 按 kind 分发,光栅化复刻渐变语义;
- N-API 层:完成字符串转换与 DisposeString 释放,不实现颜色语义;
- ArkTS 层:管理手势、PNG 缓存、PixelMap 生命周期和页面交互;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- CMP 库适配先找「不依赖 Compose 的那部分价值」,把它逐行搬进
commonMain,UI 隔离到composeMain,两条线都不丢; - 渐变复刻要盯住上游的 stops 和插值空间,sRGB 逐通道插值与 HSV 线性插值视觉上差得很远;
- 分别记录 JVM 交叉验证、Native 符号检查、HAP 构建和真机运行,避免把编译成功误认为设备链路可用。
9.4 适配成果
当前适配已完成:
colorpicker-compose的ohosArm64+ohosX64双目标与jvm交叉验证(10/10);- 上游 23 个 Compose 文件完整隔离保留,公式级复用的
HsvColor/PickerGeometry纯模型; - 单 C ABI JSON 桥与 KN 光栅化(色轮/四滑条/棋盘格);
- CMake NAPI 包装层与 ArkUI 四 Tab 验收页面;
- 双 ABI 三件套 so 链接与
llvm-nm符号验收; - HAP 构建、自动签名、真机安装和运行验证;
- 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)