本文记录 compose-shadow 接入 OpenHarmony 的完整过程,覆盖绘制类 Compose 库的适配路线盘点、阴影合成语义的纯 Kotlin 内核提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

compose-shadow 是一个"单 Modifier"的 Kotlin Multiplatform 库:Modifier.boxShadow(blurRadius, color, spreadRadius, offset, shape, clip, inset) 一个调用复刻 Figma 的 drop shadow 与 inner (inset) shadow,CSS box-shadow 风格,上游支持 android / jvm / wasmJs / ios。本次适配与 KStore(依赖 ohos kotlinx 制品线、整库真编译)是两类不同的题面:compose-shadow 的全部实现都站在 Compose 的 DrawScope / Paint / saveLayer / ColorMatrix 之上,而鸿蒙侧已审核的工具链不在库内编译 Compose——因此适配的核心动作是把上游的阴影合成语义逐条提取为纯 Kotlin 软件光栅化内核,编译成 OpenHarmony 双 ABI 动态库,经 @CName C ABI + CMake NAPI 暴露给 ArkTS,与 qrose / compose-continuous-rounded-corner-shape / segmented-control 走的是同一条社区已审路线。

项目地址: AtomGit/oh-tpc/compose-shadow.git

上游仓库: LennartEgb/compose-shadow

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

compose-shadow 为 Compose Multiplatform 应用回答"元素阴影怎么画"这一件事:Figma 设计稿里的 drop shadow 与 inner shadow,导出时是一组 (y, blur, spread, color, inset) 参数,boxShadow 把这组参数变成一个修饰符——blur 高斯模糊、spread 四向收缩扩张、offset 平移、inset 反相成内阴影、clip 只裁内容。阴影是 UI 还原度里最显眼的细节之一,KMP 项目把它放在 commonMain,由 Android、桌面、Web、iOS 共享。OpenHarmony 应用同样需要这套绘制能力,但上游构建(Kotlin 2.2.20 / Gradle 8.11.1 / AGP 8.10.1 / Compose 1.9.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示滑杆,却无法证明阴影合成语义在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证语义内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.2.20 / Gradle 8.11.1 / AGP 8.10.1 与鸿蒙工具链无法共存于一个 Gradle 构建;ohos 目标只存在于 HarmonyOS Kotlin 定制发行版。
Compose 依赖上游全部 6 个源文件都 import androidx.compose.ui.*;社区已审核的约 60 个库适配一致不在库内编译 Compose。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
绘制语义跨不了桥DrawScope / Paint / BlurMaskFilter / saveLayer 是 Compose/Skiko 的运行时概念,Modifier 没有任何 @CName 面。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:把上游 Shadows.kt 的合成语义(spread 缩放、blur 的 sigma 约定、inset 的 ColorMatrix 反相、clip 只裁内容、层叠顺序)提取为纯 Kotlin 内核并编译成双 ABI 动态库,ArkTS 通过 NAPI 调用、按参数实时重渲染 PNG 验收。渲染数学完全由内核负责,ArkTS 只负责组请求、解码位图和展示。

1.2 库提供的能力

Demo(阴影画板)提供一个真实的阴影渲染会话,对应桥接的全部入口:

  • 版本握手:shadowVersion() 返回 {"ok":true,"version":"0.1.3-ohos.1"},来自真实 Native 调用而非静态文字;
  • drop shadow:blur(高斯模糊)、spread(含负值收缩)、offset(双向)、形状(方形 / 圆角 / 圆形,桥还支持每角独立圆角)、颜色与透明度;
  • inset 内阴影:反相合成语义,blur = spread = 0 时完全不可见(与上游一致);
  • clip:只裁剪内容填充,不影响阴影(上游语义最容易误解的一条);
  • 内容层叠:drop 时内容在阴影上、inset 时内阴影在内容上;
  • 预设:Figma 默认 / 内阴影 / 硬阴影一键还原上游 art/shadow.png 与 shadow-inset.png 的典型形态。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
width / height输出 PNG 的像素尺寸(内容盒 + 两侧 margin)。
sigma本次渲染实际采用的高斯 σ(px),验证 sigma = blur / 2 约定。
ms内核渲染 + PNG 编码耗时(TimeSource.Monotonic 计时)。
bytesPNG 字节数,与 base64 长度互为印证。
pngBase64RGBA PNG 的 Base64 文本,ArkTS 解码为 PixelMap 直接显示。

内核侧完整语义(spread 双向缩放、每角圆角钳位、精确可分离高斯卷积、inset 反相、clip、层叠、参数校验)全部在 lib/src/commonMain 里随编译进入 .so,21 项 JVM 测试钉住"语义没有漂移"。

1.3 实现适配

维度要求
代码复用上游 6 个 Compose 文件 git mv 到 lib/src/composeMain(未注册源码集,永不编译,一行不改);合成语义逐条移植为内核,ShadowRenderer.kt KDoc 带上游行号对照表。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证;lib 出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);samples/(上游 CMP 示例 App)留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,载荷为 PNG Base64;不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律内核零第三方依赖(不引 Skiko、不引序列化库);工具链钉 HarmonyOS Kotlin 2.2.21-1.0.0 社区多数派线。
UI 完整单页覆盖全部桥接入口:形状、八个参数滑杆、色板与透明度、inset/clip/内容三个开关、三个预设、状态行与版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。绘制类库的验收面不是"页面长得多像上游 Demo",而是"内核的合成语义在真机上逐像素成立"——阴影的模糊半径、扩张量、偏移、内阴影反相都要与上游在 JVM 上的行为对得上。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认"语义提取"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:内核与协议建模 ── 上游 Shadows.kt 语义逐条对照、PNG 载荷、JSON 契约、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 版本握手、参数面板、防抖重渲染、状态行
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
lib/src/commonMain/            新增:纯 Kotlin 阴影内核(ShadowShape / ShadowSpec / ShadowBitmap / ShadowRenderer)
lib/src/commonTest/            新增:21 项 JVM 交叉验证测试
lib/src/composeMain/           上游 Compose 源文件隔离区(未注册、永不编译,git mv 保留历史)
example/nativeApp/             Kotlin/Native 动态库(Bridge + PngEncoder)
example/harmonyApp/            DevEco Stage、CMake N-API 和 ArkTS 页面
samples/                       上游 CMP 示例 App,留盘不进 settings
README.md / README.upstream.md fork 主页 + 上游原文保存
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
doc/                           本文章与真机效果图

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :lib 与 :example:nativeApp,rootProject.name = "ohos_compose_shadow"。上游的 AGP / Compose 插件 / vanniktech 发布 / GitHub Packages 发布段 / binary-compatibility-validator / ktlint 全部随根构建替换剥离——本 fork 不发 Maven 仓库,samples/ 依赖 compose 插件,随之留盘。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,提供 ohosArm64() / ohosX64())
JDK21(org.gradle.java.home 钉 Temurin 21.0.12)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 8.11.1 升级;2.2.21 线要求)
依赖内核与桥零第三方依赖(不引 Skiko / kotlinx 库)
DevEco product5.1.0(18) 兼容,API 18
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.composeshadow
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
位置文件依赖形态处置
lib/src/commonMain(上游)Shadows.kt + BlurMaskFilter.kt (expect)androidx.compose.ui.* 全量git mv 隔离到 composeMain
lib/src/{android,jvm,native,wasmJs}Main(上游)BlurMaskFilter 四平台 actualandroid.graphics.BlurMaskFilter / org.jetbrains.skiagit mv 隔离到 composeMain
lib/src/commonMain(新增)内核 4 文件纯 Kotlin,零依赖进 ohos + jvm 构建
lib/src/commonTest(新增)2 个测试类kotlin("test")仅 JVM 运行

