☰
Compose Multiplatform 三方库 KMPalette 的 OpenHarmony 鸿蒙化适配实战(Kotlin/Native 编译 .so + NAPI 像素级桥接 + ArkUI
2026/9/28 7:02:42 网站建设 项目流程

Compose Multiplatform 三方库 KMPalette 的 OpenHarmony 鸿蒙化适配实战(Kotlin/Native 编译 .so + NAPI 像素级桥接 + ArkUI 图片取色)

库版本:KMPalette 2.1.0(androidx-palette 模块,Google Palette 的 Kotlin 移植)|验证环境:Compose Multiplatform 生态 / Kotlin 2.2.21-1.0.0(鸿蒙定制版)|DevEco Studio 26.0.0|DevEco 模拟器|HarmonyOS 7.0.0(API 26)

「从一张图片里提取主色调」是动态主题的第一步——Android 12 的 Material You 从壁纸取色,鸿蒙的「一镜到底」同样需要从壁纸提取主题色。KMPalette(296★)是 Kotlin 生态里图片取色的事实标准库:它把 Google 的 androidx.palette 移植成了纯 Kotlin,核心算法只有 8 个文件、只依赖kotlin.math,零平台依赖。本文记录我把它完整跑上鸿蒙的全过程:与上一篇 MaterialKolor(种子色 → 配色方案)正好组成完整故事——KMPalette 负责图片 → 主色调,MaterialKolor 负责主色调 → 全应用配色,两库串联就是鸿蒙动态主题的完整算法链。这次的新课题是像素级桥接:68 万个像素如何高效穿越 ArkTS → NAPI → Kotlin/Native。

*先睹为快:DevEco 模拟器实测,KMPalette 官方演示图提取的主色调 + 六大目标色 + 全部色板,取色耗时 6ms* ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ccf2cf506ee94ce99b16af0e480a7efe.png)

一、适配目标与整体链路

目标:在鸿蒙模拟器里跑一个 ArkTS 应用,切换测试图片时,真实调用Kotlin/Native 里的 androidx.palette 取色算法,把主色调(Dominant)、六大目标色(Vibrant/Light Vibrant/Dark Vibrant/Muted/Light Muted/Dark Muted)、全部色板(Swatches)渲染出来——以此证明整条链路真正打通,而不是 UI 上摆几个写死的颜色。

整体链路:

ArkTS (Index.ets) │ import kpalette_napi from 'libkpalette.so' │ PixelMap → readPixelsToBuffer → ArrayBuffer(ARGB_8888 像素) ▼ NAPI 调用 generatePalette(buffer, width, height) libkpalette.so ← C++ NAPI 薄层 │ ▼ extern "C" 调用 libohospalette.so ← Kotlin/Native (ohosArm64 / ohosX64) │ ▼ palette 模块 → androidx-palette(8 个纯 Kotlin 算法文件)

与 MaterialKolor 适配(纯标量入参)最大的不同:这次的输入是整张图的像素。1090×624 的演示图就是 68 万个像素、2.7MB 数据,跨语言边界的传输方式直接决定成败——这是本文的核心看点。

二、为什么是 KMPalette:与 MaterialKolor 串联的完整故事

上一篇 MaterialKolor 适配验证了「种子色 → 完整配色方案」,但种子色从哪来?Material You 的答案是壁纸取色。在 Kotlin 生态里,这个「图片 → 主色调」的环节由 KMPalette 承担:

环节库输入 → 输出鸿蒙化状态
图片 → 主色调KMPalette(androidx-palette)像素 → Dominant/六大目标色本文
主色调 → 配色方案MaterialKolor(material-color-utilities)种子色 → 27 角色色 + Tonal 色阶上一篇已完成

两库串联 =壁纸 → 主色调 → 全应用配色,鸿蒙动态主题的完整算法链就此闭环。

选型时同样先过 skiko 筛查:KMPalette 的核心模块androidx-palette是 androidx Palette 的 Kotlin 移植,8 个文件全是色彩量化算法(颜色直方图、VBox 切分、目标色匹配),只依赖kotlin.math,不碰 skiko、不碰 okio、不碰 coroutines。它的 Compose 包装层(kmpalette-core,rememberDominantColorState等)在鸿蒙端由 ArkUI 替代即可。算法核心 100% 复用原库源码。

