开源鸿蒙平台 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / haze 同款旧线)
JDK21(Temurin 21.0.12.1,org.gradle.java.home 指向本机 JDK)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 wrapper 9.7.1 必须降级——2.2.21 线不支持 Gradle 9.x)
第三方依赖零(color 模块剥注解后无任何外部依赖)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.anycolorpicker
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
color43纯 Kotlin + runtime-annotation(仅 @Immutable),零 UI、零 kotlinx剥注解后整体编译(jvm + 双 ohos)
color-compose3compose.ui.graphics(Compose Color 互转)留盘不进构建
colorpicker-foundation31compose.runtime / foundation / ui + coroutines留盘不进构建
colorpicker-material328material3留盘不进构建

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 方法覆盖内核
AcpRenderacpRender(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.propertiesJDK 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.kt1 个 C ABI、6 op 分发、MiniJson、错误通道带 index
nativeApp/.../ColorRaster.kt滑轨/平面/alpha 光栅化、cusp 锚点、真彩虹
nativeApp/.../PngEncoder.kt零依赖 PNG(zlib stored + adler32 + CRC32)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 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(滑轨/平面像素出口)
NativeAcpRenderC ABI JSON 导出(6 op)
N-APIacpRender(req)向 ArkTS 暴露能力
ArkTSconvert() / 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
alphargb(... / 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)

  1. 启动进入取色器 Tab,15 个空间 chips 由 spaces op 动态生成(ArkTS 侧零硬编码);
  2. okhsl 平面拖动取色,颜色卡 css/hex/argb 实时刷新,通道滑轨 PNG 随平面通道联动重画;
  3. 解析 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);
  4. 样例 color(display-p3 1 0 0) 与 oklch(0.95 0.23 145):inGamut 徽章变红,验证色域判定在真机生效;
  5. 色域 Tab 三方法色块:clip 与 css 的映射结果肉眼可辨差异,maxChroma / cuspCss / lightness / hue 行随输入刷新;
  6. 平面 Tab okhsv 360px fast 模式秒级出图,点按取色色卡联动;
  7. 篡改输入为非法串(如 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :color 真正编译成 ohosArm64 / ohosX64 动态库——色域映射与 Okhsl 几何要在设备上跑才算数;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 wrapper 9.7.1 与 HarmonyOS Kotlin 2.2.21 不兼容,必须降 8.14.1——wrapper jar 不用动,改 distributionUrl 即可;KStore 那套「2.4.20 线留 9.x」的经验在这里不适用,工具链线的经验不能跨线搬运;
  4. runtime-annotation 没有 ohos 变体:api(androidx.compose.runtime:runtime-annotation) 会让原生解析失败——它只提供 @Immutable 注解,剥掉(8 文件 24 行)比找一个替代制品干净;
  5. gamutMemo 是全库唯一 expect,ohos actual 照 wasmJs 写单例(新内存模型共享安全),不要照 ios 写 @ThreadLocal——旧写法在新内存模型下已无必要;
  6. 上游 commonTest 依赖 serialization/resources 读 JSON 参考数据,fork 构建不声明测试依赖 + 根构建禁用 Ohos*Test*,测试文件零改动留盘;
  7. GamutMapper.convertToArgb 输出不透明像素(库文档明确 opaque)——alpha 的视觉呈现要么 ArkTS 侧叠透明度,要么桥内自绘棋盘格混合,本仓选后者;
  8. independent 着色的锚点代数(ChannelAnchors)在 foundation 模块里是 internal,没进 color 公开 API——example 侧用公开的 RgbGamut.cusp(hue) 近似,语义同为「该色相最鲜艳参考色」;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(libacp_napi.so / libohosacp.so / libc++_shared.so)缺一不可;
  10. ArkTS 只能 import libacp_napi.so,import KN so 会报 does not provide an export named 'acpRender'——HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();HAP 打包用 DevEco 自带 JBR,系统 JDK 8/17 会让 hvigorw 报不相干的错;
  12. 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.ui ohos 制品与上游 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 三条经验

  1. 路线跟着依赖形态走:KStore 靠 kotlinx ohos 制品线才能整编,anyColorPicker 剥 24 行注解就零依赖——先盘点再定路线,零依赖库的正确答案是「剥掉注解直编」,重依赖库才是「验证制品线」;
  2. 工具链线的经验不能跨线搬运:2.2.21 线降 Gradle 8.14.1、2.4.20 线留 9.x,判据是 Kotlin 线对 Gradle 的支持范围与依赖制品的 stdlib 元数据,不是「上游用什么就留什么」;
  3. 出图路径的钥匙藏在库自己的 API 里:GamutMapper 的文档写着「many give a slider track or a plane」——上游早已为滑轨/平面准备好批量像素出口,鸿蒙侧不需要重新实现取色器渲染,把 PNG 送过去即可;找到这把钥匙比写一个光栅化器便宜得多,也可靠得多。

9.4 适配成果

当前适配已完成:

  • anyColorPicker 2.0.0 CSS 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