expect 声明全库只有 1 处:internal expect fun createBlurPaint(density, blurRadius, color): Paint。它返回的是 Compose Paint(带 BlurMaskFilter),本身就是"平台绘制管线"的一部分——内核不移植这个函数,而是移植它参与形成的合成语义:jvm/native actual 里 MaskFilter.makeBlur(NORMAL, blurRadiusPx / 2) 的 sigma 约定、画笔颜色与遮罩的乘法关系,这些才是跨桥时必须保真的部分。

1.4 页面边界
标题区          compose-shadow · OpenHarmony / 副标题(CSS box-shadow · Kotlin/Native ohosArm64/ohosX64)
版本握手行      aboutToAppear 调 shadowVersion(),显示 {"ok":true,"version":"0.1.3-ohos.1"}
Tab 切换        阴影画板 / 关于
预览区          8×8 棋盘格(透明度指示)+ Image(PixelMap),280×280
控制区          形状三选 / blur·spread·offsetX·offsetY·radius·width·height·margin 八滑杆
                8 预设色板 + alpha 滑条 / inset·clip·内容填充三开关 / 三预设按钮
状态行          最近一次渲染的 sigma、耗时、尺寸、PNG 字节数与错误信息
关于区          版本握手、五层架构、上游溯源、仓库与社区链接

页面启动时先做版本握手,再按当前参数发起首次渲染;参数变化 150ms 防抖后重渲染。

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

2.1 制品线验证(绘制类库的题面更简单)