三、工程结构

kmpalette-ohos-demo/ ├── palette/ # 库模块:androidx-palette 源码(8 个算法文件) │ └── src/commonMain/kotlin/com/kmpalette/palette/ # 100% 复用上游 ├── example/ │ ├── nativeApp/ # Kotlin/Native 桥接层 → libohospalette.so │ │ └── src/ │ │ ├── commonMain/kotlin/PaletteBridge.kt # 算法包装 + JSON 序列化 │ │ └── ohosMain/kotlin/PaletteExport.kt # @CName 导出 C ABI │ └── ohosApp/ # ArkTS 鸿蒙应用 │ └── entry/src/main/ │ ├── cpp/napi_init.cpp # C++ NAPI 薄层(像素 ArrayBuffer → int*) │ ├── ets/pages/Index.ets # ArkUI 取色页面 │ └── libs/{arm64-v8a,x86_64}/ # 双 ABI .so └── settings.gradle.kts / build.gradle.kts / gradle.properties

四、适配过程:四个关键步骤

### 4.1 Gradle 工程配置(鸿蒙定制工具链)

与 MaterialKolor 适配完全同构:pluginManagement第一位放 eazytec Nexus 仓库,Kotlin2.2.21-1.0.0定制版,库模块与桥接模块声明ohosArm64()+ohosX64()双 target:

// palette/build.gradle.kts(拷入 8 个算法源文件)plugins{kotlin("multiplatform")version"2.2.21-1.0.0"}kotlin{ohosArm64()ohosX64()sourceSets{commonMain.dependencies{/* 零依赖 */}}}// example/nativeApp/build.gradle.ktskotlin{ohosArm64{binaries{sharedLib{baseName="ohospalette"}}}ohosX64{binaries{sharedLib{baseName="ohospalette"}}}sourceSets{commonMain.dependencies{api(project(":palette"))}}}

4.2 源码改造:两类注解的清理

androidx-palette 上游带着 Android 的历史包袱,两类注解在鸿蒙工程里都无法解析:

  • androidx.annotation(@ColorInt、@FloatRange、@IntRange):纯装饰性注解,删 import + 删用法即可。坑在三种形态:独立行(@ColorInt\n val rgb: Int)、行内参数(@ColorInt color: Int,)、属性前缀(@get:ColorInt)——正则要分别处理;
  • @Poko(编译期插件):Palette和Swatch两个类在用。这次运气好——两个类都没被用作 Map key(算法内部用ArrayList持有 Swatch),直接删注解与 import,无需手写 equals/hashCode。

判断方法与上一篇相同:全局搜@Poko,再搜类名是否出现在Map<、Set<泛型参数里。这次零命中,算法源码零逻辑改动。

4.3 像素级桥接:这次的核心课题

与 MaterialKolor(4 个标量入参)不同,KMPalette 的输入是整张图的像素。跨语言边界设计:

ArkTS 侧——PixelMap 读出 ARGB_8888 的 ArrayBuffer,连同宽高一起传给 NAPI:

// Index.ets 核心调用constpixelMap:image.PixelMap=awaitsource.createPixelMap({desiredPixelFormat:image.PixelMapFormat.ARGB_8888})constinfo=awaitpixelMap.getImageInfo()constbyteCount=info.size.width*info.size.height*4// 每像素 4 字节constbuffer=newArrayBuffer(byteCount)awaitpixelMap.readPixelsToBuffer(buffer)constraw:string=kpalette_napi.generatePalette(buffer,info.size.width,info.size.height)constparsed=JSON.parse(raw)asPaletteJson

C++ NAPI 层——napi_get_arraybuffer_info拿到裸指针,校验byteLength / 4 == width * height后直接把int*传下去,零拷贝:

// napi_init.cppvoid*data=nullptr;size_t byteLength=0;napi_get_arraybuffer_info(env,args[0],&data,&byteLength);constintpixelCount=static_cast<int>(byteLength/4);if(pixelCount!=width*height){/* range error */}constchar*raw=OhosPaletteGenerate(static_cast<constint*>(data),pixelCount,width,height);// raw 是 JSON 字符串,转 napi string 后立即 OhosPaletteFree(raw)

