开源鸿蒙平台 CMP 三方库「color-picker-dialog」适配全流程
本文记录
color-picker-dialog接入 OpenHarmony 的完整过程,覆盖 UI 型 CMP 库的引擎盘点、源码集拆分、真实引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
color-picker-dialog是基于 Compose Multiplatform 的取色对话框库:给定一个初始Color,它提供 HSV 面板 / 色相条 / HEX·RGB 双通道输入的完整取色交互,onConfirm以 HEX 字符串回吐结果,专为持久化(如 SQL)设计;配套parseColor/colorToHex/getHue/getSaturation/getValue/toArgb/defaultColors一组纯数学工具。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")同源但处境相反:qrose 的编码器本身就是纯 Kotlin,而 color-picker-dialog 五个源文件全部依赖 Compose——连工具层的ColorExt.kt都以 ComposeColor值类为签名。因此本适配的关键动作是引擎再表达:把ColorExt.kt的全部语义(取整方式、回退策略、HSV 公式、默认色板)逐条平移为 32 位 ARGBInt上的纯 KotlinColorEngine(全仓唯一新增库源码),Compose 对话框源码原样封存进src/composeMain不参与 ohos 编译;真实引擎被编译成 OpenHarmony 动态库,ArkUI 页面复刻取色器交互作为验收宿主——页面上出现的每一个颜色值都来自 Native 引擎,ArkTS 不做任何本地颜色数学。
项目地址: AtomGit/oh-tpc/color-picker-dialog.git
上游仓库: isaacjadrey/color-picker-dialog
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
color-picker-dialog 为 Compose 应用提供"选一个颜色、拿到一个 HEX 字符串"这一件事的完整答案:ColorPickerDialog(initialColor, onDismiss, onConfirm) 一行弹窗,内部 SaturationValuePanel 二维取饱和度/明度、HueSlider 取色相、ColorInputBox 支持手输 HEX(输入满 7 位实时反解 HSV,非法输入按上游语义静默回退默认首色)、defaultColors 提供五个预置色板,onConfirm(hex) 直接吐出可落 SQL 的字符串。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套取色与持久化能力,但上游构建(Kotlin 2.3.20 / Compose 1.10.3 / AGP 9.1.0 / Gradle 9.3.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示渐变和按钮,却无法证明上游的颜色引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证颜色引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.3.20 / Gradle 9.3.1 / AGP 9.1.0(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 全库 UI 化 | 五个源文件全部依赖 androidx.compose;连纯数学的 ColorExt.kt 签名也是 Compose Color,不能像 qrose 那样"编码器直接进 commonMain"。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:颜色引擎的 ARGB 再表达与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。取色语义完全由上游原版实现负责,ArkTS 只负责交互、展示与日志。
1.2 库提供的能力
Demo 提供一个真实的取色会话,对应桥暴露的全部入口:
- 版本握手(version):
colorPickerVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字; - 默认色板(defaults):五个预置色由引擎
defaultColors计算返回,ArkTS 不硬编码; - HSV 取色(hsv):面板/色相条每次拖动都以 h/s/v 调引擎,返回全量颜色信息;
- HEX 解析(parse):手输 HEX 满 7 位触发解析,非法输入回退默认首色(上游
parseColor语义),响应带fallback标记; - 确认持久化(Apply Color):
onConfirm(hex)的语义在 Demo 中落为日志记录——确认后的 HEX 字符串即业务侧要持久化的值。
响应(info 对象)包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
fallback | parse 输入非法、引擎按上游语义回退时为 true。 |
hex / argb | #RRGGBB 大写字符串 / 32 位 ARGB 整数。 |
a/r/g/b | 四通道 0-255 整数。 |
hue/saturation/value | HSV 三分量(hue 为 0-360 度)。 |
rgb | 上游对话框只读 RGB 展示串(如 "30, 111, 206",截断取整与上游一致)。 |
引擎侧完整能力(HSV↔RGB 扇区插值、hex 解析与格式化、默认色板、取整差异)全部在 commonMain 里随编译进入 .so,JVM 侧保留 jvm() 目标做交叉验证,证明 ohos .so 里执行的颜色逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游五个源文件零改动:四个 UI 文件整体封存 src/composeMain(非源集、不编译),ColorExt.kt 的语义由 commonMain 新增的 ColorEngine.kt(全仓唯一新增库源码)逐条镜像。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);sample(Android 示例)、apiCheck、Maven 发布链路全部移出构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI;parse 永不失败(回退语义显式化)。 |
| 依赖纪律 | 引擎零外部依赖(连 kotlinx 都不需要),桥内置 MiniJson 扁平解析,so 体积只有 ~3.4MB。 |
| UI 完整 | 单页覆盖上游对话框全部交互面:SV 面板、色相条、HEX/RGB 输入、默认色板、Selected Color、Cancel/Apply、操作日志和版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
ColorPickerDialog是androidx.compose.ui.window.Dialog上的 Composable,社区 CMP 1.9.2 工具链接不进上游 Compose 1.10;ArkTS Demo 复刻的是交互语义,颜色数值语义全部由 Kotlin 引擎保证。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认"全库 UI 化"处境、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1 降级
第 3 阶段:桥协议建模 ── 5 个 op 的 JSON 契约、info 全量响应、fallback 显式化、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 引擎权威状态机、SV 面板/色相条手势、7 位 HEX 触发、确认链路
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
color-picker-dialog/ 上游取色库(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
color-picker-dialog/src/commonMain/ 新增:ColorEngine 纯 Kotlin 引擎(唯一新增库源码)
color-picker-dialog/src/composeMain/ 上游五个源文件封存(git mv 保留历史,不编译)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
sample/ 上游 Android 示例,留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :color-picker-dialog 与 :example:nativeApp,rootProject.name = "ohos_color_picker_dialog"。上游的 vanniktech 发布插件、binary-compatibility-validator、AGP classpath、sample 模块全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Eclipse Temurin,org.gradle.java.home 写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 wrapper 9.3.1 降级;2.2.21 旧线要求) |
| 外部依赖 | 无(引擎零依赖,桥零依赖) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.colorpicker |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 文件 | 依赖形态 | 处置 |
|---|---|---|
ColorPickerDialog.kt | Dialog/material3/pointerInput/icons-extended | 封存 composeMain |
components/SaturationValuePanel.kt | Compose Canvas + 手势 | 封存 composeMain |
components/HueSlider.kt | Compose Canvas + 手势 | 封存 composeMain |
components/ColorInputBox.kt | material3 BasicTextField | 封存 composeMain |
util/ColorExt.kt | 唯一 Compose 依赖是 Color 值类(数学本身纯 Kotlin) | 语义镜像进 commonMain |
expect/actual 声明全库为 0——上游本来就是纯 commonMain,这为引擎再表达扫清了障碍。ColorExt.kt 值得单独说明:它的取色数学(max/min 求 HSV、roundToInt 格式化 HEX、toInt 截断的 RGB 展示、非法回退默认首色)一行都不碰 Compose,唯一的耦合是 Color 参数与返回值。把 Color 换成与 Color.toArgb() 逐位同布局的 32 位 ARGB Int,全部语义可以原样平移——这就是整个 fork 在库侧唯一的新增源文件 ColorEngine.kt:
// color-picker-dialog/src/commonMain/kotlin/io/github/isaacjadrey/util/ColorEngine.kt
// 与上游 ColorExt.kt 逐条对照:
// defaultColors → defaultColorsArgb / defaultColorsHex()
// getHue/getSaturation/getValue(Color) → 同名 (argb: Int)
// colorToHex(Color) → argbToHex(argb)(roundToInt + 大写 + padStart)
// parseColor(String) → parseColorArgb(String)(非法回退默认首色)
// Color.hsv(h,s,v) → hsvToArgb(h,s,v)(标准扇区插值,同 Android HSVToColor)
1.4 页面边界
标题区 COLOR PICKER · OPENHARMONY / 版本握手行(KMP 取色对话框引擎 · ohosArm64/ohosX64)
Tab 切换 Pick Color / About
SV 面板 二维渐变面板(底色=引擎纯色相,白→透明横叠,透明→黑纵叠)+ 白色游标拖拽
色相区 HUE 标签 + 当前度数 + 七段彩虹渐变条 + 游标拖拽(0-360)
输入区 HEX CODE 输入框(满 7 位触发解析)/ RGB CODE 只读展示
色板区 五个默认色圆点(来自 op=defaults,选中描白边)
确认区 48vp 色块 + Selected Color + 确认状态 / Cancel / Apply Color 按钮
日志区 带时间戳的操作日志(最新在前,上限 100 条)
关于区 版本握手、桥接协议、分层结构、仓库与社区链接、已知边界
页面启动时先调 colorPickerVersion() 做握手,再取 op=defaults 拿默认色板,最后以初始色 #1E6FCE(上游默认首色)调 op=parse 初始化全部状态。
第 2 阶段:目标与依赖打通
2.1 依赖边界验证(本适配的成败手)
KStore 适配时要先验证社区 Maven 上的 kotlinx ohos 制品线;本库连这一步都省了——commonMain 的引擎和 ohosMain 的桥都是零外部依赖的纯 Kotlin,唯一要解析的"外部制品"就是 HarmonyOS Kotlin 插件本身:
org.jetbrains.kotlin.multiplatform:2.2.21-1.0.0 (eazytec Maven,提供 ohosArm64()/ohosX64())
libs.versions.toml 收敛为两行:kotlin = "2.2.21-1.0.0" + kotlin-multiplatform 插件别名。compose、AGP、vanniktech、validator、material3 全部删除——它们只服务于被剥离的 UI 线与发布线。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区批准的 CMP 库钉法有两条线:qrose 线是 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1,KStore 线是 2.4.20-0.1.0-07 + Gradle 9.6.1。本库没有 kotlinx 依赖,选线不受制品元数据约束,按最小工具链风险选 qrose 线:
- 引擎源码是从 Compose 1.10 库里平移出来的初等数学,不使用任何 2.3/2.4 语法特性,2.2.21 编译零成本;
- 上游 Gradle 9.3.1 在 2.2.21 旧线上不可用,wrapper 降级 8.14.1(cmp_qrose 实测同款组合);
- 上游
gradle-daemon-jvm.properties(JETBRAINS daemon criteria)与 8.14 冲突,删除,统一走gradle.properties的org.gradle.java.home钉 Temurin 21。
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_color_picker_dialog"
include(":color-picker-dialog")
include(":example:nativeApp")
2.3 编译真实引擎
color-picker-dialog/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
}
}
上游脚本里的 explicitApi()、android target(com.android.kotlin.multiplatform.library + withHostTestBuilder/withDeviceTestBuilder)、iOS framework 三件套、vanniktech mavenPublishing 发布块全部随重写剥离。实测:
./gradlew :color-picker-dialog:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :color-picker-dialog:compileKotlinOhosArm64 # BUILD SUCCESSFUL
./gradlew :color-picker-dialog:compileKotlinOhosX64 # BUILD SUCCESSFUL
引擎语义一行没动,只有 ColorEngine.kt 是新的;composeMain 里的五个上游文件一个字节没改。
2.4 上游遗留的处置:sample、apiCheck、发布链路
上游工程有三块"长在旧构建上"的遗留:sample(Android Compose 示例,引用已删除的插件别名)、color-picker-dialog/api/(binary-compatibility-validator 的 dump 基线)、publish.yml + release.sh(Maven Central 发布流)。处置与 cmp_qrose 一致——留盘、不进构建:settings.gradle.kts 移除 :sample,validator 插件随根构建替换消失(api 目录变成纯文档),发布流保留为上游遗迹(只在 release 事件触发,任务已不存在)。CI 收敛为单 job 编译 JVM 目标;ohos 构建本地执行。
第 3 阶段:桥协议建模
3.1 JSON 契约
io.github.isaacjadrey.util/**(引擎,ARGB/HSV/HEX 纯数学)
↓ parseColorArgb / hsvToArgb / argbToHex / rgbCode / defaultColorsHex
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API colorPickerDispatch() / colorPickerVersion()
↓
ArkUI Index.ets(单页两 Tab)
请求与响应:
请求 : {"op":"version"}
{"op":"defaults"}
{"op":"parse","hex":"#1E6FCE"}
{"op":"hsv","h":210,"s":0.85,"v":0.8}
{"op":"hex","argb":-14782514}
响应 : {"ok":true,"fallback":false,"hex":"#1E6FCE","argb":-14782514,
"a":255,"r":30,"g":111,"b":206,
"hue":212.3876,"saturation":0.8544,"value":0.8078,
"rgb":"30, 111, 206"}
{"ok":false,"error":"..."}
3.2 为什么一次调用返回全量 info
三个不对称设计:其一,hsv 的响应是全量颜色信息(hex/argb/三通道/HSV/RGB 串),ArkTS 拿到一次响应就能刷新整页——页面上不存在引擎没算过的颜色值,这是"真适配"与"ArkTS 重写"的分水岭;其二,parse 永不失败,非法输入返回回退色的 info 并标 fallback:true,把上游 parseColor 的静默回退语义显式化,Demo 据此提示"非法输入 → 引擎回退 #1E6FCE"而不是把上游行为藏起来;其三,浮点输出由桥内 num() 手写定点化(4 位小数去尾零),避免 Kotlin/Native Float.toString 的科学计数法混进 JSON。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ColorPickerDispatch")
fun colorPickerDispatch(request: String): String = try {
dispatch(request)
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、缺 op / hex / h / s / v、不支持的 op,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进日志区。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libcolorpicker_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoscolorpicker.so)
│ ColorPickerDispatch / ColorPickerVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;HarmonyOS CAdapter 也不会把 @CName 函数挂到 ArkTS 模块表(cmp_qrose 已验证),所以必须 CMake 再包一层 NAPI so。
4.2 Kotlin/Native 导出函数
@CName("ColorPickerDispatch")
fun colorPickerDispatch(request: String): String // 5 个 op 一口进
@CName("ColorPickerVersion")
fun colorPickerVersion(): String = "1.0.0-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ColorPickerDispatch | colorPickerDispatch(req) | parseColorArgb / hsvToArgb / argbToHex / rgbCode / defaultColorsHex / isValidHexColor |
ColorPickerVersion | colorPickerVersion() | fork 版本握手(真机版本行的数据来源) |
MiniJson 只解析扁平 JSON(按引号状态机切顶层字段、剥转义、认数字),不引 kotlinx.serialization——引擎零依赖,桥也零依赖。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"colorPickerDispatch", nullptr, NapiColorPickerDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"colorPickerVersion", nullptr, NapiColorPickerVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscolorpicker_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscolorpicker_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohoscolorpicker SHARED IMPORTED)
set_target_properties(ohoscolorpicker PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(colorpicker_napi SHARED napi_init.cpp)
target_link_libraries(colorpicker_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoscolorpicker)
OHOS_ARCH 为空或 libohoscolorpicker.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const colorPickerDispatch: (request: string) => string;
export const colorPickerVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 引擎权威的页面状态机
页面把上游对话框的状态机逐条搬过来,唯一区别是每个转换都过一次 Native:
aboutToAppear(): void {
this.version = colorPickerVersion(); // 版本握手:真实 Native 调用
const defs = JSON.parse(colorPickerDispatch('{"op":"defaults"}'));
this.defaults = defs.colors; // 默认色板来自引擎
this.applyHex(this.initialHex, true); // 初始色 #1E6FCE → HSV
}
private applyHsv(h: number, s: number, v: number): void {
const res = JSON.parse(colorPickerDispatch(JSON.stringify({ op: 'hsv', h: h, s: s, v: v })));
this.hue = h; this.sat = s; this.val = v;
this.info = res; this.rgbText = res.rgb;
this.pureHueHex = this.pureHueOf(h); // 面板底色 = 引擎算的纯色相
if (this.hexInput.toUpperCase() !== res.hex.toUpperCase()) {
this.hexInput = res.hex; // 上游 LaunchedEffect 的同步规则
}
}
三处与上游逐字对齐的细节:hex 输入框只在 hexInput.uppercase() != currentHex.uppercase() 时被覆写(用户输入小写不会被强转大写);输入满 7 位(含 #)才触发解析反推 HSV,不足 7 位不动;非法 7 位输入走引擎回退,页面日志记 invalid hex ... → engine fallback。
5.2 页面交互
Pick Color Tab 自上而下:SV 面板(底色 = op=hsv h,1,1 的引擎返回值,上叠白→透明横向饱和度层与透明→黑纵向明度层,白色游标随拖动移动,每次 move 都是一次 op=hsv)、HUE 彩虹条(右侧实时度数)、HEX CODE 输入框 + RGB CODE 只读展示(截断取整与上游一致)、五个默认色圆点(选中描白边)、Selected Color 区(48vp 色块 + 确认状态)、Cancel / Apply Color 双按钮(配色沿用上游硬编码色板:卡片 #111827、确认键 #1E6FCE)、带时间戳的操作日志(最新在前、上限 100 条)。About Tab 展示版本握手行、桥接协议表、ArkTS → NAPI → C ABI → Kotlin 引擎 分层说明与仓库 / 社区链接。
5.3 对比度和错误边界
确认键深蓝 #1E6FCE 白字,Cancel 透明底灰描边,语义与上游按钮层级一致;ok:false 时错误原文进日志区;fallback:true 时状态行提示回退——Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoscolorpicker)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/io/github/isaacjadrey/colorpicker/
│ └── Bridge.kt @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.colorpicker、versionName 1.0.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/libohoscolorpicker_api.h
├── src/main/cpp/types/libcolorpicker_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :color-picker-dialog:compileKotlinJvm # 引擎 JVM 交叉验证
./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 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 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.colorpicker -m entry
四、完整代码对照
4.1 整体架构
io.github.isaacjadrey.util/**(ColorEngine,语义镜像上游 ColorExt.kt)
│ parseColorArgb / hsvToArgb / argbToHex / rgbCode / defaultColorsHex
Bridge.kt + MiniJson
│ ColorPickerDispatch / ColorPickerVersion
libcolorpicker_napi.so
│ colorPickerDispatch / colorPickerVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(单页两 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_color_picker_dialog、sample 移出 |
gradle/libs.versions.toml | 只留 Kotlin 2.2.21-1.0.0 + KMP 插件 |
gradle.properties(新增并提交) | Temurin 21 路径、version=1.0.0-ohos.1、KN 开关 |
build.gradle.kts(根) | 版本 1.0.0-ohos.1、全局禁用 Ohos*Test* 任务 |
color-picker-dialog/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
color-picker-dialog/src/commonMain/.../ColorEngine.kt | 唯一新增库源码(ARGB/HSV/HEX 纯数学) |
color-picker-dialog/src/composeMain/** | 上游五个源文件封存(git mv,零改动) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、MiniJson、fallback 显式化、错误通道 |
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 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(19 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | parseColorArgb(hex) | 上游 parseColor:可选 #、恰 6 位、非法回退默认首色 |
| 引擎 | hsvToArgb(h, s, v) | 上游 Color.hsv:标准扇区插值,入参先 clamp |
| 引擎 | argbToHex(argb) | 上游 colorToHex:roundToInt、大写、#RRGGBB |
| 引擎 | rgbCode(argb) | 上游对话框只读 RGB 串:toInt 截断(与 HEX 的取整刻意不同) |
| 引擎 | getHue / getSaturation / getValue | RGB→HSV 三分量(灰色 hue=0 与上游一致) |
| 引擎 | defaultColorsArgb / defaultColorsHex | 上游 defaultColors 五色 |
| Native | ColorPickerDispatch / ColorPickerVersion | C ABI JSON 导出 |
| N-API | colorPickerDispatch(req) / colorPickerVersion() | 向 ArkTS 暴露能力 |
| ArkTS | applyHsv / applyHex | 引擎权威状态机(同步规则与上游逐字对齐) |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(colorPickerDispatch(JSON.stringify({
op: 'parse', hex: '#1E6FCE'
})));
// res.hex === '#1E6FCE', res.hue === 212.3876, res.rgb === '30, 111, 206'
val argb = parseColorArgb(hex) // 非法输入 → defaultColorsArgb.first()
info(argb, fallback = !isValidHexColor(hex))
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:引擎再表达而不是整库编译
qrose 的编码器本来就在 commonMain、零 UI 依赖,加 target 即可;KStore 依赖的 kotlinx ohos 制品线已补齐,真编译整库即可。color-picker-dialog 处境相反:五个文件全部依赖 Compose,"整库编译"根本不存在——但库的价值恰恰不在 Dialog 而在 ColorExt 的语义细节(round vs truncate、回退、大小写、灰色 hue=0)。把这份语义逐条平移到 ARGB Int 上,UI 封存 composeMain,是"全库 UI 化"处境下的唯一正确答案;语义漂移由 JVM 交叉验证兜底(见 6.2)。
决策 2:工具链选 qrose 线(Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 降级)
引擎零依赖,选线不受制品元数据约束,按最小风险选社区批准最早的 qrose 组合。Gradle 从上游 9.3.1 降到 8.14.1 是 2.2.21 旧线的硬要求;gradle-daemon-jvm.properties 与 8.14 冲突随之删除,JDK 统一由 org.gradle.java.home 钉死。KStore 的"2.4.20 线可留 Gradle 9.x"经验不适用于本库——没有依赖拉着工具链走,就选最保守的。
决策 3:根构建替换(cmp_qrose 模式),sample / apiCheck / 发布链路留盘
上游根构建(vanniktech 发布、binary-compatibility-validator、AGP classpath、foojay)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample 是 Android Compose 示例、api/ 是 validator 基线、publish.yml/release.sh 是 Maven 发布流——全部留盘不进 settings.gradle.kts,与 qrose 处置 androidapp/desktopApp 完全同款。
决策 4:parse 永不失败 + fallback 显式化
上游 parseColor 对非法输入返回默认首色而不是抛异常。桥把这个行为显式化为 fallback:true 而不是报错——Demo 能展示"非法输入 → 回退 #1E6FCE"这条上游真实语义,验收时它反而是必测用例。把上游的静默行为变成可观测的协议字段,是"语义镜像"优于"重写"的直接证据。
决策 5:零依赖引擎 + 手写 MiniJson
引擎不需要 kotlinx(颜色数学是初等算术),桥不为 5 个键引 kotlinx.serialization。收益直接反映在产物上:含 Kotlin/Native 运行时的 libohoscolorpicker.so 只有 ~3.4MB,是同期适配库里最轻的一档。浮点输出由 num() 手写定点化,绕开 KN Float.toString 的科学计数法。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin jdk-21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;jvm() 目标保留就是为了这条闸门:
val argb = parseColorArgb("#1E6FCE")
println(argbToHex(argb)) // #1E6FCE
println(getHue(argb)) // 212.38757
println(getSaturation(argb)) // 0.8543689
println(getValue(argb)) // 0.80784315
println(rgbCode(argb)) // 30, 111, 206
println(argbToHex(hsvToArgb(210f, 0.85f, 0.8f))) // #1F75CC
println(argbToHex(parseColorArgb("#GGGGGG"))) // #1E6FCE(回退)
钉住四组语义:HEX 用 roundToInt 而 RGB 展示用 toInt 截断(上游刻意不同);非法 7 位输入回退默认首色;hsv(210,0.85,0.8) 逐位等于标准扇区插值;灰色 hue=0 而非 NaN。真机页面显示的 hue=212.3876 / rgb=30, 111, 206(桥 4 位定点化)与 JVM 打印一致,证明换端没漂。
6.3 HAP 验证
:color-picker-dialog:compileKotlinJvm / OhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填三件套)
llvm-nm -D: ColorPickerDispatch / ColorPickerVersion / libohoscolorpicker_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libcolorpicker_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
实测产物:libohoscolorpicker.so arm64 约 3.4MB / x86_64 约 3.2MB(含 Kotlin/Native 运行时与引擎),libc++_shared.so 约 1.8MB / 1.7MB;CAdapter 链接期 clang api.cpp 警告与 cmp_qrose 同款(生成胶水,不影响产物)。
6.4 功能用例
- 启动后版本握手行显示
1.0.0-ohos.1(来自真实 Native 调用,非静态文字),日志区记录version handshake; - 默认色板五个圆点来自
op=defaults,与上游defaultColors逐项一致(#1E6FCE / #E91E63 / #10B981 / #F59E0B / #6366F1); - 拖动 SV 面板白色游标,HEX、RGB、色块实时跟随,每次 move 都是一次
op=hsvNative 调用;游标拖到左上角 HEX 收敛为#FFFFFF、右下角为#000000; - 拖动 HUE 条,SV 面板底色(引擎
h,1,1)与 HEX 同步变化,右侧度数实时刷新; - HEX 输入合法 7 位(如
#10B981),两个游标瞬间跳到对应 HSV 位置,RGB 只读框同步; - HEX 输入非法 7 位(如
#GGGGGG),引擎回退#1E6FCE,日志出现invalid hex ... → engine fallback,页面不崩溃; - 点默认色板圆点,状态整体切换,当前色描白边;
- 点 Apply Color,日志记录
confirm → #XXXXXX (hex string ready to persist, e.g. SQL)——onConfirm(hex)的持久化语义;点 Cancel 复位初始色; - 切到 About Tab,版本握手值、桥接协议表、分层结构与 AtomGit 链接完整可见;
- 每步操作日志按时间戳滚动留痕(最新在前,上限 100 条)。
6.5 验证结论
颜色引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的取色会话:版本握手、默认色板、HSV 拖拽、HEX 双向输入、非法回退与确认持久化语义闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/color-picker-dialog.git
cd color-picker-dialog
export JAVA_HOME="/path/to/jdk-21"
./gradlew :color-picker-dialog:compileKotlinJvm # 引擎 JVM 交叉验证
./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.colorpicker -m entry
hdc shell hilog | grep -i colorpicker
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:color-picker-dialog真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 2.2.21 旧线必须配 Gradle 8.14.1——上游 9.3.1 wrapper 原样沿用会在插件解析期失败,"KStore 线可留 9.x"的经验不能跨线套用;
- 上游
gradle.properties被 .gitignore 忽略(“防止私钥入库”),本 fork 的 JDK 路径与版本号恰恰要写进它——忽略行必须删除并提交,否则换机构建直接失败; gradle-daemon-jvm.properties的 JETBRAINS daemon criteria 在 Gradle 8.14 上不兼容,删除后统一由org.gradle.java.home管 JVM;- 全库 UI 化的 CMP 库不能照抄 qrose 的"加 target 就完":
ColorExt.kt的Color签名必须先再表达为 ARGB,否则 ohos 编译器在 commonMain 就找不到androidx.compose; - 上游语义里的取整差异是坑不是 bug:HEX 用 roundToInt、RGB 展示用 toInt 截断,镜像时"修正"它反而制造漂移;
- Kotlin/Native 的
Float.toString会输出科学计数法(如1.0E-4),直接拼 JSON 会毁掉 ArkTS 侧解析,桥内手写 4 位定点化; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝(00303038 must have required property 'material')——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - DevEco 打开工程会生成
.idea/、.hvigor/、entry/build/、entry/libs/,root.gitignore的build/.idea虽跨目录生效,entry/libs(含 so 三件套)与oh_modules必须显式追加,否则二进制混进提交。
8.2 已知问题
- HarmonyOS 上没有 Compose
Dialog:上游ColorPickerDialog等 UI 封存于composeMain,等社区 CMP 工具链与上游 Compose 1.10 对齐后可整体接回,届时 ArkTS Demo 降级为对照样例; - HUE 条 / SV 面板的渐变由 ArkUI 绘制,引擎保证的是数值语义(游标位置 ↔ s/v、底色 = 引擎纯色相、HEX/RGB = 引擎字符串)——与 Vico(图表交 ArkTS Canvas)同口径;
DialogProperties(如usePlatformDefaultWidth)在鸿蒙侧无对应物,本 fork 不透出;sample(Android)、api/基线、publish.yml/release.sh为上游遗留,不可在鸿蒙环境使用;- android / iOS target 剥离(源文件留在树里),发布、apiCheck 一并剥离,本 fork 不发 Maven Central。
8.3 未来优化方向
- 把 6.2 节的 JVM 语义对账落成常驻
jvmTest(含 HSV 0-360 网格逐位比对上游Color.hsv); - 桥增加
op=palette(从任意初始色生成建议色板)与透明度通道(ARGB 的 A 目前恒为 255,与上游一致); - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补模拟器(x86_64)验收矩阵与 uitest 自动化截图脚本(qrose 已有先例)。
九、总结
9.1 核心难点回顾
上游 color-picker-dialog(五个源文件全部依赖 Compose)
→ 引擎再表达:ColorExt 语义逐条平移为 ARGB 纯 Kotlin(唯一新增库源码)
→ UI 封存:五个上游文件 git mv 进 composeMain,零改动
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1 降级)
→ 根构建替换(cmp_qrose 模式,sample/发布链路留盘)
→ ohosArm64 / ohosX64 KLIB + 零依赖 so(~3.4MB)
→ C ABI(@CName × 2)+ MiniJson + fallback 显式化
→ C++ N-API(DisposeString 纪律)
→ ArkTS 引擎权威状态机(同步规则与上游逐字对齐)
→ ArkUI 真机单页验收(拖拽 / 输入 / 回退 / 确认持久化)
9.2 封装层次
- 引擎层:
ColorEngine——上游ColorExt的 ARGB 再表达(取整差异、回退策略、HSV 公式、默认色板原样); - Native 层:双 ABI 动态库,2 个 C ABI 输出全量 JSON;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:引擎权威状态机、交互手势、日志与错误/回退提示;
- DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 型 CMP 库的适配面在"语义"不在"控件": color-picker-dialog 的持久化卖点全部长在
ColorExt的细节里(round vs truncate、静默回退、大小写、灰色 hue=0)。把这些语义原样平移过桥,比把 Dialog 控件搬过来更接近"适配这个库";控件会随工具链换代,语义才是跨端资产。 - 工具链选线按依赖约束走,没有依赖就选最保守线: qrose 线(2.2.21 + 8.14.1)与 KStore 线(2.4.20 + 9.6.1)的差异由各自依赖矩阵决定;本库零依赖,直接选社区批准最久的组合,把变量减到最少。
- 把上游的静默语义变成协议字段是验收的捷径:
parse永不失败 +fallback:true让"非法输入回退默认色"从一条需要翻源码确认的行为,变成 Demo 上一个可点击、可日志留痕的用例——语义镜像做对了,验收自然就顺了。
9.4 适配成果
当前适配已完成:
- color-picker-dialog
1.0.0颜色引擎的双 ABI Kotlin/Native 编译(上游五个源文件零改动,唯一新增库源码ColorEngine.kt); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,双 ABI); - DevEco Stage ArkUI 两 Tab 验收页面(SV 面板、色相条、HEX/RGB、默认色板、Cancel/Apply、操作日志、版本握手);
- JVM 交叉验证(语义逐组对账)+ HAP 构建(双 ABI 三件套 so,arm64 ~3.4MB / x86_64 ~3.2MB)、签名、设备安装和真机验证;
- 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)