KStore 适配要先验证 kotlinx 四件套的 ohos 制品;compose-shadow 内核零第三方依赖,需要验证的只有两件事:其一,eazytec Maven 上的 HarmonyOS Kotlin 2.2.21-1.0.0 发行版提供 ohosArm64() / ohosX64() target(这是唯一入口,Maven Central 官方 Kotlin 没有这两个 target);其二,本机 Konan 已缓存 ohos 交叉编译工具链(sysroot-hms / cpf-llvm)。验证通过后,commonMain 一行依赖坐标都不用写。

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

社区两条工具链线:多数派 2.2.21-1.0.0 + Gradle 8.14.1(qrose / collapsing-toolbar / ktoast 等约 40 个),新派 2.4.20-0.1.0-07 + Gradle 9.x(跟着 kotlinx 新制品走的库)。本内核零依赖,没有"依赖元数据反推版本"的约束,直接选多数派线;上游源码本来就是 Kotlin 2.2.20 编的,2.2.21 与之同代,源码级兼容成本为零。Gradle 从上游 8.11.1 升到 8.14.1(与 qrose wrapper 逐字节一致),gradle.properties 钉 JDK 21 与 kotlin.native.ignoreDisabledTargets=true。

settings.gradle.kts 把 eazytec 双仓写在最前(顺序错了配置期直接 Unresolved reference: ohosArm64):

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()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_compose_shadow"

include(":lib")
include(":example:nativeApp")
2.3 编译内核

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

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()                 // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

实测:

.\gradlew.bat :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64   # BUILD SUCCESSFUL

上游 6 个文件一个字没动(只是移动位置),内核 4 个文件是新增纯 Kotlin。

2.4 测试任务的处置

ohos target 没有 test runner,根构建全局禁用:

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

测试统一在 JVM 跑(:lib:jvmTest),ohos 侧只承担编译与链接。

第 3 阶段:内核与桥协议建模(技术核心)

3.1 语义对照:上游 Shadows.kt → 内核

这是整个适配的锚点,逐条对照(行号指上游文件):

上游行为位置内核实现
spreadSize = ±2·spread 上求 outlineShadows.kt:50-54,68阴影盒四边外扩(drop)/ 内缩(inset)spread;圆角数值不变,按新尺寸 min(w,h)/2 钳位(对齐 createOutline)
canvas.translate(offset - spread)Shadows.kt:66drop 等价于「外扩后平移 (offsetX, offsetY)」;inset 采用对称解读(见 8.2)
MaskFilter.makeBlur(NORMAL, blur/2)BlurMaskFilter.jvm/native.ktsigma = blurRadiusPx / 2,精确可分离高斯卷积,核半径 ceil(3σ)、边界 clamp
clip 原 outline + ColorMatrix(alphaScale=-1, shiftAlpha(255·colorAlpha)) + saveLayerShadows.kt:97-112内阴影 alpha = colorAlpha·(1−cov)·insideCov,RGB 取阴影色
shadowModifier.clip(shape) 只包内容链Shadows.kt:80只把内容填充乘原 shape coverage,阴影永不裁剪
if (inset) drawContent() 先画内容Shadows.kt:48,77drop:内容在阴影上;inset:内阴影在内容上

inset 公式的完整推导(第 5 节"决策 4"展开):模糊层进入 ColorMatrix 时每个像素的 alpha 是 255·colorAlpha·cov(画笔颜色 alpha × 模糊遮罩),矩阵 alphaScale = −1、shiftAlpha(255·colorAlpha) 把它映射为 255·colorAlpha − 255·colorAlpha·cov = 255·colorAlpha·(1−cov)。所以内阴影在形状内边缘最强(cov→0)、深入内部趋于 0,blur = spread = 0 时 cov 在内部处处为 1,内阴影完全不可见——与上游视觉行为一致。

