本文记录 composable-sheep-lib 接入 OpenHarmony 的完整过程,覆盖纯 Canvas 绘制库的能力盘点、模型与绘制分离、真实模型编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、软件光栅化与 PNG 编码、ArkUI 真机页面、HAP 构建和真机验收。

composable-sheep-lib 是基于 Compose Multiplatform 纯 Canvas 的卡通羊绘制库:给定一个 Sheep 模型与尺寸策略,它提供 ComposableSheep(整羊:绒毛云 + 头 + 眼睛 + 墨镜 + 腿)、ComposableFluff(只有绒毛云)与 LoadingSheep(无限旋转 + 缩放脉动的加载羊)三个入口,绒毛按 FluffStyle(Uniform / UniformIntervals / Random / Manual)分块,腿部有 twoLegsStraight / fourLegs 两套预设,还带 showGuidelines 调试辅助线。本次适配与存储类库(如 KStore 的"真编译引擎")有本质区别:这是一个绘制库,公开 API 全部站在 androidx.compose.ui 上——因此适配把边界放在"模型与几何留、绘制指令换":羊的模型(Sheep / FluffStyle / Leg / SheepColor)与贝塞尔几何(圆周点、控制点)去 Compose 化后原样编译成 OpenHarmony KLIB,Canvas 绘制由 example/nativeApp 里的 Kotlin/Native 软件光栅化器 1:1 重放(与社区 qrose / Vico / Reorderable 的光栅化路线同源),ArkUI 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/composable-sheep-lib.git

上游仓库: nicole-terc/composable-sheep-lib

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

