开源鸿蒙平台 CMP 三方库「anyColorPicker」适配全流程
开源鸿蒙平台 CMP 三方库「anyColorPicker」适配全流程
本文记录
anyColorPicker接入 OpenHarmony 的完整过程,覆盖取色器库盘点、纯 Kotlin 颜色模型的目标手术、真实引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、光栅化出图、ArkUI 真机页面、HAP 构建和真机验收。
anyColorPicker是 Kotlin Multiplatform / Compose Multiplatform 取色器库:ColorValue把颜色表示为「空间 + 分量 + 缺失位图 + alpha」,15 个 CSS Color 4 色彩空间(sRGB、linear sRGB、Display P3、XYZ D65/D50、Lab、LCH、Oklab、OkLCh、HSL、HWB、HSV、Okhsl、Okhsv、CMYK)任意互转,CSS 字符串与 hex 双向解析,三种色域映射方法(CSS 二分 / 色度收缩闭式解 / 裁剪),滑轨与平面由GamutMapper批量出像素。本次适配与 KStore 的「kotlinx 制品线」路线正好是两个方向:颜色模型模块依赖为零(唯一的第三方依赖只是一个@Immutable注解),剥掉 24 行注解后整个 CSS Color 4 引擎原样编译成 OpenHarmony KLIB;真正过不了桥的反而是 Compose UI 层——上游 CMP 1.12 / Kotlin 2.4 与社区 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 对不齐,colorpicker-foundation/colorpicker-material3保留源码不编,鸿蒙侧的滑轨与平面用 Kotlin/Native 光栅化 PNG 复现,每个像素都出自库自己的GamutMapper.convertToArgb,ArkUI 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/anyColorPicker.git
上游仓库: side-codes/anyColorPicker
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
anyColorPicker 2.0.0 为 Compose 应用提供「选一个颜色」这一件事的完整答案:ColorPicker(state, space) 一个组件覆盖任意空间,15 个命名变体(RgbColorPicker / OkhslColorPicker / …)开箱即用,ChannelSlider / ChannelPlane / AlphaSlider 是可组合的底层部件,ColorPickerDialog 带新旧色对比与空间切换,M3 主题(ColorPickerDefaults)与无 Material 的 Basic 组件两套皮。支撑这套 UI 的是 color 模块里的 CSS Color 4 颜色模型:ColorValue 是不可变值类型,value.to(Oklch) 一行跨空间,parseCss 吃 oklch(0.7 0.15 none) 与 color(display-p3 1 0 0),toGamut 按 CSS §14.2 把出域色映射回 sRGB,GamutMapper 是为滑轨/平面准备的批量像素出口。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套颜色能力,但上游构建(Kotlin 2.4.20 / CMP 1.12.1 / Gradle 9.7.1 / AGP 9.4.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以画出滑杆,却无法证明颜色引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行——色域映射、none 分量、Okhsl 几何这些数学不跑在设备上,取色器就只是一个装饰。适配需要同时验证颜色模型、光栅化、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.20 / Gradle 9.7.1 / AGP 9.4.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建;HarmonyOS Kotlin 2.2.21 线不支持 Gradle 9.x,wrapper 必须降 8.14.1。 |
| 依赖形态特殊 | color 模块唯一第三方依赖是 androidx.compose.runtime:runtime-annotation(仅 @Immutable 注解),该制品没有 ohos 变体,原生依赖解析必失败。 |
| Compose UI 编不进 | colorpicker-foundation / colorpicker-material3 / color-compose 共 60+ 文件站在 compose.ui / material3 上,上游 CMP 1.12 与社区 CMP 1.9.2 工具链对不齐。 |
| 渲染语义跨不了桥 | Compose 的 Canvas / Brush / Painter 是 Kotlin 侧概念,C ABI 只能同步传 JSON;滑轨渐变与二维色平面需要另一条出图路径。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:颜色模型模块的 ohos 编译(剥注解、零算法改动)、根构建替换(cmp_qrose 模式)、C ABI/N-API 桥接层与 Kotlin/Native 光栅化。颜色数学完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 四个 Tab 覆盖桥接的全部入口:
- 取色器(Picker):15 个空间横滑切换,平面取自该空间最后两个通道(拖动取色),其余通道每条一个 PNG 滑轨,alpha 轨自带棋盘格背景;contextual / independent 两种着色模式与 fast / css / clip 三种映射可切;当前颜色卡实时显示 css / hex / argb;
- 解析(Parse):
parseCss的全部语法面——hex、148 个 CSS 命名色、transparent、rgb()/hsl()逗号与空格两种写法、hwb()/lab()/lch()/oklab()/oklch()/color(),none 分量原样保留,失败时CssColorParseException的error与出错位置index一起回传; - 色域(Gamut):inGamut 判定(对 sRGB 色域)、css / chroma / clip 三种映射方法并排色块对比、
maxChroma(该亮度色相下 sRGB 容得下的最大 OkLCh 色度)与cusp(该色相最鲜艳色); - 平面(Plane):okhsv / okhsl / hsv / hsl / srgb 五种预设 × 三种渲染模式 × 三种尺寸,点按取色。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文(解析失败另带 index)。 |
op / space | 回显本次操作与颜色所在空间。 |
components | 逐通道键值(通道 id → 数值或 null),null 即 CSS 的 none 分量。 |
css / cssLegacy / hexLast / hexFirst / argb | 现代与 legacy 两种 CSS 输出、alpha 在前/在后两种 hex、映射到 sRGB 的 0xAARRGGBB。 |
inGamut | 对 sRGB 色域的判定(isInGamut,容差 7.5e-5)。 |
pngBase64 / width / height | 滑轨与平面的光栅化产物。 |
引擎侧完整能力(15 空间转换矩阵融合、Okhsl/Okhsv 几何、闭式解色域边、converter 缓存)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留同路径冒烟交叉验证,证明 ohos .so 里执行的颜色数学没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | color 模块全部 commonMain 文件原样编译,不提取、不重写;仅剥 8 个文件的 @Immutable 注解(共 24 行,算法零改动)与新增 5 行 ohos gamutMemo actual(全仓唯一新增库源文件)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);colorpicker-foundation / colorpicker-material3 / color-compose / sample / screenshot-tests(全部依赖 Compose)留盘不进构建。 |
| 桥接稳定 | 1 个 C ABI 承载 6 个 op(spaces / parse / convert / track / plane / gamut),UTF-8 JSON 进出;异常永远不跨 FFI。 |
| 出图路径 | 滑轨/平面像素全部走库自己的 GamutMapper.convertToArgb 批量接口,零依赖 PNG 编码器(zlib stored)+ Base64 回传。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:15 空间切换、平面拖动取色、PNG 滑轨、alpha 棋盘格、解析样例、色域三方法、平面渲染矩阵。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。鸿蒙侧的滑轨与平面是 Kotlin/Native 光栅化 PNG 的近似复现——无 Material 主题、无动画、无障碍语义等待 Compose 模块接回;但每一个像素的颜色值都出自上游颜色引擎,这是验收的锚点。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点四层模块与依赖边界、确认 color 模块零依赖可整编、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1、剥 @Immutable
第 3 阶段:桥协议建模 ── 6 op JSON 契约、comps 逗号序列化、none 位图、错误通道带 index
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:光栅化与 ArkUI ── GamutMapper 批量出像素、cusp 锚点独立着色、alpha 棋盘格、四 Tab 页面
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
color/ 上游 CSS Color 4 颜色模型(进 ohos 构建,jvm + ohosArm64 + ohosX64)
color/src/ohosMain/ 新增:GamutMemo actual(5 行,全仓唯一新增库源文件)
color-compose/ Compose Color 桥接(依赖 compose.ui.graphics,留盘不进 settings)
colorpicker-foundation/ Compose 基础组件(31 文件,留盘不进 settings)
colorpicker-material3/ Material3 取色器(28 文件,留盘不进 settings)
sample/ screenshot-tests/ 上游示例与截图测试(留盘不进 settings)
example/nativeApp/ Kotlin/Native 动态库(AcpBridge + ColorRaster + PngEncoder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :color 与 :example:nativeApp,rootProject.name = "anyColorPicker"。上游的 build-logic convention 插件(AGP KMP library + dokka + vanniktech 发布 + BCV + wasm/ios 目标)、foojay resolver、screenshot 配置全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / haze 同款旧线) |
| JDK | 21(Temurin 21.0.12.1,org.gradle.java.home 指向本机 JDK) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 wrapper 9.7.1 必须降级——2.2.21 线不支持 Gradle 9.x) |
| 第三方依赖 | 零(color 模块剥注解后无任何外部依赖) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.anycolorpicker |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
color | 43 | 纯 Kotlin + runtime-annotation(仅 @Immutable),零 UI、零 kotlinx | 剥注解后整体编译(jvm + 双 ohos) |
color-compose | 3 | compose.ui.graphics(Compose Color 互转) | 留盘不进构建 |
colorpicker-foundation | 31 | compose.runtime / foundation / ui + coroutines | 留盘不进构建 |
colorpicker-material3 | 28 | material3 | 留盘不进构建 |
expect 声明全库只有 1 处:internal expect fun gamutMemo(): GamutMemo——Okhsl/Okhsv 几何求解的行缓存(cusp、三角函数、行根),上游给了 ios(@ThreadLocal)、jvm/android(ThreadLocal.withInitial)、wasmJs(单例全局)三个 actual。ohos 补第四种,这就是整个 fork 唯一的新增库源文件:
// color/src/ohosMain/kotlin/codes/side/color/internal/GamutMemo.ohos.kt
// One shared memo, as on wasmJs: the new Kotlin/Native memory model shares it across threads,
// and the NAPI entry point calls in from ArkTS's single JS thread anyway.
private val memo = GamutMemo()
internal actual fun gamutMemo(): GamutMemo = memo
runtime-annotation 的处置是本次适配的「依赖手术」:它只提供 @Immutable 注解(Compose 编译器的稳定性提示),没有 ohos 变体,原生解析必失败。剥法与社区早期 qrose 剥编码器注解完全同款——8 个文件各删一行 import 一行注解,共 24 行,算法零改动:
CmykView.kt / ColorValue.kt / OklabViews.kt / XyzViews.kt /
OkhsxViews.kt / HueSaturationViews.kt / LabViews.kt / RgbViews.kt
- import androidx.compose.runtime.Immutable
- @Immutable
值得一记的是版本兼容性实测:上游源码是 Kotlin 2.4.20 时代写的,color/commonMain 在 HarmonyOS Kotlin 2.2.21 下一次编译通过——纯算法模块不用新语言特性,kotlin.math.cbrt、Double.sameBits 这些在 2.2 stdlib 里都已稳定。挡住适配的从来不是语法,是依赖和目标。
1.4 页面边界
标题区 anyColorPicker · OpenHarmony / 副标题行(KMP 取色器库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 取色器 / 解析 / 色域 / 平面
取色器 Tab 15 空间横滑 chips → 平面图(最后两通道,拖动取点)→ 通道滑轨 × N + alpha 棋盘格轨
→ contextual/independent 着色切换 → fast/css/clip 映射切换 → 随机色按钮 → 颜色卡
解析 Tab TextInput + 8 个样例 chips → 空间徽章 + 逐通道行 + css/cssLegacy/hex/argb/inGamut
色域 Tab 输入 + 样例 → inGamut 徽章 + 三方法色块对比 + maxChroma/cusp/lightness/hue
平面 Tab 空间预设 × 映射模式 × 尺寸 → plane 图点按取色
页面启动时先调 spaces op 拉取 15 个空间与通道定义(id、min、max、step、isHue、precision 全部来自 Native 运行时的 ColorSpaces.all,不在 ArkTS 侧硬编码),再渲染取色器 Tab。
第 2 阶段:目标与依赖打通
2.1 依赖形态判定(本适配的路线分岔口)
KStore 走「kotlinx ohos 制品线」是因为存储引擎站在 coroutines / serialization / kotlinx-io 上;anyColorPicker 的 color 模块盘点结果是零依赖(剥注解后)——不需要制品线,整模块直编即可。真正的分岔在另一头:UI 三模块(foundation / material3 / color-compose)站在 Compose 上,社区 CMP 是 1.9.2 而 upstream 是 CMP 1.12 / Compose compiler 随 Kotlin 2.4——把 Painter 编进 ohos 不是本次适配能承载的工程量。处置与社区已过审的 Vico / Reorderable / qrose 相同:Compose 源码留盘隔离,鸿蒙侧用 KN 光栅化出图。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区已批准的两条 Kotlin 线:2.2.21-1.0.0(qrose / haze)与 2.4.20-0.1.0-07(KStore,跟着 kotlinx 制品的 stdlib 元数据走)。anyColorPicker 零依赖,没有制品反推的约束,选旧线 2.2.21-1.0.0——它是社区验证最充分的一条。代价是 Gradle:上游 wrapper 是 9.7.1,2.2.21 线最高支持 8.14.1(qrose 先例),wrapper 必须降级,distributionUrl 改成 gradle-8.14.1-bin.zip 即可,wrapper jar 不用动。
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 = "anyColorPicker"
include(":color")
include(":example:nativeApp")
gradle.properties 钉死 JDK 与版本,三行 kotlin.native 开关继承 qrose 的踩坑结论:
org.gradle.java.home=C:\\Users\\Administrator\\.jdks\\jdk-21
version=2.0.0-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true
2.3 编译真实引擎
color/build.gradle.kts 收敛为纯 KMP 模块(上游经 build-logic convention 插件挂了 AGP / wasm / ios / dokka / 发布,全部剥掉):
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
// Upstream shares one ThreadLocal-based memo between JVM and Android; Android is not
// built in this fork, but the source file layout is kept as upstream left it.
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate {
common {
group("jvmAndAndroid") {
withJvm()
withCompilations { it.platformType == KotlinPlatformType.androidJvm }
}
}
}
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
}
}
保留上游自定义的 jvmAndAndroid hierarchy group(android 目标不构建,但 GamutMemo.jvmAndAndroid.kt 的文件布局原样留在树上)。实测:
./gradlew :color:compileKotlinJvm # BUILD SUCCESSFUL (48s)
./gradlew :color:compileKotlinOhosArm64 :color:compileKotlinOhosX64 # BUILD SUCCESSFUL (21s)
43 个 commonMain 文件一个没动,剥掉的只有注解,新增的只有那 5 行 ohos actual。
2.4 测试依赖的处置:serialization / resources 无需 ohos 制品
上游 commonTest(26 个文件)依赖 kotlinx-serialization-json 与 kotlinx-resources 读 JSON 参考数据,二者解析不需要进 ohos——处置是两条纪律的组合:测试源集不进 fork 构建(commonTest.dependencies 不声明),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
上游测试文件零改动留在磁盘,交叉验证改用「与桥逐字段同路径」的 JVM 冒烟集(见 6.2)。
第 3 阶段:桥协议建模
3.1 JSON 契约
codes.side.color/**(上游 CSS Color 4 引擎,原样编译)
↓ ColorValue / ColorSpaces / GamutMapper
AcpBridge.kt(@CName × 1,6 op 扁平 JSON 进出)
↓ UTF-8 JSON
N-API acpRender()
↓
ArkUI Index.ets(四 Tab)
请求与响应:
请求 : {"op":"spaces"}
{"op":"parse","text":"oklch(0.7 0.15 none)"}
{"op":"convert","space":"okhsl","comps":"210,1,0.65","missing":"","alpha":1.0,"to":"srgb","map":"fast"}
{"op":"track","space":"okhsl","comps":"210,1,0.65","channel":"h","mode":"independent","map":"fast","width":720,"height":36}
{"op":"plane","space":"okhsv","comps":"210,1,1","xChannel":"s","yChannel":"v","map":"fast","size":360}
{"op":"gamut","text":"oklch(0.95 0.23 145)"}
响应 : {"ok":true,"op":"parse","space":"oklch","components":{"l":0.7,"c":0.15,"h":null},
"alpha":1.0,"css":"oklch(0.7 0.15 none)","hexLast":"#E7729BFF","argb":4293358135,
"inGamut":true,"missing":"h","pngBase64":...}
{"ok":false,"error":"...","index":12}
三个关键设计:其一,comps 是按空间通道顺序的逗号分隔串,none 直接写进序列("none,1,0.65"),桥内拼缺失位图交给 space.color(comps, alpha, missing)——这是库公开的构造口,ColorValue 的构造器是 internal 的,不绕开库的校验自造颜色;其二,光栅 op 的 map 字段三档(fast=ChromaReduction(ClosedForm)、css=Css()、clip=Clip)正好是库的三种 GamutMapping,与上游 PlaneRendering.Fast / Canonical 的取舍同构;其三,解析失败回传 index——CssColorParseException 自带出错位置,ArkTS 侧能指出「第 12 个字符不对」。
3.2 颜色的进出:两条路共用一个出口
private fun colorValueOf(json: MiniJson): ColorValue {
val text = json.str("text")
if (text.isNotBlank()) return ColorValue.parseCss(text)
val space = spaceById(json.str("space").ifBlank { "srgb" })
val (comps, bits) = compsArray(json, space)
return space.color(comps, json.dbl("alpha", 1.0), bits)
}
text 走 parseCss(连带 148 个命名色与 transparent),space+comps 走工厂——同一个 ColorValue 出口,后续 toCssString / toHexString / components() 全是库的公开 API。数值出参做了 1e-5 舍入去零(0.32549 而非 0.32549000001),argb 用无符号长整型输出(0xFF53B0AE → 4283674798),ArkTS 直接给 Image/色块当颜色用。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI,解析异常单独带 index:
@CName("AcpRender")
fun acpRender(request: String): String {
return try {
val json = MiniJson.parse(request)
when (json.str("op")) { "spaces" -> ...; "parse" -> ...; /* 6 op 分发 */ }
} catch (e: CssColorParseException) {
"""{"ok":false,"error":${jsonString(e.message ?: e.toString())},"index":${e.index}}"""
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
}
非法 JSON、未知空间、通道数不匹配、超界尺寸(width 钳 16…2048、size 钳 64…720),全部走同一个 ok:false 通道。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libacp_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosacp.so)
│ AcpRender
▼
UTF-8 JSON + DisposeString
与 KStore 的 2 符号不同,这里 1 个符号承载 6 个 op——颜色操作的形态天然是「一进一出无状态」,op 分发收在 Kotlin 侧,N-API 层只做搬运。
4.2 Kotlin/Native 导出函数
@CName("AcpRender")
fun acpRender(request: String): String // spaces / parse / convert / track / plane / gamut 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
AcpRender | acpRender(req) | 6 op 分发:ColorSpaces.all 元数据、parseCss/parseHex、ColorValue.to()、GamutMapper 光栅化、isInGamut/maxChroma/cusp |
llvm-nm -D 双 ABI 验证均为全局 T 符号:AcpRender、libohosacp_symbols。
4.3 C++ N-API 方法分发
static napi_value NapiAcpRender(napi_env env, napi_callback_info info) {
/* napi_typeof 校验必须是 string(两段式取串:先量长度再 vector<char> 拷贝)
OH_LOG_INFO 打字节数
const char *raw = AcpRender(request.c_str());
napi_create_string_utf8 包装返回后,必须 libohosacp_symbols()->DisposeString(raw) */
}
napi_property_descriptor desc[] = {
{"acpRender", nullptr, NapiAcpRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
KN 返回的字符串必须经 DisposeString 归还 Kotlin 运行时,绝不 free()。libohosacp_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohosacp SHARED IMPORTED)
set_target_properties(ohosacp PROPERTIES
IMPORTED_LOCATION ${KN_SO} # entry/libs/${OHOS_ARCH}/libohosacp.so
IMPORTED_NO_SONAME TRUE)
add_library(acp_napi SHARED napi_init.cpp)
target_link_libraries(acp_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosacp)
OHOS_ARCH 为空或 libohosacp.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const acpRender: (request: string) => string;
第 5 阶段:光栅化与 ArkUI 宿主
5.1 光栅化:像素全部出自库自己
滑轨与平面是 Compose 版的灵魂(ChannelSlider 的渐变轨、ChannelPlane 的二维色场),鸿蒙侧的出图路径不是重写颜色数学,而是调用库为此准备的批量接口——GamutMapper 的文档原话就是「many give a slider track or a plane」:
// 平面:按源空间通道数打包,一次批量映射成 0xAARRGGBB
val src = DoubleArray(size * size * n)
for (row in 0 until size) {
val yv = yMin + (1.0 - row.toDouble() / (size - 1)) * ySpan // 顶行 = y 通道 max
for (x in 0 until size) {
comps.copyInto(src, p, 0, n)
src[p + xChannel.index] = xMin + fraction * xSpan
src[p + yChannel.index] = yv
p += n
}
}
mapper.convertToArgb(src, 0, pixels, 0, size * size)
两个着色模式对应上游 ColoringMode:
- contextual:非滑动通道取自当前颜色(锚点 = 用户手里的色);
- independent:锚点 = sRGB 色域在该色相的
cusp(最鲜艳色)转换到当前空间——与库内部ChannelAnchors的思想一致;上游锚点代数是 internal 没进公开 API,example 侧用公开的RgbGamut.cusp(hue)近似。hue 通道的 independent 轨是真彩虹:每个像素单独取该色相的 cusp 做锚,而不是一条固定锚扫过去。
alpha 轨是唯一自绘的部分:convertToArgb 输出不透明像素(文档明确 opaque),透明度由桥内以 8px 棋盘格背景做混合——与 Compose 版 checkerboard 修饰同款视觉。
private fun blendOverChecker(r: Int, g: Int, b: Int, alpha: Double, x: Int, y: Int): Int {
val bg = if (((x / 8) + (y / 8)) and 1 == 0) 0xF6 else 0xD8
val out = { c: Int -> (alpha * c + (1 - alpha) * bg).toInt().coerceIn(0, 255) }
return (0xFF shl 24) or (out(r) shl 16) or (out(g) shl 8) or out(b)
}
PNG 编码器零依赖(zlib stored 块 + adler32 + CRC32 查表,继承 qrose 审核模式),Base64 用 kotlin.io.encoding.Base64(stdlib 自带)。
5.2 ArkTS 宿主:状态与刷新纪律
aboutToAppear(): void {
this.loadSpaces(); // spaces op 拉取 15 空间与通道定义,ArkTS 不硬编码任何空间
}
private convert(): void {
const req: ConvertReq = {
op: 'convert', space: this.spaceId, comps: this.compsString(),
missing: this.missingString(), alpha: this.alpha, to: this.spaceId, map: this.mapMode
};
const res = JSON.parse(acpRender(JSON.stringify(req))) as ConvertOk;
if (res.ok) { this.colorCard = res; } else { this.status = res.error ?? 'failed'; }
}
刷新纪律照 Compose 版 PlaneRasters 的缓存语义:平面拖动只走 convert(毫秒级)刷新颜色卡与指示点;track / plane 的 PNG 只在其依赖的非本通道分量、模式或映射变化时重画,且经串行序号守卫防止旧响应覆盖新请求。PNG 显示链路:util.Base64Helper().decodeSync(b64) → bytes.buffer.slice(byteOffset, byteOffset + byteLength) → image.createImageSource(...).createPixelMap()(slice 一步不能省,否则把整个 buffer 喂给解码器)。
5.3 对比度和错误边界
空间 chips 选中态深色反白;解析失败红卡显示 error 原文与 index;ok:false 统一进状态行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行,e as Record<string, Object> 读取 message,全程无 any。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosacp)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/
│ ├── AcpBridge.kt @CName("AcpRender") + 6 op + MiniJson
│ ├── ColorRaster.kt 滑轨/平面/alpha 棋盘格光栅化
│ └── PngEncoder.kt 零依赖 PNG(zlib stored)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.anycolorpicker、versionName 2.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/libohosacp_api.h
├── src/main/cpp/types/libacp_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :color:compileKotlinJvm :color:compileKotlinOhosArm64 :color:compileKotlinOhosX64
./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.anycolorpicker -m entry
四、完整代码对照
4.1 整体架构
codes.side.color/**(上游 CSS Color 4 引擎原样编译)
│ ColorValue / ColorSpaces(15) / parseCss / toGamut / isInGamut
ColorRaster.kt + PngEncoder.kt
│ GamutMapper.convertToArgb → 0xAARRGGBB → PNG → Base64
AcpBridge.kt + MiniJson
│ AcpRender(6 op)
libacp_napi.so
│ acpRender
Index.ets
│ JSON parse + @State 更新 + PixelMap 显示
▼
ArkUI 真机页面(四 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、Compose 模块移出 |
gradle/libs.versions.toml | 只钉 Kotlin 2.2.21-1.0.0 + kotlin-multiplatform 插件 |
build.gradle.kts(根) | 版本 2.0.0-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle.properties | JDK 21、版本号、kotlin.native 三开关 |
color/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,保留上游 jvmAndAndroid hierarchy |
8 × *View.kt / ColorValue.kt | 剥 @Immutable(各 2 行,算法零改动) |
color/src/ohosMain/.../GamutMemo.ohos.kt | 唯一新增库源文件(5 行,wasmJs 同款单例) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../AcpBridge.kt | 1 个 C ABI、6 op 分发、MiniJson、错误通道带 index |
nativeApp/.../ColorRaster.kt | 滑轨/平面/alpha 光栅化、cusp 锚点、真彩虹 |
nativeApp/.../PngEncoder.kt | 零依赖 PNG(zlib stored + adler32 + CRC32) |
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 四 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(22 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | ColorValue.parseCss(text) | CSS/hex/命名色解析(CssColorParseException 带 index) |
| 引擎 | space.color(comps, alpha, missing) | 按空间构造颜色(桥的 comps 入口) |
| 引擎 | value.to(space) | 任意空间互转(CSS §11.2 无力色处理) |
| 引擎 | value.toCssString / toHexString | 现代/legacy CSS 与双序 hex 输出 |
| 引擎 | value.isInGamut / toGamut | 色域判定与映射(Css / ChromaReduction / Clip) |
| 引擎 | Srgb.gamut.cusp(hue) / maxChroma(l, h) | 色相最鲜艳色 / 最大色度(independent 锚点来源) |
| 光栅 | GamutMapper.convertToArgb(src, off, dst, off, count) | 批量源空间分量 → 0xAARRGGBB(滑轨/平面像素出口) |
| Native | AcpRender | C ABI JSON 导出(6 op) |
| N-API | acpRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | convert() / renderTrack() / renderPlane() | 触发操作并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(acpRender(JSON.stringify({
op: 'parse', text: '#53B0AE'
}))) as ParseOk;
// res.ok === true; res.space === 'srgb'
// res.components === { r: 0.32549, g: 0.6902, b: 0.68235 }
// res.hexLast === '#53B0AEFF'; res.argb === 4283674798; res.inGamut === true
val value = ColorValue.parseCss("#53B0AE")
value.toCssString() // color(srgb 0.32549 0.6902 0.68235)
value.toHexString(HexAlpha.Last) // #53B0AEFF
value.to(Oklch)[Oklch.H] // null —— none 语义随空间转换传递
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:整编颜色模型而不是提取「转换内核」
color 模块剥 24 行注解后依赖为零,没有「抽内核」的必要——15 空间、矩阵融合、converter 缓存、Okhsl/Okhsv 闭式解全部是上游原件进 .so。抽内核反而引入漂移风险:色域映射的收敛容差、无力色位图的传递顺序,手抄一遍就可能变味。
决策 2:工具链钉 2.2.21-1.0.0,Gradle 降 8.14.1
零依赖意味着没有 kotlinx 制品的 stdlib 元数据约束,选社区验证最充分的 2.2.21 旧线;代价是 Gradle 从上游 9.7.1 降到 8.14.1(2.2.21 线不支持 9.x,qrose 先例)。与 KStore 的 2.4.20 线留 9.x 正好相反——两条工具链线的经验不能互相套用,判据始终是「依赖制品要什么 stdlib、Kotlin 线支持什么 Gradle」。
决策 3:Compose 模块留盘隔离,光栅化走库的批量 API
上游 UI 三模块站在 CMP 1.12 上,社区鸿蒙 CMP 是 1.9.2,接 Painter 不是本 fork 能承载的工程量。但 GamutMapper.convertToArgb 本来就是库为滑轨/平面设计的批量像素出口——光栅化不是「重新实现取色器渲染」,而是「把 Compose Canvas 里画的东西改用 PNG 送过去」,每个像素的颜色值都出自上游引擎。等社区 compose.ui ohos 制品与上游对齐后,Compose 模块接回,光栅化降级为 fallback。
决策 4:comps 逗号序列化 + none 位图,不传嵌套数组
桥的请求解析是手写 MiniJson(扁平 map,继承 qrose 审核模式),嵌套数组会显著增加解析器复杂度。comps 按空间通道顺序逗号分隔、none 直接写进序列,桥内拼位图交给 space.color()——序列化形态跟着库的构造口走,ArkTS 侧 values.join(',') 一行可得。
决策 5:JVM 同路径冒烟替代上游测试集
上游 commonTest 依赖 serialization/resources 读 JSON 参考数据,fork 构建里测试源集不进构建。交叉验证改用与桥逐字段同调用路径的 JVM 冒烟集(parse/convert/hex/gamut/批量像素七个样例)——它的价值不在覆盖面,而在「桥发的每个请求、库算的每个响应,JVM 上都能复现」;真机读数再与冒烟输出对账(见 6.4),三层同值才算闭环。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Temurin 21.0.12.1)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
与桥完全相同的 color API 调用路径(parseCss → toCssString / toHexString / mapper.convertToArgb),七个样例:
#53B0AE -> space=srgb css=color(srgb 0.32549 0.6902 0.68235) hexLast=#53B0AEFF argb=0xFF53B0AE inGamut(srgb)=true
oklch(0.7 0.15 none) -> space=oklch css=oklch(0.7 0.15 none) hexLast=#E7729BFF inGamut=true
hsl(210 100% 50%) -> space=hsl hexLast=#0080FFFF argb=0xFF0080FF inGamut=true
color(display-p3 1 0 0) -> hexLast=#FF0B0CFF inGamut(srgb)=false ← P3 红正确判定出域并被映射
rgb(83 176 174 / 50%) -> css=color(srgb 0.32549 0.6902 0.68235 / 0.5) hexLast=#53B0AE80
red -> space=srgb hexLast=#FF0000FF ← 命名色
oklch(0.95 0.23 145) -> hexLast=#B5FFB7FF inGamut=false ← 超饱和正确出域
#53B0AE -> oklch l=0.70056 c=0.08823 h=193.43 ← 跨空间换算
hsl s-ramp: 0xFF808080 0xFF5580AA 0xFF2A80D5 0xFF0080FF ← H=210 固定 S 0→100 的滑轨像素
legacy: hsl(210, 100%, 50%) ← legacy 语法输出
| 钉住什么 | 样例 |
|---|---|
| hex 往返 | #53B0AE → #53B0AEFF |
| none 分量 | oklch(0.7 0.15 none) 的 h 为 null 且不炸 |
| 色域判定与映射 | display-p3 红、oklch(0.95 0.23 145) 出域后分别映射为 #FF0B0C / #B5FFB7 |
| alpha | rgb(... / 50%) → #53B0AE80 |
| 批量像素 | HSL S 滑轨四点:灰 0xFF808080 → 纯蓝 0xFF0080FF(convertToArgb 正是桥的出图路径) |
6.3 HAP 验证
:color:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
llvm-nm -D: AcpRender / libohosacp_symbols 均为全局 T 符号(双 ABI)
copyKnSoArm64 / copyKnSoX64: finished(三件套回填 entry/libs)
BuildNativeWithCmake: finished(双 ABI 编出 libacp_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 7.9 MB,内含双 ABI 各三件套(libacp_napi.so / libohosacp.so / libc++_shared.so);HAP 内 strip 后 KN so 每 ABI 约 2.4 MB(debugShared 未 strip 时 arm64 约 4.7 MB / x86_64 约 4.4 MB,含 Kotlin/Native 运行时)。
6.4 功能用例(真机 arm64)
- 启动进入取色器 Tab,15 个空间 chips 由
spacesop 动态生成(ArkTS 侧零硬编码); - okhsl 平面拖动取色,颜色卡 css/hex/argb 实时刷新,通道滑轨 PNG 随平面通道联动重画;
- 解析 Tab 输入
#53B0AE:空间徽章srgb,逐通道r 0.3255 / g 0.6902 / b 0.6824,css=color(srgb 0.32549 0.6902 0.68235),cssLegacy=rgb(83, 176, 174),hexLast #53B0AEFF、hexFirst #FF53B0AE、argb 4283674798、inGamut true——与 6.2 的 JVM 冒烟输出逐位一致(4283674798即0xFF53B0AE); - 样例
color(display-p3 1 0 0)与oklch(0.95 0.23 145):inGamut 徽章变红,验证色域判定在真机生效; - 色域 Tab 三方法色块:clip 与 css 的映射结果肉眼可辨差异,maxChroma / cuspCss / lightness / hue 行随输入刷新;
- 平面 Tab okhsv 360px fast 模式秒级出图,点按取色色卡联动;
- 篡改输入为非法串(如
oklch(0.7),红卡显示错误原文与出错位置index,应用不崩。
6.5 验证结论
颜色模型、光栅化、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,解析 Tab 的读数与 JVM 交叉验证逐位一致——真机上算颜色的就是上游那 43 个 commonMain 文件(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/anyColorPicker.git
cd anyColorPicker
export JAVA_HOME="/path/to/jdk-21"
./gradlew :color:compileKotlinJvm :color:compileKotlinOhosArm64 :color:compileKotlinOhosX64 # klib
./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.anycolorpicker -m entry
hdc shell hilog | grep AcpNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:color真正编译成ohosArm64/ohosX64动态库——色域映射与 Okhsl 几何要在设备上跑才算数; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游 wrapper 9.7.1 与 HarmonyOS Kotlin 2.2.21 不兼容,必须降 8.14.1——wrapper jar 不用动,改
distributionUrl即可;KStore 那套「2.4.20 线留 9.x」的经验在这里不适用,工具链线的经验不能跨线搬运; runtime-annotation没有 ohos 变体:api(androidx.compose.runtime:runtime-annotation)会让原生解析失败——它只提供@Immutable注解,剥掉(8 文件 24 行)比找一个替代制品干净;gamutMemo是全库唯一 expect,ohos actual 照 wasmJs 写单例(新内存模型共享安全),不要照 ios 写@ThreadLocal——旧写法在新内存模型下已无必要;- 上游
commonTest依赖 serialization/resources 读 JSON 参考数据,fork 构建不声明测试依赖 + 根构建禁用Ohos*Test*,测试文件零改动留盘; GamutMapper.convertToArgb输出不透明像素(库文档明确 opaque)——alpha 的视觉呈现要么 ArkTS 侧叠透明度,要么桥内自绘棋盘格混合,本仓选后者;- independent 着色的锚点代数(
ChannelAnchors)在 foundation 模块里是 internal,没进 color 公开 API——example 侧用公开的RgbGamut.cusp(hue)近似,语义同为「该色相最鲜艳参考色」; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(libacp_napi.so/libohosacp.so/libc++_shared.so)缺一不可; - ArkTS 只能 import
libacp_napi.so,import KN so 会报does not provide an export named 'acpRender'——HarmonyOS Kotlin 的 CAdapter 不会把@CName业务符号挂到 ArkTS; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();HAP 打包用 DevEco 自带 JBR,系统 JDK 8/17 会让hvigorw报不相干的错; - DevEco 打开工程后会自动重建
.clang-tidy/.clangd,连同.idea/、entry/build/、entry/libs/、oh_modules/一起 gitignore,否则混进提交(根锚定的/build/盖不住嵌套目录)。
8.2 已知问题
- 鸿蒙侧无 Compose UI:滑轨/平面是 KN 光栅化 PNG 近似,无 Material 主题、无动画、无障碍语义与本地化数字格式;
- independent 着色锚点是 cusp 近似,hue 轨为逐像素 cusp 的真彩虹,与上游
ChannelAnchors在个别空间有细微差别; fast(ChromaReduction)与canonical(Css)两种映射在出域色上有细微差异——这是上游PlaneRendering.Fast / Canonical的同款取舍,不是适配引入的;- PNG 用 zlib stored 未压缩,360×360 平面数百 KB 量级,超大平面偏胖;
colorpicker-foundation/colorpicker-material3/color-compose/sample/screenshot-tests剥离(源文件留在树里),上游 wasm/ios/android 目标一并剥离,本 fork 不发 Maven Central;.github/workflows保留上游原样(仓库托管在 AtomGit,Actions 不执行),已在适配文档注明;- HAP 安装必须使用与 bundle name
org.terminator.ohos.anycolorpicker匹配的签名。
8.3 未来优化方向
- 等社区
compose.uiohos 制品与上游 CMP 版本对齐后接回 Compose 模块,光栅化降级为 fallback; - 把
ColorPickerDialog的「新旧色对比 + 空间切换」补进 ArkTS Demo; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)验收矩阵与 uitest 自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 color(43 个 commonMain,CSS Color 4 引擎)
→ 依赖盘点:零依赖(仅 @Immutable 注解制品)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 9.7.1 降 8.14.1)
→ 根构建替换(cmp_qrose 模式,Compose 三模块留盘隔离)
→ 剥 @Immutable × 8 文件(24 行,算法零改动)+ 唯一 5 行 ohos actual
→ ohosArm64 / ohosX64 KLIB
→ C ABI(@CName × 1,6 op JSON)+ comps 逗号序列化 + none 位图
→ 光栅化(GamutMapper.convertToArgb 批量像素 + cusp 锚点 + alpha 棋盘格)
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析 + PixelMap 显示
→ ArkUI 真机四 Tab 验收(解析读数与 JVM 逐位一致)
9.2 封装层次
- 引擎层:上游原版
ColorValue、15 空间、矩阵融合与 converter 缓存、Okhsl/Okhsv 闭式解; - 光栅层:
GamutMapper.convertToArgb批量出像素,cusp 锚点 independent 着色,零依赖 PNG 编码; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、PNG 解码(Base64 → PixelMap)、操作分发与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 路线跟着依赖形态走:KStore 靠 kotlinx ohos 制品线才能整编,anyColorPicker 剥 24 行注解就零依赖——先盘点再定路线,零依赖库的正确答案是「剥掉注解直编」,重依赖库才是「验证制品线」;
- 工具链线的经验不能跨线搬运:2.2.21 线降 Gradle 8.14.1、2.4.20 线留 9.x,判据是 Kotlin 线对 Gradle 的支持范围与依赖制品的 stdlib 元数据,不是「上游用什么就留什么」;
- 出图路径的钥匙藏在库自己的 API 里:
GamutMapper的文档写着「many give a slider track or a plane」——上游早已为滑轨/平面准备好批量像素出口,鸿蒙侧不需要重新实现取色器渲染,把 PNG 送过去即可;找到这把钥匙比写一个光栅化器便宜得多,也可靠得多。
9.4 适配成果
当前适配已完成:
- anyColorPicker
2.0.0CSS Color 4 引擎的双 ABI Kotlin/Native 编译(上游commonMain文件零重写,仅剥 24 行注解 + 5 行 ohos actual); - 1 个 C ABI 符号承载 6 op 的桥接层与 C++ N-API 包装(
llvm-nm验证全局 T 符号); - Kotlin/Native 光栅化出图(滑轨/平面/alpha 棋盘格,全部经库自己的
GamutMapper); - DevEco Stage ArkUI 四 Tab 验收页面(15 空间取色、解析、色域三方法、平面矩阵);
- JVM 同路径交叉验证全绿 + HAP 构建(双 ABI 六件套 so,约 7.9MB)、签名、设备安装和真机验证(解析读数与 JVM 逐位一致);
- AtomGit 项目文档、OpenHarmony README(中英双份,中文主文档 22 节)和真机效果图。
适配过程 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)