3.2 JSON 契约
dev.lennartegb.shadows/**(新增内核:光栅化 + 高斯 + 反相合成)
        ↓ ShadowRenderer.render(spec, w, h, content, margin) → ShadowBitmap
Bridge.kt(@CName × 2,扁平 JSON 进、PNG Base64 出)
        ↓ UTF-8 JSON
N-API shadowRender(request) / shadowVersion()
        ↓
ArkUI Index.ets(画板 + 关于)

请求(全部字段可选、带默认值与钳位)与响应:

请求 : {"shape":"rounded","radius":16,"blur":16,"spread":0,"offsetX":0,"offsetY":0,
        "width":160,"height":120,"margin":48,"color":"#40000000",
        "content":"#FF673AB7","clip":1,"inset":0}
响应 : {"ok":true,"width":256,"height":216,"sigma":8,"ms":6,"bytes":183297,"pngBase64":"..."}
       {"ok":false,"error":"invalid color: #XYZ"}   ← 校验失败/非法参数统一走错误通道

参数钳位表:width/height 16…640、margin 8…256、blur 0…200、spread −160…200、offsetX/Y −320…320、radius 与每角圆角 0…320;出现任一 topLeft/topRight/bottomRight/bottomLeft 字段即启用每角独立圆角(radius 作未指定角缺省);color 支持 #RRGGBB 与 #AARRGGBB,默认 #40000000(Figma 默认 drop shadow:黑 25%)。

3.3 载荷为什么是 PNG 位图而不是矢量

同为绘制类库,compose-continuous-rounded-corner-shape 的桥返回 SVG path 字符串(矢量几何无损重绘);阴影不一样——blur 是位图效果,模糊后的每个像素 alpha 都携带信息,没有等价的矢量表达。选择 PNG(zlib stored 未压缩块 + adler32 + crc32 全手写编码,qrose 同款)+ Base64 文本:一次典型渲染(256×216)PNG 约 180 KB,Base64 后约 240 KB,150ms 防抖下真机交互流畅;画布尺寸被钳位在 640+2×256 以内,体积上限可控。

3.4 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("ShadowRender")
fun shadowRender(request: String): String = try {
    /* 解析 → 钳位 → ShadowSpec → render → PNG → Base64 */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、非法颜色、超范围参数(钳位兜底后基本不会触发)、内核校验失败(消息与上游逐字一致,如 blurRadius can't be negative.),全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libshadow_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosshadow.so)
  │ ShadowRender / ShadowVersion
  ▼
纯 Kotlin 内核 → PNG → Base64 → DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;Modifier 更是没有任何可导出面。所以与 qrose / KStore 相同:C ABI + JSON + 位图载荷。

4.2 Kotlin/Native 导出函数
@CName("ShadowRender")
fun shadowRender(request: String): String   // 参数解析 + 钳位 + 渲染 + PNG,一口进

@CName("ShadowVersion")
fun shadowVersion(): String = """{"ok":true,"version":"0.1.3-ohos.1"}"""
C ABI 符号ArkTS 方法覆盖内核
ShadowRendershadowRender(req)语义对照表全部行为 + PNG 编码
ShadowVersionshadowVersion()fork 版本握手(真机版本行的数据来源)

桥内 JSON 用手写 MiniJson(扁平 key-value,无数组无嵌套)——sharedLib 不引 kotlinx.serialization,避免把序列化运行时整包拖进 so;扁平协议也不需要。计时用 TimeSource.Monotonic(kotlin.system.getTimeMillis 在 2.2.21-ohos 发行版里是 error 级弃用)。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"shadowRender", nullptr, NapiShadowRender, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"shadowVersion", nullptr, NapiShadowVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打请求字节数、调 KN 函数并包装返回。KN 返回的 const char* 必须经 libohosshadow_symbols()->DisposeString(raw) 归还 Kotlin 运行时,绝不 free()。libohosshadow_api.h 按社区已审仓库同款手写:只声明 ExportedSymbols 结构体与两个业务符号,不拖整份生成头——前提是前两个成员必须是 DisposeStablePointer / DisposeString(已对照 KN 生成头逐行确认,截断结构体在这两个偏移上 ABI 安全)。

4.4 CMake 和类型声明
set(KN_SO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH})
if(NOT EXISTS ${KN_SO_DIR}/libohosshadow.so)
    message(FATAL_ERROR "Missing ${KN_SO_DIR}/libohosshadow.so. Run :example:nativeApp:linkDebugSharedOhosArm64 / OhosX64 first.")
endif()

add_library(ohosshadow SHARED IMPORTED)
add_library(shadow_napi SHARED napi_init.cpp)
target_link_libraries(shadow_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohosshadow)

OHOS_ARCH 为空或 KN so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。Gradle 侧 copyKnSoArm64/X64 任务在 linkDebugShared* 之后自动把 libohosshadow.so 与 ~/.konan/dependencies 里对应 triple(aarch64-linux-ohos / x86_64-linux-ohos)的 libc++_shared.so 拷进 entry/libs/{arm64-v8a,x86_64}。

export const shadowRender: (request: string) => string;
export const shadowVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 版本握手和触发渲染
aboutToAppear(): void {
  this.version = shadowVersion();   // 版本握手:来自真实 Native 调用
  this.scheduleRender();
}

private shadowColorHex(): string {  // alpha 滑条(0..100) 合成进 #AARRGGBB
  const alphaByte: number = Math.round(this.alpha * 2.55);
  return `#${alphaByte.toString(16).toUpperCase().padStart(2, '0')}${this.color.substring(1)}`;
}