composable-sheep-lib 为 Compose Multiplatform 应用提供"羊"这一件事的全部答案:ComposableSheep(modifier) 一行出羊(必须指定尺寸策略,因为底层是 Canvas),Sheep 模型承载绒毛样式、头部角度、墨镜位移、腿与五路颜色,FluffStyle 四档决定绒毛云的分块节奏,LoadingSheep 把整羊变成旋转加载指示器,DrawScope.drawComposableSheep 系列扩展还能挂到任意 drawBehind。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这类轻量装饰性绘制能力,但上游构建(Kotlin 2.4.10 / Gradle 9.4.1 / AGP 9.2.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS 画一只"像的羊",页面可以显示,却无法证明羊的模型与几何在 OpenHarmony ARM64 设备上完成编译、链接和执行——那就是一次重写而不是适配。适配需要同时验证模型层、几何层、光栅化器、PNG 编码、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
公开 API 全在 Compose 上Composable / DrawScope / Color / Offset / Brush 全是 androidx.compose.* 类型,CMP 鸿蒙线(1.9.2)与上游快照(Compose 1.11 / Kotlin 2.4)无法混编。
工具链不一致上游 Gradle 9.4.1 + AGP 9.2.1 与鸿蒙工具链无法共存于一个 Gradle 构建;HarmonyOS Kotlin 2.2.21 线必须配 Gradle 8.14.1。
绘制语义跨不了桥DrawScope 指令、infiniteRepeatable 动画、Brush 渐变是 Compose 侧概念,C ABI 只能同步一来一回。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:模型与几何的 ohos 编译(去 Compose 化)、Compose 绘制的源码集隔离(留档不编译)、光栅化重放与 C ABI/N-API 桥接层。羊的形态完全由上游模型与几何决定,ArkTS 只负责传参数、解码 PNG 和展示。

1.2 库提供的能力

Demo 提供一个真实的羊绘制会话,对应桥接的全部入口:

  • 整羊(sheep):SheepRender 按模型渲染完整绒毛云(二次贝塞尔闭合路径)、头部(椭圆 + 眼睛 + 瞳孔 + 墨镜半圆 + 桥线)与腿(旋转圆角矩形),绘制顺序与上游 drawComposableSheep 一致:腿 → 绒毛 → 头;
  • 绒毛(fluff):action=fluff 对应 ComposableFluff,只画半径 size * 0.5 * 0.8 的绒毛云;
  • 点击换色:点击羊随机换绒毛颜色且排除当前色,与上游 Android demoapp 行为一致;
  • 全参数面板:FluffStyle 四档、Uniform 分块数 3…24、headAngle −30…30°、glassesTranslation −40…40px、双腿/四腿切换、八色色板、showGuidelines 辅助线;
  • 加载羊(loading):Tab 三对应 LoadingSheep,旋转 0→360°(FastOutSlowIn)+ 缩放 1↔0.8(Linear)脉动,旋转轴心 (0.5, 0.1) 与上游 TransformOrigin 一致,默认蓝色与上游 demoapp 一致;
  • 性能回显:页面显示每次渲染的实际耗时(真机实测 512×512 约 300ms,debug so)。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
action回显本次渲染类型(sheep / fluff)。
width / height / bytes产物 PNG 的尺寸与字节数。
pngBase64RGBA PNG 的 Base64 文本,ArkTS 解码为 PixelMap 显示。

模型侧完整能力(FluffStyle 分块策略、Leg 工厂、SheepColor 调色板、圆周点 / 贝塞尔控制点 / 中点几何)全部在上游 commonMain 里随编译进入 .so;JVM 侧保留 7 张冒烟渲染图 + alpha 通道计数交叉验证,证明 ohos .so 里执行的模型与几何没有漂移。

1.3 实现适配

维度要求
代码复用模型与几何(Sheep / FluffStyle / Leg / SheepColor / GeometryMaths / ListExtensions)原样编译,公式与注释一字未改;仅颜色载体 Color→Long、点载体 Offset→Pt 两个机械替换,外加 SheepColor.random(Random) 两个重载(全库唯一新增公共 API)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 demoapp/(纯 Android)留盘不进构建,Compose 绘制文件整体 git mv 到 src/composeMain。
绘制保真光栅化器 1:1 移植 parts/DrawLegs / DrawFluff / DrawHead / DrawEyes / DrawGlasses 的全部常量与公式,2×2 超采样抗锯齿;异常永远不跨 FFI。
依赖纪律库零第三方依赖(上游本来就只用 Compose),桥零序列化框架(手写 MiniJson)。
UI 完整三 Tab 覆盖上游三入口:羊(点击换色 + 全参数面板)、绒毛、加载羊(旋转脉动)。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。羊的绘制产物是 Kotlin/Native 侧生成的 PNG 像素;ArkTS Demo 只是把桥暴露的绘制能力与参数面画出来。LoadingSheep 的动画由 ArkTS animateTo 承担(等价于上游 graphicsLayer 的 rotationZ / scale),不跨帧回调 N-API。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认绘制 API 的 Compose 依赖面、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、HarmonyOS Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:模型与绘制分离 ── commonMain 去 Compose 化(Color→Long、Offset→Pt)、Compose 文件 git mv 到 composeMain
第 4 阶段:光栅化与桥协议 ── SheepRenderer 软件光栅化、PngEncoder 零依赖 PNG、SheepRender JSON 契约
第 5 阶段:ArkUI 宿主     ── 三 Tab 页面、参数面板、加载动画、错误边界
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
composableSheep/                        上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
composableSheep/src/commonMain/         模型与几何(去 Compose 化后原样编译)
composableSheep/src/composeMain/        上游 Compose 绘制(git mv 平移,不参与 ohos 编译,留档)
example/nativeApp/                      Kotlin/Native 动态库(Bridge + SheepRenderer + PngEncoder)
example/harmonyApp/                     DevEco Stage、CMake N-API 和 ArkTS 页面
demoapp/                                上游 Android demoapp,留盘不进 settings
README.OpenHarmony.md / _CN.md          鸿蒙适配说明(中英双份)
README.upstream.md                      上游原文保存
doc/                                    本文章与效果图

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :composableSheep 与 :example:nativeApp,rootProject.name = "ohos_composable_sheep_lib"。上游的 dokka / vanniktech 发布插件、AGP classpath、com.android.kotlin.multiplatform.library、iOS framework 声明全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(Wrapper 钉死;2.2.21 线必须从上游 9.4.1 降级)
依赖库与桥均零第三方依赖
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.composablesheep
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
文件上游依赖形态处置
model/FluffStyle.kt model/Leg.kt util/ListExtensions.kt纯 Kotlin原样保留 commonMain
model/Sheep.ktcompose.ui.graphics.Color 字段改写:Color→Long(0xAARRGGBB)
util/SheepColor.ktColor 常量改写:同上,原值不变;新增 random(Random) 重载
util/GeometryMaths.ktcompose.ui.geometry.Offset改写:自有 Pt(x, y),函数名 / 公式 / 注释不动
ComposableSheep.kt LoadingSheep.kt parts/*(5 文件)util/BasicGuidelines.kt util/Dimens.kt@Composable / DrawScope / Brush / Path / 动画git mv 到 src/composeMain,不编译,留档待 CMP 鸿蒙线对齐后接回

全库 15 个 Kotlin 文件 100% 在 commonMain、零 expect/actual、零平台源码集——比 KStore 还干净,唯一的"平台性"就是 Compose 类型本身。这决定了适配的主战场在类型替换的边界划在哪里:模型拿走(改载体),绘制留下(光栅化重放)。

1.4 页面边界
标题区          ComposableSheep · 点击随机换绒毛色 / 渲染耗时行(PNG 512×512 · xxx ms)
Tab 切换        羊 / 绒毛 / 加载
羊 Tab          300×300 展示区(点击随机换色)+ FluffStyle 四档 + 分块数滑杆
                + headAngle 滑杆 + glassesTranslation 滑杆 + 腿预设 + 八色色板
                + showGuidelines 开关 + 换一只(新种子)按钮
绒毛 Tab        绒毛云展示 + FluffStyle 四档 + 色板 + 辅助线开关 + 换一朵按钮
加载 Tab        300×300 旋转脉动羊(轴心 50%/10%)+ spinning 开关 + 时长滑杆 + 色板

页面启动时三个 Tab 各自完成首次渲染;参数变更即时重渲并刷新耗时行。

第 2 阶段:目标与依赖打通

2.1 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

库零第三方依赖,工具链不需要跟着依赖矩阵走,直接沿用社区已批准的钉法(与 qrose / haze 同款):HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21。上游 wrapper 是 9.4.1,2.2.21 线配 9.x 在配置期直接失败,必须整包替换 wrapper(jar / 脚本 / properties 一并钉死)。KStore 那条"2.4.20 线可留 Gradle 9.x"的经验在这里不适用——两条工具链线的经验不能互相套用。

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_composable_sheep_lib"

include(":composableSheep")
include(":example:nativeApp")

gradle/libs.versions.toml 从 25 行砍到 5 行,只剩 Kotlin 一个插件别名;gradle.properties 钉 Temurin 21 与 version=1.2.0-ohos.1。

2.2 编译真实模型

composableSheep/build.gradle.kts 收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}
    }
}

实测:

./gradlew :composableSheep:compileKotlinJvm            # BUILD SUCCESSFUL(commonMain 无 Compose 依赖的证据)
./gradlew :composableSheep:compileKotlinOhosArm64      # BUILD SUCCESSFUL
./gradlew :composableSheep:compileKotlinOhosX64        # BUILD SUCCESSFUL
2.3 测试任务的处置

根构建沿用社区纪律,全局禁用 ohos 测试任务(上游 commonTest 只有一个空壳,不构成验证面,真正的交叉验证在第 4 阶段的渲染冒烟):

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

第 3 阶段:模型与绘制分离(本适配的成败手)

3.1 两个机械替换

Color→Long:上游 Sheep / SheepColor 里的 androidx.compose.ui.graphics.Color 换成 Long(0xAARRGGBB),色值逐字照抄(0xFFC51162 依旧是 0xFFC51162)。Long 可以直接进光栅化器的 source-over 混合器,不需要任何转换层。

Offset→Pt:GeometryMaths.kt 里的 Offset 换成自有的 data class Pt(x: Float, y: Float),getCircumferencePointForAngle / getCurveControlPoint / getMiddlePoint / distanceToOffset→distanceToPt / getSlopeTo 的函数名、公式与"疯狂数学"注释全部不动——这些函数正是绒毛形状的灵魂,一个字符都不能漂。

唯一新增的公共 API 是 SheepColor.random(random: Random) 两个重载:桥侧需要种子可复现的随机颜色(上游 random() 用全局 RNG 不可注入),这是全 fork 唯一的 API 扩张。

3.2 Compose 文件整体平移

8 个 Compose 依赖文件(ComposableSheep.kt、LoadingSheep.kt、parts/ 全部 5 个、util/BasicGuidelines.kt、util/Dimens.kt)用 git mv 平移到 src/composeMain/kotlin/...——build 脚本不声明该源集也不应用 Compose 插件,这些文件不参与任何 target 编译,纯留档。git 把它们识别为 100% rename,历史可追溯。上游 demoapp(Android 工程)同样留盘不进 settings。

这个处置与 qrose 的 composeMain 完全同构:等哪天 CMP 鸿蒙线升到与上游 Compose 1.11 同代,把文件移回 commonMain、恢复上游 build 脚本即可,鸿蒙桥(nativeApp / harmonyApp)一行不用动。

第 4 阶段:光栅化与桥协议(技术难点)

4.1 绘制怎么"过河"
dev.nstv.composablesheep.library/**(上游模型 + 几何,原样编译)
        ↓ Sheep / FluffStyle / Leg / 圆周点 / 贝塞尔控制点
SheepRenderer.kt(软件光栅化,1:1 移植 parts/*.kt)
        ↓ ARGB 像素缓冲(2×2 超采样)
PngEncoder.kt(零依赖 PNG:RGBA + zlib stored 块)
        ↓ ByteArray
Bridge.kt(@CName("SheepRender"),扁平 JSON 进出)
        ↓ UTF-8 JSON(Base64 PNG)
N-API sheepRender(request)
        ↓
ArkUI Index.ets(Base64 解码 → PixelMap → Image)

绘制库与存储库的桥有本质差异:存储桥传"操作结果"(小 JSON),绘制桥传"像素产物"(整张图)。所以桥的载荷是 Base64 PNG,渲染全部发生在 Native 侧一次完成,ArkTS 拿现成位图显示——不逐帧跨 N-API(动画交给 ArkTS 属性动画)。

4.2 SheepRenderer:把 DrawScope 翻译成像素判定

光栅化器按上游 parts/ 的绘制顺序重放,常量逐字照抄:

上游绘制光栅化实现
腿 drawRoundRect + rotate(pivot=圆周点)旋转圆角矩形:像素先绕 pivot 逆旋转 −θ,再判定未旋转矩形(含 CornerRadius(20f) 圆角);OverlapPercentage=0.5、宽高比例不变
绒毛 drawPath(quadraticTo)累计百分比 → getCircumferencePointForAngle 圆周点 → getCurveControlPoint 控制点 → 每段二次贝塞尔展平 16 折点 → 偶奇扫描线填充
头 drawArc(360°) + 旋转旋转椭圆(rx=r, ry=2r/3),中心 center−(r+r/3, h/4)
眼睛 drawCircle + 瞳孔矩形圆心正向旋转(圆的旋转不变性),瞳孔矩形按原始坐标 + headAngle+10° 旋转
墨镜 drawArc(0°,180°) ×2 + 桥线下半圆饼(y≥圆心判定)+ 6px 桥线(点到线段距离),先 translate(glassesTranslation) 再旋转,pivot 同步下移
showGuidelines蓝底圆 / 青辅助圆 / 红点 / 红弦线 / 黄中点 / 20 格虚线网格 / 过中心轴,虚线按 10/10 px 分段

两条工程纪律:其一,旋转统一为"像素逆变换"——像素绕 pivot 转 −θ 后判定是否落在未旋转形状里,与 Compose rotate(degrees, pivot) 数学等价,圆/椭圆/半圆/圆角矩形/线段全部一个套路(只有圆走圆心正向旋转的捷径);其二,2×2 超采样 + 预乘盒式滤波下采样逼近 Skia 抗锯齿边缘,512×512 真机 debug so 实测约 300ms。

4.3 PngEncoder:为什么敢用未压缩块

零依赖 PNG 编码器输出 8-bit RGBA,zlib 走 stored(未压缩)块 + adler32 + crc32——512×512 约 1MB,免掉整个 deflate 实现。HarmonyOS image.createImageSource 对标准 PNG 照单全收,体积对 Demo 场景完全可接受。

4.4 桥协议与错误通道
请求 : {"action":"sheep","size":512,"seed":20261020,"fluffStyle":"uniform","chunks":10,
        "headAngle":5,"glassesTranslation":0,"legs":"twoStraight","fluffColor":"#C51162",
        "showGuidelines":false,"background":"#FFFFFF"}
响应 : {"ok":true,"action":"sheep","width":512,"height":512,"bytes":1049236,"pngBase64":"..."}
       {"ok":false,"error":"..."}

请求 20 个字段全部可选、带默认值,默认值与上游 Sheep 模型逐字段一致(缺 fluffColor 时按种子随机取调色板色)。一个不对称设计:seed 非零时 Random 分块先在桥内算好百分比再走 FluffStyle.Manual——上游 FluffStyle.Random 消费全局 RNG 不可注入,预计算循环与库内 buildFluffPercentages 完全同构,既保住可复现又不改库。序列化用手写 MiniJson(扁平 string map,约 50 行),不为 20 个标量字段引入 kotlinx.serialization 整条依赖链。

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI;非法 JSON、坏颜色、越界参数全部走同一个 ok:false 通道,ArkTS 侧统一渲染错误条。

第 5 阶段:原生桥接

5.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libsheep_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohossheep.so)
  │ SheepRender / libohossheep_symbols
  ▼
UTF-8 JSON + DisposeString
5.2 Kotlin/Native 导出函数
@CName("SheepRender")
fun sheepRender(request: String): String   // sheep / fluff 一口进,JSON 进 JSON 出
C ABI 符号ArkTS 方法覆盖能力
SheepRendersheepRender(req)整羊 / 绒毛 + 全参数(样式 / 角度 / 位移 / 腿 / 颜色 / 辅助线 / 底色)
libohossheep_symbols(内部)DisposeString 归还 KN 返回字符串的运行时内存
5.3 C++ N-API 与 CMake
napi_property_descriptor desc[] = {
    {"sheepRender", nullptr, NapiSheepRender, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 SheepRender 并包装返回。KN 返回的 const char* 必须经 libohossheep_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohossheep_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号,必须带 extern "C" 守卫——见 8.1 踩坑第 1 条),不拖整份生成头。

add_library(ohossheep SHARED IMPORTED)
set_target_properties(ohossheep PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(sheep_napi SHARED napi_init.cpp)
target_link_libraries(sheep_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohossheep)

OHOS_ARCH 为空或 libohossheep.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。Gradle 侧 copyKnSoArm64 / copyKnSoX64 在 link 任务后自动把 libohossheep.so + 匹配 triple 的 libc++_shared.so 回填 entry/libs/<abi>/。

export const sheepRender: (request: string) => string;

第 6 阶段:ArkUI 宿主

6.1 渲染管线与页面交互
private renderToPixelMap(request: Record<string, Object>): Promise<image.PixelMap> {
  const raw: string = sheepRender(JSON.stringify(request));
  const parsed: RenderResult = JSON.parse(raw) as RenderResult;
  if (!parsed.ok || parsed.pngBase64 === undefined) {
    throw new Error(parsed.error ?? 'render failed');
  }
  const bytes: Uint8Array = new util.Base64Helper().decodeSync(parsed.pngBase64);
  const sliced: ArrayBuffer = bytes.buffer.slice(bytes.byteOffset,
    bytes.byteOffset + bytes.byteLength);
  const src: image.ImageSource = image.createImageSource(sliced);
  return src.createPixelMap();      // 成功后 @State 刷新 Image
}

三个 Tab 各持一份 PixelMap 状态。羊 Tab 点击展示区随机换色(排除当前色,复刻上游 demoapp);滑杆在 SliderChangeMode.End / Click 才触发重渲,拖动过程不轰炸 Native;渲染耗时行直接回显 Date.now() 差值。错误(ok:false)渲染为页面错误条,不崩溃。

6.2 LoadingSheep 的动画等价

上游是 graphicsLayer { rotationZ = rotation; scaleX/Y = animatedScale },TransformOrigin(0.5, 0.1)。ArkTS 侧等价实现:

this.getUIContext().animateTo({
  duration: this.durationMillis,           // 默认 1000ms,FastOutSlowIn
  curve: Curve.FastOutSlowIn,
  onFinish: () => setTimeout(() => this.spinOnce(token), this.delayMillis)  // 300ms 每周期停顿
}, () => { this.angle = 360; });
// Image 上:.rotate({ angle, centerX: '50%', centerY: '10%' })
//           .scale({ x, y, centerX: '50%', centerY: '10%' })  // 1↔0.8 Linear 往复

羊以 50% 宽、1:1 宽高比锚在 300×300 容器底部居中,与上游 fillMaxSize(.5f).aspectRatio(1f).align(BottomCenter) 一致;spinning 开关通过递增 spinToken 令牌让 onFinish 链自然失效。

6.3 构建、签名和安装
./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.composablesheep -m entry

四、完整代码对照

4.1 整体架构

dev.nstv.composablesheep.library.model / util(上游模型 + 几何,原样编译)
    │ Sheep / FluffStyle / Leg / SheepColor / Pt 几何
SheepRenderer.kt(光栅化,parts/*.kt 的 1:1 移植)
    │ ARGB 像素 + 2×2 超采样
PngEncoder.kt(零依赖 PNG)
    │ RGBA ByteArray
Bridge.kt + MiniJson
    │ @CName("SheepRender")
libsheep_napi.so
    │ sheepRender
Index.ets
    │ Base64 → PixelMap → @State
    ▼
ArkUI 真机页面(羊 / 绒毛 / 加载 三 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_composable_sheep_lib、demoapp 移出
gradle/libs.versions.toml只剩 HarmonyOS Kotlin 2.2.21-1.0.0 一个插件别名
build.gradle.kts(根)版本 1.2.0-ohos.1、全局禁用 Ohos*Test* 任务
composableSheep/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,零依赖
composableSheep/src/commonMain/...模型与几何(3 个文件去 Compose 化,3 个原样)
composableSheep/src/composeMain/...上游 Compose 绘制 8 文件(git mv 平移,不编译)
example/nativeApp/build.gradle.kts双 ABI sharedLib(baseName=ohossheep)、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt@CName + MiniJson + 种子化 Random→Manual + 错误通道
nativeApp/.../SheepRenderer.kt软件光栅化(腿/绒毛/头/眼/镜/辅助线 + 逆旋转 + 扫描线填充)
nativeApp/.../PngEncoder.kt零依赖 PNG(stored zlib + adler32 + crc32)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../cpp/include/libohossheep_api.h手写 C 符号声明(extern “C” 守卫)
harmonyApp/.../ets/pages/Index.etsArkUI 三 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(24 节)

4.3 关键 API 对照

层次API作用
模型Sheep(fluffStyle, headAngle, glassesTranslation, legs, 五路颜色)羊的全部形态参数(鸿蒙侧颜色为 Long ARGB)
模型FluffStyle.Uniform / UniformIntervals / Random / Manual绒毛分块策略(lazy 百分比列表)
模型twoLegsStraight() / fourLegs(inner, outer)腿部预设工厂
模型SheepColor.list / random(exclude)八色调色板与随机取色
几何getCircumferencePointForAngle / getCurveControlPoint圆周点 / 二次贝塞尔控制点(绒毛形状的灵魂)
绘制SheepRenderer.renderSheep / renderFluff光栅化重放(drawComposableSheep / ComposableFluff 等价)
NativeSheepRenderC ABI JSON 导出
N-APIsheepRender(request)向 ArkTS 暴露能力
ArkTSrenderToPixelMap(req)触发渲染并刷新 Image

4.4 ArkTS 与 Kotlin 的边界

const raw: string = sheepRender(JSON.stringify({
  action: 'sheep', size: 512, seed: 20261020,
  fluffStyle: 'uniform', chunks: 10,
  fluffColor: '#C51162', background: '#FFFFFF'
}));
const parsed = JSON.parse(raw);      // { ok, width, height, bytes, pngBase64 }
val sheep = Sheep(
    fluffStyle = parseFluffStyle(json, rng),      // 上游模型
    headAngle = json.dbl("headAngle", 5.0).toFloat(),
    glassesTranslation = json.dbl("glassesTranslation", 0.0).toFloat(),
    legs = if (four) fourLegs(inner, outer) else twoLegsStraight(),
    fluffColor = parseOptionalColor(...) ?: SheepColor.random(rng)
)
val image = SheepRenderer.renderSheep(size, sheep, showGuidelines, background)

两者之间只传输扁平 JSON 字符串与 Base64 PNG,不传输 Kotlin 对象、Canvas 指令或未校验的动态结构。

五、关键决策说明

决策 1:模型与绘制分离,而不是整库编译或整库重写

与 KStore"整库真编译"相反:本库公开 API 全在 Compose 上,ohos 侧既编不了 Compose、也不该把羊"重写"成 ArkUI 控件(那会丢失"模型来自上游源码"这唯一可信凭据)。切分线画在类型依赖上:模型与几何只碰 Color / Offset 两个载体类型,机械替换后原样编译;绘制指令 100% 依赖 DrawScope,整体隔离到 composeMain。这条线让"羊长什么样"仍由上游代码决定。

决策 2:光栅化重放而不是逐指令回调

绘制桥有两种做法:把 drawPath / drawArc 逐条跨 N-API 回调给 ArkTS Canvas,或在 Native 侧一次性光栅化成 PNG。前者每帧几百次跨语言调用,动画场景必然掉帧;后者 Native 一次算完,ArkTS 只显示位图,动画(旋转/缩放)交给 ArkTS 属性动画零成本承担。选后者,与 qrose / Vico 同路。

决策 3:工具链沿用 2.2.21 旧线,Gradle 降 8.14.1

库零第三方依赖,不存在 KStore 式"stdlib 元数据反推钉版"问题,直接采用社区已批准的 qrose 同款钉法,审查口径零争议。代价是 wrapper 整包替换(9.4.1→8.14.1),换来的是与已审核仓库完全一致的工具链矩阵。

决策 4:seed 预计算走 Manual,不改上游 Random

FluffStyle.Random 的百分比是 by lazy 消费全局 RNG,种子注不进去。桥侧在请求带 seed 时先按同构循环算好百分比列表再构造 FluffStyle.Manual——上游类零改动,随机分块又可复现(同种子同形状),Demo 的"换一只"按钮才有确定性行为可验收。

决策 5:渲染冒烟做交叉验证,而非单元测试

上游 commonTest 是空壳,没有可复用的测试资产。可信度改由渲染冒烟承担:同一份 SheepRenderer + PngEncoder 源文件在临时 JVM 工程编译运行,产出 7 张 512×512 PNG(默认羊 / 辅助线 / 四腿 / Intervals 配色 / Random / 绒毛 / 透明底)逐张肉眼比对上游 README 效果,透明底再做 alpha 计数(65% 全透明 + 877 个半透明边缘)。之后同一份代码编进 .so——ohos 与 JVM 行为一致。

六、测试与验证

6.1 测试环境

本次验证使用 Windows 11(x64)、JDK 21(Temurin)、Gradle 8.14.1、DevEco Studio 26.0、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew -p build/smoke run     # 临时 JVM 工程(gitignored),同一份渲染源文件

7 张冒烟图全部通过:

渲染图钉住什么
sheep_default.png默认羊:洋红绒毛 / 深灰头 / 黑墨镜 / 双腿,与上游 demoapp 一致
sheep_guidelines.png辅助线:蓝底圆 / 青辅助圆 / 红点弦线 / 虚线网格 / 过中心轴
sheep_four_legs.pngfourLegs() 四腿(外八)+ headAngle −12° + 墨镜位移 14px
sheep_intervals.pngUniformIntervals(5,15) + 五路自定义配色
sheep_random.png种子 42 的随机分块
fluff_only.pngComposableFluff 半径 0.4×size
sheep_transparent.png透明底 alpha:171846 全透明 / 89421 不透明 / 877 半透明边缘

6.3 HAP 验证

:composableSheep:compileKotlinJvm: BUILD SUCCESSFUL
:composableSheep:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: SheepRender / libohossheep_symbols 均为全局 T 符号(双 ABI)
产物:arm64-v8a/libohossheep.so ≈3.97MB、x86_64 ≈3.67MB(debug,含 KN 运行时)
BuildNativeWithNinja: finished(双 ABI 编出 libsheep_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully

6.4 功能用例

  1. 启动后「羊」Tab 完成首次渲染,显示默认洋红羊与耗时行(真机实测 512×512 · 298ms,debug so);
  2. 点击羊,绒毛颜色随机变化且不与当前色重复(上游 demoapp 行为);
  3. FluffStyle 切换 Intervals / Random / Manual,绒毛分块节奏随之变化;
  4. Uniform 分块数滑杆 3→24,绒毛块数实时变化;
  5. headAngle −30…30°,头部(含眼睛墨镜)绕头中心旋转;
  6. glassesTranslation −40…40px,墨镜竖直位移;
  7. 腿预设切 fourLegs,出现四条腿(外腿外八旋转);
  8. showGuidelines 打开,叠加网格 / 底圆 / 辅助圆 / 红点弦线;
  9. 「绒毛」Tab 只渲染绒毛云,样式与色板可用;
  10. 「加载」Tab 蓝色羊持续旋转 + 0.8↔1.0 脉动,轴心在羊上方 10%;spinning 关闭立即停;时长滑杆改转速;换色即时重渲;
  11. 全部参数组合下 ok:true,非法输入(空 JSON / 坏颜色)显示错误条不崩溃。

6.5 验证结论

模型层、几何层、光栅化器、PNG 编码、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的绘制会话:整羊 / 绒毛 / 加载三入口、全参数面板与点击换色闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/composable-sheep-lib.git
cd composable-sheep-lib
export JAVA_HOME="/path/to/jdk-21"
./gradlew :composableSheep:compileKotlinJvm                                      # 模型无 Compose 依赖的证据
./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.composablesheep -m entry
hdc shell hilog | grep SheepNapi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 手写 libohossheep_api.h 漏了 extern "C" 守卫:napi_init.cpp 是 C++,没有守卫时编译器按 C++ 规则修饰名字,链接期报 undefined symbol: SheepRender(char const*),而链接器其实已在 so 里找到 C 符号(did you mean: extern "C" SheepRender)——手写 KN 符号头必须带 #ifdef __cplusplus extern "C" 包裹,对照已审核仓库原版逐行核过再提交;
  2. ArkTS 严格模式四连:@State tabIndex 与 CustomComponent 内置属性同名直接拒绝(改名 currentTab);两个 Tab 重复声明 fluffColorIdx 报 Duplicate identifier(拆开命名);LengthMetrics.vp() 在编译上下文里"只是类型不能当值"(Flex space 改子元素 margin);@Builder 方法必须 this. 前缀调用;
  3. Column.alignItems 收 HorizontalAlign,手滑写 VerticalAlign 编译期类型错误——Column 主轴竖直、交叉轴水平,与 Row 相反;
  4. animateTo 全局函数在新 SDK 标记 deprecated(功能正常),换 this.getUIContext().animateTo(...) 消警;
  5. Circle.fill/stroke 属性要求 SDK 26 而工程 compatible 是 5.1.0(18),编译告警——色板圆点改用 Column + borderRadius + backgroundColor 实现后告警清零;
  6. Gradle 9.4.1 上游 wrapper 配 2.2.21 HarmonyOS Kotlin 在配置期直接失败,wrapper 必须 jar / 脚本 / properties 整包替换成 8.14.1,只改 properties 不够;
  7. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  8. 旋转数学的正逆方向最容易混:先算"旋转后的形状中心再判定"对非圆图形是错的,统一改成"像素逆变换回形状空间再判定"后一类通用(圆例外,圆心正向旋转即可);
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  11. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内保持空数组 + "signingConfig": "default",证书绝不进入 AtomGit;
  12. 根 .gitignore 的 /build/ 盖不住嵌套目录——example/harmonyApp/ 的 oh_modules、.hvigor、entry/build、entry/libs、.cxx 与 *.so 要单独追加,否则构建产物混进提交。

8.2 已知问题

  • 上游 fluffBrush: Brush 重载(径向 / 线性渐变绒毛)桥内暂只支持纯色,渐变需按像素插值,列入后续;
  • LoadingSheep 每周期 delayMillis=300 用 onFinish + setTimeout 逼近,与 Compose infiniteRepeatable(tween(delayMillis)) 有毫秒级时序差异;
  • showGuidelines 中 dp 相关尺寸(8dp / 2dp 点径)按画布比例折算,与具体密度下上游像素值不逐像素相等;
  • debug so 渲染 512×512 约 300ms,release 明显更快;参数拖动只在松手时重渲,避免连续 Native 调用;
  • android / iOS / desktop 目标与发布配置剥离(源文件留在树里),本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.composablesheep 匹配的签名。

8.3 未来优化方向

  • 桥补 fluffGradient 字段,支持上游 Brush 渐变(线性 / 径向插值进光栅化器);
  • 光栅化 SIMD 化或 release-only 优化,把 512×512 渲染压进 100ms;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本(uitest + capture)。

九、总结

9.1 核心难点回顾

上游 composableSheep(15 文件 100% commonMain,公开 API 全在 Compose)
    → 切分线:模型与几何留 / 绘制走(Color→Long、Offset→Pt 两个机械替换)
    → Compose 绘制 git mv 到 composeMain(不编译,留档)
    → 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1)
    → ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
    → SheepRenderer 光栅化(parts/*.kt 1:1 移植 + 2×2 超采样)
    → PngEncoder 零依赖 PNG(stored zlib)
    → C ABI(@CName)+ JSON(Base64 PNG 载荷)
    → C++ N-API(DisposeString 纪律 + extern "C" 守卫)
    → ArkTS Base64 → PixelMap
    → ArkUI 真机三 Tab 验收(羊 / 绒毛 / 加载)

9.2 封装层次

  • 模型层:上游原版 Sheep / FluffStyle / Leg / SheepColor(载体 Long 化);
  • 几何层:上游原版圆周点 / 贝塞尔控制点 / 中点 / 斜率(载体 Pt 化);
  • 光栅化层:腿 / 绒毛 / 头 / 眼 / 墨镜 / 辅助线的像素重放 + 超采样抗锯齿;
  • 编码层:零依赖 RGBA PNG;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、参数面板、加载动画与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 绘制类 CMP 库的适配线画在类型依赖上:模型碰的载体类型(Color / Offset)机械替换,指令碰的类型(DrawScope / Brush)整体隔离——替换面最小化让"形态仍由上游决定"成立,隔离面留档让未来 CMP 鸿蒙线对齐后可整体接回;
  2. 绘制桥传像素不传指令:一次性光栅化 + Base64 PNG 比逐指令跨 N-API 回调稳一个数量级,动画交给 ArkTS 属性动画,Native 只算静态帧;
  3. 没有测试资产时,渲染冒烟就是交叉验证:同一份渲染源文件在 JVM 出 7 张比对图 + alpha 计数,再编进 so——"JVM 上长这样、真机上长一样"是绘制库版"内核没漂"的可信证明。

9.4 适配成果

当前适配已完成:

  • composable-sheep-lib 1.2.0 模型与几何的双 ABI Kotlin/Native 编译(上游公式零重写,唯一新增公共 API 是 SheepColor.random(Random) 重载);
  • SheepRenderer 光栅化器与 PngEncoder 零依赖 PNG(上游 parts/*.kt 全部常量与公式 1:1 移植);
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证双 ABI 全局 T 符号);
  • DevEco Stage ArkUI 三 Tab 验收页面(羊 / 绒毛 / 加载,全参数面板,点击换色,渲染耗时回显);
  • 7 张 JVM 渲染冒烟比对 + alpha 计数 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证(512×512 渲染 298ms);
  • 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。

参考文档

Logo

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

更多推荐