Kotlin 侧——@CName导出 C ABI,把CPointer<IntVar>拷贝为IntArray后立即交给算法:

// PaletteExport.kt@CName("OhosPaletteGenerate")funohosPaletteGenerate(pixelsPtr:CPointer<IntVar>?,pixelCount:Int,width:Int,height:Int,):CPointer<ByteVar>{valjson=try{requireNotNull(pixelsPtr){"pixelsPtr is null"}require(pixelCount==width*height){"size mismatch"}valpixels=IntArray(pixelCount){i->pixelsPtr[i]}// 一次性拷贝PaletteBridge.generatePaletteJson(pixels,width,height)}catch(t:Throwable){"{\"error\":\"${t.message?:"unknown"}\"}"}returnjsonToCString(json)// nativeHeap 分配,null 结尾}@CName("OhosPaletteFree")funohosPaletteFree(ptr:CPointer<ByteVar>?){ptr?.let{nativeHeap.free(it.rawValue)}// 谁分配谁释放}

像素内存的所有权链:ArkTS 的 ArrayBuffer 由 NAPI 直接读裸指针(零拷贝)→ Kotlin 侧IntArray(pixelCount) { i -> pixelsPtr[i] }一次性拷入 Kotlin 堆(之后 C 侧 buffer 可任意释放)→ 返回的 JSON 字符串在 nativeHeap 分配,C++ 侧转成 napi string 后立即OhosPaletteFree。每个环节谁分配谁释放,无泄漏无悬垂。

PaletteBridge把Palette.from(pixels, width, height).generate()的结果序列化为 JSON:全部 Swatch(rgb + population + hsl)、六大目标色(无匹配为 null)、主色调。

4.4 编译与部署

# 1. 编译双 ABI release .so.\gradlew.bat :example:nativeApp:linkReleaseSharedOhosArm64 ` :example:nativeApp:linkReleaseSharedOhosX64# 2. 部署到 entry/libs(含 Kotlin/Native 运行时 libc++_shared.so)entry/libs/arm64-v8a/libohospalette.so entry/libs/x86_64/libohospalette.so# 3. 构建 HAP(命令行:JAVA_HOME 指向 DevEco JBR + DEVECO_SDK_HOME 指向 sdk 根目录)$env:JAVA_HOME ="C:/Program Files/Huawei/DevEco Studio/jbr"$env:DEVECO_SDK_HOME ="C:/Program Files/Huawei/DevEco Studio/sdk"hvigorw.bat assembleHap--mode module-p product=default# 4. 安装到模拟器(注意:hdc 的路径参数必须用反斜杠)hdc-t 127.0.0.1:5555 install-r entry-default-unsigned.hap

五、踩坑记录(4 个)

坑现象解法
注解三种形态@ColorInt删不干净:独立行/行内参数/@get:前缀正则分别处理 + CRLF 行尾\r?
cinterop 下标pixelsPtr[i]unresolved referenceimport kotlinx.cinterop.get(运算符扩展函数)
DEVECO_SDK_HOME命令行 hvigor 报 00303217/00303312显式指向DevEco Studio/sdk(根目录,含 default/hms/openharmony)
hdc 路径解析install报 no such file:正斜杠路径被拼接错乱本地路径一律用反斜杠C:\...

另一个值得记录的坑:ArkTS 严格模式的 Record 索引。palette.targets[name]在 ForEach 渲染里既报「Object is possibly null」又不好收窄,最终方案是在数据层把 Record 展开为类型化数组(TargetItem[]),渲染层只消费数组——ArkTS 的 UI 语法里连const局部声明都不允许,视图模型化是正解。

六、运行效果(DevEco 模拟器实测)

Demo 做成深色 UI,三张测试图片(KMPalette 官方演示图 / 品牌 Logo / 壁纸截图)一键切换,每次切换都真实走一遍 像素读取 → NAPI → 取色算法 → JSON 回传 → ArkUI 渲染。

官方演示图(1090×624,68 万像素)——主色调#E0E8F0、六大目标色、全部色板一次到位,取色耗时6ms:

*官方演示图:主色调 #E0E8F0(pop 1339)+ 六大目标色(#F09050 鲜活 / #B0D0E0 浅鲜活 / #2030A0 深鲜活 / #707080 柔和…)+ 全部色板,取色 6ms*

品牌 Logo(932×368)——紫色系图片,色板整体偏移:

*品牌 Logo:算法对紫色系图片输出完全不同的色板组合*

壁纸截图(1320×2232,294 万像素)——深色系壁纸:

*壁纸截图:294 万像素 4ms 完成取色,深色系色板*

真实性验证(hilog)——每次切换都有日志铁证:

KpaletteNapi: generatePalette w=1090 h=624 pixels=680160 KpaletteNapi: generatePalette result len=1447 head={"swatches":[{"rgb":-5189408,"population":650,... KpaletteNapi: generatePalette w=1320 h=2232 pixels=2946240 KpaletteNapi: generatePalette result len=1368 head={"swatches":[{"rgb":-16740144,... KpaletteNapi: generatePalette w=932 h=368 pixels=342976 KpaletteNapi: generatePalette result len=1448 head={"swatches":[{"rgb":-9947008,...

三张图三个不同的len与首 Swatch——算法对不同图片真实产出不同色板,非 mock。294 万像素的壁纸取色仅 4ms(色彩量化的 VBox 切分对像素数不敏感),68 万像素 6ms——纯内存计算,无任何 IO。

七、FAQ

Q1:68 万像素跨 NAPI 会不会很慢?不会。ArrayBuffer 走napi_get_arraybuffer_info拿裸指针零拷贝,Kotlin 侧一次IntArray拷贝(~2.7MB memcpy),实测全流程 4–6ms。

Q2:为什么不用 PixelMap 直接传?NAPI 跨语言边界传裸数据(ArrayBuffer/int*)最稳,传对象(PixelMap 句柄)需要跨运行时引用管理,复杂度陡增。极简切片原则:边界上只传数据和 JSON 字符串。

Q3:ARGB_8888 的字节序对得上吗?ArkTS 侧readPixelsToBuffer出来的是大端 ARGB 打包的 32 位序列,按int32读取后与 KotlinInt位表示一致,算法内部位运算(rgb >>> 16 & 0xFF)不受符号影响。

Q4:@Poko这次为什么能直接删?判断标准是类是否被用作 Map/Set key——Palette/Swatch都只在ArrayList里,没有哈希需求。上一篇的Hct命中过,这次零命中。

Q5:命令行构建 HAP 报 DEVECO_SDK_HOME 错误?DEVECO_SDK_HOME要指向DevEco Studio/sdk(根目录,下面有 default/hms/openharmony),指到sdk/default会报「找不到对应 SDK 版本」(00303312)。

Q6:hdc 传文件报 no such file?hdc 对正斜杠本地路径的拼接有 bug,install/file recv的本地路径一律用反斜杠。

八、总结与参考

KMPalette 适配完成,与 MaterialKolor 串联后,鸿蒙动态主题的算法链完整闭环:壁纸像素 →(KMPalette)→ 主色调 →(MaterialKolor)→ 27 角色色 + Tonal 色阶 → ArkUI 渲染。本篇验证的方法论增量是像素级桥接:ArrayBuffer 零拷贝读指针 + Kotlin 侧一次性 IntArray 拷贝 + JSON 字符串回传,全流程 4–6ms——证明 Kotlin/Native .so 不只能接标量,也能高效接大数据。改造量依旧趋近于零(删注解 + 零逻辑改动),再次印证「选型先筛 skiko,纯算法库优先」的选型逻辑。

OpenHarmony 三方库社区地址:https://atomgit.com/oh-tpc
OpenHarmony 三方库适配地址:https://atomgit.com/oh-tpc/KMPalette
github 三方库地址:https://github.com/jordond/KMPalette
官方文档地址:https://github.com/jordond/KMPalette/blob/main/README.md
鸿蒙定制仓库地址:https://maven.eazytec-cloud.com/nexus/repository/maven-public/
鸿蒙适配版:https://atomgit.com/oh-tpc/KMPalette

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询