private render(): void {
  const body: string = JSON.stringify({
    shape: this.shape, blur: this.blurPx, spread: this.spread,
    offsetX: this.offsetX, offsetY: this.offsetY, radius: this.radiusPx,
    width: this.boxWidth, height: this.boxHeight, margin: this.marginPx,
    color: this.shadowColorHex(),
    content: this.showContent ? '#FF673AB7' : '',
    inset: this.inset, clip: this.clipContent
  });
  const parsed: ShadowResponse = JSON.parse(shadowRender(body)) as ShadowResponse;
  if (!parsed.ok || parsed.pngBase64 === undefined) { /* 状态行显示 error */ return; }
  this.decodePng(parsed.pngBase64);      // Base64Helper → createImageSource → PixelMap → @State 刷新
}

参数变化统一走 150ms setTimeout 防抖,滑杆拖动不会打爆 Native 调用;content 传空串表示不画内容填充。

5.2 页面交互

阴影画板自上而下:8×8 棋盘格预览区(灰白相间指示透明度,中央叠 PixelMap)、形状三选、八个带 px 标签的滑杆(blur / spread / offsetX / offsetY / radius / width / height / margin)、8 预设色板 + alpha 滑条、inset / clip / 内容填充三个开关、三个预设按钮、状态行(sigma、耗时、尺寸、PNG 字节数)。关于 Tab 展示版本握手、五层架构(ArkTS → libshadow_napi.so → libohosshadow.so → 纯 Kotlin 内核 → PNG)、上游溯源与仓库 / 社区链接。

5.3 对比度和错误边界

ok:false 时错误原文进状态行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并 hilog 打点;PixelMap 解码失败有独立 catch 分支。预览区用棋盘格而不是纯色背景,透明度差异(阴影 alpha、clip 前后)一眼可辨。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosshadow)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/org/terminator/ohos/shadow/
│       ├── Bridge.kt                         @CName × 2 + MiniJson + 参数钳位(167 行)
│       └── PngEncoder.kt                     纯 Kotlin PNG(stored 块 + adler32 + crc32)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.composeshadow、versionName 0.1.3-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/libohosshadow_api.h
        ├── src/main/cpp/types/libshadow_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(547 行)
6.2 构建、签名和安装
.\gradlew.bat :lib:jvmTest :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64
.\gradlew.bat :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 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

hdc list targets -v
hdc -t <设备序列号> install -r \
  entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.composeshadow -m entry

四、完整代码对照

4.1 整体架构

lib/src/commonMain(新增纯 Kotlin 内核)
    │ ShadowSpec 校验(消息与上游逐字一致)
    │ ShadowRenderer:coverage 光栅化 → 高斯(σ=blur/2) → spread/offset → 合成
lib/src/composeMain(上游原文件,git mv 隔离,永不编译)
    │ Shadows.kt / BlurMaskFilter.kt + 4 actual
Bridge.kt + MiniJson + PngEncoder
    │ ShadowRender / ShadowVersion(@CName × 2)
libshadow_napi.so(CMake NAPI)
    │ shadowRender / shadowVersion
Index.ets
    │ JSON.stringify → JSON.parse → Base64 → PixelMap
    ▼
ArkUI 真机页面(画板 + 关于)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_compose_shadow、samples 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单插件钉版
build.gradle.kts(根)版本 0.1.3-ohos.1、全局禁用 Ohos*Test* 任务
lib/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,零依赖
lib/src/composeMain/**(移动)上游 6 个 Compose 文件原样隔离,git 历史 100% 保留
lib/src/commonMain/**(新增)ShadowShape / ShadowSpec / ShadowBitmap / ShadowRenderer(359 行,KDoc 带上游行号对照)
lib/src/commonTest/**(新增)21 项 JVM 交叉验证(216 + 109 行)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、MiniJson、参数钳位、错误通道、PNG Base64
nativeApp/.../PngEncoder.kt手写 PNG 编码(IHDR / stored IDAT / adler32 / crc32)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(画板 + 关于,547 行)
README.OpenHarmony.md / _CN.md鸿蒙适配说明(中英双份)

4.3 关键 API 对照

层次API作用
内核ShadowSpec(shape, blur, spread, offsetX/Y, colorArgb, inset, clip)参数模型 + 上游同款校验(消息逐字一致)
内核ShadowRenderer.render(spec, w, h, content, margin)光栅化 + 高斯 + 反相合成 + 层叠,输出 ARGB 位图
内核spec.sigmaPx()blurRadiusPx / 2,Skiko sigma 约定
NativeShadowRender / ShadowVersionC ABI JSON 导出
N-APIshadowRender(req) / shadowVersion()向 ArkTS 暴露能力
ArkTSrender() / applyPreset()组请求、防抖、预设一键还原

4.4 ArkTS 与 Kotlin 的边界

const parsed: ShadowResponse = JSON.parse(shadowRender(JSON.stringify({
  shape: 'rounded', blur: 16, spread: 0, offsetX: 0, offsetY: 0,
  radius: 16, width: 160, height: 120, margin: 48,
  color: '#40000000', content: '#FF673AB7', clip: 1, inset: 0
}))) as ShadowResponse;
// parsed.ok === true;Base64 解码 → PixelMap → Image 显示
val spec = ShadowSpec(shape, blur, spread, offsetX, offsetY, colorArgb, inset, clip)
val bitmap = ShadowRenderer.render(spec, width, height, contentArgb, margin)
val png = PngEncoder.encode(bitmap.argb, bitmap.width, bitmap.height)
// {"ok":true,"width":..,"height":..,"sigma":..,"ms":..,"bytes":..,"pngBase64":".."}

两者之间只传输扁平 JSON 字符串和 Base64 位图文本,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:提取"合成语义内核",而不是整库编译或重写 UI

上游 API 完全构建在 Modifier / DrawScope / Paint 上:没有纯 Kotlin 可编译的"引擎层",也没有任何 @CName 面。社区 CMP 鸿蒙线(1.9.2)与上游版本(1.9.1)虽然数字接近,但已审核的约 60 个库适配无一在库内编译 Compose——把 Compose 拖进 ohos 构建意味着引入一条没有审核先例的工具链路径。而阴影的合成语义(spread 缩放、sigma 约定、inset 反相、clip 边界、层叠)是确定性数学,可以在纯 Kotlin 里逐条复刻、在 JVM 上逐条测试。提取语义而不是搬源码集,是绘制类库适配的可复制模式。

决策 2:工具链选多数派线 2.2.21-1.0.0 + Gradle 8.14.1

内核零第三方依赖,不存在"依赖元数据反推 Kotlin 版本"的约束(KStore 选 2.4.20 线正是因为 coroutines 制品线逼的)。多数派线的好处是与其他已审仓库的 wrapper、gradle.properties、settings 模板逐字节一致,审核与维护成本最低;上游 Kotlin 2.2.20 与 2.2.21 同代,源码级兼容零成本。

决策 3:根构建替换(cmp_qrose 模式),samples 留盘

上游根构建(AGP / Compose 插件 / vanniktech 发布 / GitHub Packages / binary-compatibility-validator / ktlint)对鸿蒙没有保留价值,整体替换为极简 ohos 构建。samples/ 是完整 CMP App,依赖 compose 插件与 Android 配置,留盘不进 settings.gradle.kts——与 qrose 处理上游 androidapp/desktopApp 的方式一致,ohos Compose 制品线可用后可整体复活。

决策 4:inset 公式按 ColorMatrix 严格推导,spread 采用对称解读

字面上"反相"容易写成 clamp(colorAlpha − cov)——这个公式在 50% 透明色时内阴影完全消失,一眼看似合理、实则是错的。正确推导必须计入画笔颜色 alpha:模糊层像素 alpha 是 255·colorAlpha·cov,经 alphaScale = −1 + shiftAlpha(255·colorAlpha) 后为 255·colorAlpha·(1−cov)。开发中错误公式直接被两条 JVM 测试(半透明内阴影可见性、内边缘 alpha>0)拦下,避免了带病上线。另一处有意偏差:inset + spread≠0 时上游字面 translate(offset − spread) 会让内阴影向左上偏 2·spread,内核采用与 drop 对称的 Figma 读法(先内缩后平移 (offsetX, offsetY)),已在内核 KDoc 与适配说明中明确标注。

决策 5:21 项 JVM 交叉验证是"语义没漂"的唯一可信证明

与 KStore 不同,内核是新代码而非上游原件——"换线不换版"的论证不适用,必须用测试钉住每条语义:spread 扩张/收缩的包围盒、offset 质心位移、sigma 数值、inset 在 blur=spread=0 时全图 alpha=0、clip 只裁内容不裁阴影、层叠顺序、校验异常消息与上游逐字相同、圆形 coverage 旋转对称。测试先于桥接存在,桥接只是把被测试钉住的内核暴露出去。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12)、Gradle 8.14.1、HarmonyOS Kotlin 2.2.21-1.0.0、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

.\gradlew.bat :lib:jvmTest

21 项用例全部通过:

测试类用例数钉住什么
ShadowRendererTest12blur=0 硬边位置;sigma=blur/2 与 3σ 外 alpha=0;spread 正/负对包围盒的影响(±2px 容差);offset 质心位移(±1px);inset 盒外恒 0、零参数不可见、内边缘可见;clip 只裁内容(圆角外无内容色);drop/inset 层叠顺序;圆形旋转对称(4 对称点相等)
ShadowSpecTest9校验异常消息逐字一致(blurRadius can't be negative. 等)、校验顺序、NaN/Infinity 映射到 must be specified、sigmaPx() 数值

性能实测:JVM 上 720×720(blur=20、spread=6、圆角 24、内容合成)单帧 72ms——真机画板默认 256×216 规模下为毫秒级,防抖交互无压力。

6.3 HAP 验证

:lib:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
llvm-nm -D: ShadowRender / ShadowVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libshadow_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
产物arm64-v8ax86_64
libohosshadow.so(debugShared,未 strip)2,608,616 B(≈2.61 MB)2,231,736 B(≈2.23 MB)
libc++_shared.so1,850,184 B1,731,432 B
libshadow_napi.soCMake 产物,三件套同目录同左

6.4 功能用例

  1. 启动后版本握手行显示 {"ok":true,"version":"0.1.3-ohos.1"},来自真实 Native 调用(hilog 可见 ShadowNapi 打点);
  2. 默认参数(圆角 16 / blur 16 / 黑 25%)首帧即渲染,预览区棋盘格上出现带模糊外阴影的内容盒;
  3. 拖动 blur 滑杆 0↔60,阴影边缘从硬边连续过渡到弥散;状态行 sigma 数值同步变化(恒等于 blur/2);
  4. spread 拉到正值阴影扩大、负值收缩;offsetX/Y 改变投影方向(负 offsetY 即"向上"投影,与截图一致);
  5. 切换方形 / 圆角 / 圆形,阴影轮廓随之变化;圆角滑杆只影响 rounded 形态;
  6. 打开 inset:阴影转为内容盒内边缘的内阴影,blur=spread=0 时完全消失(与上游一致);offset 拖动可见内阴影偏向;
  7. 关闭 clip:内容填充溢出到圆角外(直角矩形填充);开启 clip:内容严格裁剪进形状——验证"clip 只裁内容不裁阴影";
  8. 色板换色 + alpha 滑条改变浓度;三预设一键还原 Figma 默认 / 内阴影 / 硬阴影;
  9. 每次渲染状态行显示 sigma / ms / 尺寸 / PNG 字节数,异常参数被钳位、非法颜色走 ok:false 通道显示 error 原文。

6.5 验证结论

阴影合成内核、Kotlin/Native 双 ABI 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,画板按参数实时渲染:版本握手、drop/inset 阴影、spread/offset/形状/颜色全参数覆盖、clip 语义、层叠顺序与上游在 JVM 上被 21 项测试钉住的行为一致(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-shadow.git
cd compose-shadow
export JAVA_HOME="/path/to/jdk-21"
./gradlew :lib:jvmTest                                               # 21 项交叉验证
./gradlew :lib:compileKotlinOhosArm64 :lib: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.composeshadow -m entry
hdc shell hilog | grep Shadow

八、遗留问题与改进方向

8.1 踩坑复盘

  1. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64(解析到了官方 Kotlin);
  2. ohos target 没有 test runner,Ohos*Test* 任务要在根构建全局禁用,否则 CI/无设备环境卡死;
  3. kotlin.system.getTimeMillis() 在 2.2.21-ohos 发行版是 error 级弃用,计时改用 TimeSource.Monotonic.markNow().elapsedNow();
  4. ArkTS 的 @Component struct 继承 CustomComponent,基类把通用属性方法(.blur() / .radius() / .margin() / .clip())合成为同名成员,@State blur / radius / margin / clip 直接撞名编译失败——改名 blurPx / radiusPx / marginPx / clipContent;而夹在报错行之间的 color / alpha / inset 未报错,说明冲突是按基类成员精确匹配的,不必无差别加前缀;
  5. 桥两侧字段协议失配只在端到端首跑时暴露:ArkTS 读 pngBase64 而桥发的是 png;alpha 作为独立字段发送、桥只认 #AARRGGBB——修复为响应补 bytes 字段、请求侧 shadowColorHex() 把 alpha 滑条合成进颜色。JSON 字段名应当作跨语言契约管理;
  6. inset 公式的字面减法 clamp(colorAlpha − cov) 是错的(漏了画笔 alpha 的乘法参与),半透明色的内阴影会整体消失——必须按 ColorMatrix 全链路推导(见决策 4),两条 JVM 测试把它拦在了集成之前;
  7. 手写 libohosshadow_api.h 的截断 ExportedSymbols 结构体只有在前两个成员恰为 DisposeStablePointer / DisposeString 时才 ABI 安全,写之前必须对照 KN 生成头逐行确认;
  8. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  9. KN 返回字符串必须经 DisposeString 归还运行时,N-API 层不能 free();
  10. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内 signingConfigs 保持空数组,证书绝不进入 AtomGit。

8.2 已知问题

  • inset + spread≠0 时内核采用对称解读(上游字面 translate 会向左上偏 2·spread),是与上游的一处有意偏差,已在内核 KDoc 标注;
  • coverage 光栅化是像素中心二值采样、无抗锯齿,边缘平滑完全依赖高斯模糊;上游 GPU 路径在绘制时抗锯齿;
  • 上游 Android actual 用 android.graphics.BlurMaskFilter(radius) 是另一套平台半径约定,上游本身跨平台就有此差异;内核跟 Skiko 多数派(sigma = blur/2);
  • PNG 为 zlib stored 未压缩块,大画布体积可观(受 640+2×256 钳位约束),Base64 再 ×4/3;
  • 桥是同步一次一调用,适合参数化取图,不适合逐帧动画流水线;
  • samples/(上游 CMP 示例 App)不进构建;本 fork 不发 Maven 仓库,上游发布链路随根构建剥离。

8.3 未来优化方向

  • 评估"显示列表/帧协议"(KLoading 桥先例)支持实时阴影动画,替代每次全量 PNG;
  • PNG 编码升级为 deflate 压缩块,或按内容自适应调色板,压掉 stored 块的体积冗余;
  • 增加 ArkUI 原生 .shadow() 属性的并排对照模式,直观呈现内核与系统渲染的差异;
  • 在 CI 中加入 ohos 编译、ELF 符号检查(llvm-nm 断言 ShadowRender/ShadowVersion 为全局 T)和 HAP 构建;
  • 扩展形状族(多边形 / 路径)与多重阴影(上游单 modifier 叠加语义)。

九、总结

9.1 核心难点回顾

上游 Shadows.kt(全部站在 Compose 上)
    → 判定"提取合成语义"路线(社区 ~60 库一致不编 Compose)
    → 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 + Gradle 8.14.1)
    → 上游 6 文件 git mv 隔离到 composeMain
    → 纯 Kotlin 内核(spread/offset/σ=inset 反相/clip/层叠,KDoc 行号对照)
    → 21 项 JVM 测试钉住语义(含 ColorMatrix 公式纠错)
    → ohosArm64/ohosX64 sharedLib(@CName × 2 + MiniJson + PNG)
    → C++ N-API(DisposeString 纪律,截断 api 头 ABI 校验)
    → ArkTS 画板(防抖重渲染 + 状态行)
    → ArkUI 真机验收(版本握手 / 全参数 / 预设 / clip 语义)

9.2 封装层次

  • 内核层:参数校验(上游同款消息)、coverage 光栅化、精确高斯、inset 反相合成、clip 与层叠;
  • Native 层:双 ABI 动态库,2 个 C ABI 输出 JSON + PNG Base64;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:参数面板、防抖调度、位图解码、状态行与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 绘制类库的适配单元是"合成语义"而不是"源码集":Modifier 没有任何可导出面,但它的行为是确定性数学——语义可推导、可测试,就可以跨桥;qrose 提编码器、continuous-corner-shape 提几何、本项目提阴影合成,是同一条路的三次落地;
  2. 零依赖内核的工具链选多数派线而不是最新线:没有依赖元数据反推约束时,2.2.21-1.0.0 + Gradle 8.14.1 与其他已审仓库的模板逐字节一致,审核与维护成本最低;Gradle 版本跟着工具链线走(2.2.21 线升 8.14.1),不沿用也不抢跑;
  3. 桥两侧的字段协议是跨语言契约,要端到端对拍:png vs pngBase64、独立 alpha vs 合成 #AARRGGBB 这类失配,单侧编译都发现不了,只有首跑才暴露——桥协议字段表应当在写第一行桥代码前定稿,并让两侧从同一张表生成/核对。

9.4 适配成果

当前适配已完成:

  • compose-shadow 0.1.3 阴影合成语义的纯 Kotlin 内核(4 文件 359 行核心 + 21 项测试),ohosArm64 / ohosX64 双 ABI Kotlin/Native 编译(libohosshadow.so ≈2.61 MB / ≈2.23 MB);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 验收页面(画板 + 关于,547 行):版本握手、全参数滑杆、三预设、inset / clip / 内容开关、状态行;
  • 21 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证(版本握手与参数化渲染见效果图);
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