本文记录 compose-continuous-rounded-corner-shape 接入 OpenHarmony 的完整过程,覆盖几何库适配的边界盘点、androidx.graphics:graphics-shapes 无 ohos 制品时的数学内核移植、真实库逐浮点交叉验证、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

compose-continuous-rounded-corner-shape 是基于 Compose Multiplatform 的连续(平滑)圆角形状库:在官方 RoundedCornerShape 的参数体系上增加 smooth 参数(0…1,默认 0.6,即 Apple 设计系统的连续圆角),提供随布局方向镜像的 ContinuousRoundedCornerShape 与不镜像的 AbsoluteContinuousRoundedCornerShape。它的全部平滑圆角数学都委托给 androidx.graphics:graphics-shapes 的 RoundedPolygon.rectangle(...),再把结果三次贝塞尔转成 Compose Path。本次适配与社区前两批路线(qrose 的"Painter 隔离 + 自研光栅化"、KStore 的"kotlinx 制品线补齐后真编译整库")都不同:上游唯一的算法依赖 androidx.graphics:graphics-shapes 是 androidx 官方库,不存在也不会出现 ohos 制品线——因此本仓把该库的矩形平滑圆角路径数学逐表达式移植为纯 Kotlin commonMain 内核,并用真实 graphics-shapes 1.1.0 在 JVM 侧做 16 项逐浮点交叉验证,让同一条轮廓线在 ArkUI 上以 SVG path 的形态矢量化渲染出来,ArkUI 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/compose-continuous-rounded-corner-shape

上游仓库: adamglin0/compose-continuous-rounded-corner-shape

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

compose-continuous-rounded-corner-shape 为 Compose Multiplatform 应用提供"连续圆角矩形"这一件事的完整答案:ContinuousRoundedCornerShape(corner, smooth) 一行拿到 Apple 式平滑圆角,四角可以独立、可以按 Dp/px/百分比声明,smooth == 0 退化为普通 RoundedCornerShape,RTL 布局下 start/end 角自动镜像。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、JS/Wasm 共享。OpenHarmony 应用同样需要这套形状能力,但上游构建(Kotlin 2.2.21 / CMP 1.9.1 / AGP 8.12.3 / Gradle 9.0.0)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画出圆角,却无法证明上游赖以成名的平滑圆角数学在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证几何内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
算法依赖缺失平滑圆角数学全部在 androidx.graphics:graphics-shapes 里,该库是 androidx 官方制品,.module 元数据里只有 android/desktop/iOS/macOS/JS/Wasm/Linux/mingw 变体,没有也不可能有 ohos 线,“换线不换版”(KStore 路线)直接失效。
UI 依赖缺失上游 4 个源文件全部继承 Compose 的 CornerBasedShape、操作 Outline/Path,compose.ui/compose.foundation 同样没有 ohos 变体,Compose Shape API 无法参与 ohos 编译。
工具链不一致上游 Kotlin 2.2.21 / Gradle 9.0.0 / AGP 8.12.3(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
形状语义跨不了桥Compose Shape 是 Modifier 体系里的对象,跨不过 FFI;能在 ABI 上稳定传输的是纯几何数据。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:平滑圆角几何内核的移植与 ohos 编译、移植数学与真实库的一致性证明、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。圆角语义完全由移植内核负责,ArkTS 只负责调用 Native、解析 JSON 和渲染路径。

1.2 库提供的能力

Demo 提供一个真实的形状会话,对应桥暴露的全部入口:

  • 版本握手:shapeVersion() 返回 fork 版本号 1.0.6-ohos.1,来自真实 Native 调用而非静态文字;
  • 连续圆角渲染:smooth 0…1 连续可调,0 是普通圆角、0.6 是 Apple 默认、1 是最大平滑,ArkUI Path().commands() 矢量渲染,无级缩放;
  • 四角控制:四角联动 / 独立半径两种模式,Dp 级像素半径,半径超过边长一半时由内核逐边裁剪自动收敛(与 Compose 行为一致);
  • 尺寸控制:宽高独立滑杆,验证非正方形与宽高比极端时的几何;
  • RTL 镜像:开关联动 LayoutDirection.Rtl 语义,交换 start/end 角(Absolute 变体即关闭此开关);
  • 对照模式:一键叠加 smooth=0 的红色描边轮廓,平滑角相对普通圆角的外扩量肉眼可见;
  • 结构透明:info 行实时显示当前轮廓的三次贝塞尔段数(全直角 4 段、四角全圆角 16 段、混合形态居间),几何结构可观测。

响应包含:

字段作用
ok请求是否成功,失败时 error 带原文。
width / height回显本次渲染的形状尺寸。
smooth / rtl回显生效的平滑参数与镜像开关(越界值会被夹取后回显)。
cubicCount轮廓的三次贝塞尔段数,几何结构的直接证据。
path绝对坐标 SVG path 指令串(M … C … Z),ArkUI Path().commands() 直接可用。

引擎侧完整能力(RoundedCorner 三段曲线、逐边空间裁剪、特征扁平化、RTL 镜像、百分比圆角)全部随移植内核进入 .so,JVM 侧保留 16 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的几何与上游在 Android/iOS/Desktop 渲染的几何没有漂移。

1.3 实现适配

维度要求
代码复用graphics-shapes 1.1.0 的矩形平滑圆角路径(RoundedCorner、cutAdjusts、flattening)逐表达式移植,运算顺序与原库一致;上游 4 个 Compose 源文件零改动迁入 src/composeMain。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 sample/compose-app 留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律移植内核零第三方依赖;真实 graphics-shapes 只出现在 jvmTest(证据链),绝不进 ohos classpath。
UI 完整双 Tab 覆盖全部桥接入口:形状预览(尺寸/平滑/四角/RTL/对照/颜色)+ 关于(版本握手、架构分层、仓库与社区链接)。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。compose.ui 与 androidx.graphics:graphics-shapes 没有 ohos 制品,上游 Shape API 被完整保留在 src/composeMain(不注册源码集、不参与编译);适配面因此是"几何内核 + 桥 + ArkUI 渲染宿主",等这些制品出现后 Compose API 可原样接回。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、验证 graphics-shapes 制品可达性、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、wrapper 升 9.6.1
第 3 阶段:几何内核移植   ── graphics-shapes 矩形路径逐表达式移植、RTL/百分比语义对齐、SVG path 出口
第 4 阶段:桥协议与原生   ── @CName C ABI、C++ N-API、DisposeString、双 ABI 三件套
第 5 阶段:ArkUI 宿主     ── 形状游乐场(滑杆/开关/对照/颜色)、版本握手、错误通道
第 6 阶段:示例与验证     ── jvmTest 交叉验证、llvm-nm 符号检查、HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
super-rounded-corner-shape/            上游形状库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
super-rounded-corner-shape/src/commonMain/…/geoshape/
                                       新增:移植几何内核(GraphicsShapesPort.kt)+ 公共入口
super-rounded-corner-shape/src/composeMain/
                                       上游 4 个 Compose Shape 文件原样迁移(隔离,不注册不编译)
super-rounded-corner-shape/src/jvmTest/
                                       新增:16 项逐浮点交叉验证(vs 真实 graphics-shapes 1.1.0)
example/nativeApp/                     Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkTS 页面
sample/ docs/                          上游示例与文档,留盘不进构建
README.md / README.upstream.md         fork 中文首页 + 上游原文保存
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :super-rounded-corner-shape 与 :example:nativeApp,rootProject.name = "ohos_continuous_corner_shape"。上游的 AGP / Compose 编译器 / dokka / binary-compatibility-validator / vanniktech 发布插件链全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 Temurin 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.6.1(wrapper 从上游 9.0.0 升版;2.4.20 线支持 Gradle 9.x)
androidx(仅测试)androidx.graphics:graphics-shapes:1.1.0 + androidx.collection:collection:1.5.0(desktop 变体)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.continuousshape
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
文件依赖形态处置
ContinuousRoundedCornerShape.kt(233 行)CornerBasedShape / CornerSize / @FloatRange迁 composeMain 隔离,零改动
ContinuousRoundedCornerShapeImpl.kt(130 行)RoundedPolygon / Outline / Path / FloatFloatPair迁 composeMain 隔离;其 createOutline 的数学语义成为移植内核的行为规范
AbsoluteContinuousRoundedCornerShape.kt / …Impl.kt同上迁 composeMain 隔离
(外部)androidx.graphics.shapesRoundedPolygon.rectangle + CornerRounding(radius, smoothing) + Cubic移植:矩形路径子集逐表达式进 commonMain

值得单独说明:这个库的"引擎"不在自己源码里——上游全部 4 个文件加起来只是把 Compose 的角参数翻译给 graphics-shapes 再把 Cubic 列表转成 Path。真正的平滑圆角数学(RoundedCorner 的 flanking 曲线、圆形弧近似、逐边空间裁剪、特征扁平化)全在 graphics-shapes 的 commonMain 里。所以本仓适配的第一性问题是:graphics-shapes 的数学怎么到 ohos 上。

1.4 页面边界
Tab 切换        形状预览 / 关于
预览区          平滑圆角形状(Path 矢量渲染)+ 对照描边层 + info 行(N 段三次贝塞尔 · W×H)
尺寸与平滑      宽度 / 高度(80..340vp)/ smooth(0..1,步进 0.01)
圆角半径区      四角联动开关 + 联动滑杆,或四角独立滑杆(0..220vp)
外观区          5 色填充选择 / 仅描边开关 / 对照 smooth=0 开关 / RTL 镜像开关
操作区          重置为默认值
关于区          版本握手、库简介、调用链分层、验收要点、仓库与社区链接

页面启动时先调 shapeVersion() 做握手,再渲染默认形状(300×300、四角 50、smooth 0.6)。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)与 google maven 上有没有 graphics-shapes 的 ohos 制品。用 .module 元数据逐个验证:

androidx.graphics:graphics-shapes:1.1.0(dl.google.com)
  variants: androidApiElements / desktopApiElements / iosArm64 / iosSimulatorArm64 / iosX64 /
            js / wasmJs / linuxArm64 / linuxX64 / macosX64 / macosArm64 / mingwX64 / tvos / watchos / metadata
  → 没有 ohosArm64 / ohosX64

三个关键结论:其一,graphics-shapes 是 androidx 官方 KMP 库,变体矩阵由 Google 官方发布,不存在第三方 ohos 制品线,KStore 那条"换线不换版"的路在这里走不通;其二,desktopApiElements 变体存在,意味着 JVM 侧可以把真实库拉进测试 classpath 当"标准答案"——移植的正确性可以闭环验证,这是决定移植路线可行的前提;其三,移植面可以做到很小:上游只用到了 RoundedPolygon.rectangle 一条路径,星形/Morph/多边形等 graphics-shapes 其余能力与本库无关。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07 + Gradle 9.6.1)

社区两条已审核工具链线:qrose 用的 2.2.21-1.0.0(必须降 Gradle 8.14.1),KStore 用的 2.4.20-0.1.0-07(支持 Gradle 9.x)。本仓上游 wrapper 本来就是 9.x(9.0.0),升到 KStore 同款 9.6.1 即可免降级;移植内核是纯 Kotlin、零第三方依赖,不存在 kotlinx 制品线拖版本的问题。Kotlin 钉 2.4.20-0.1.0-07、Gradle 9.6.1、JDK 21,与 KStore 已审核矩阵完全一致。

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

dependencyResolutionManagement {
    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_continuous_corner_shape"

include(":super-rounded-corner-shape")
include(":example:nativeApp")
2.3 编译移植内核

super-rounded-corner-shape/build.gradle.kts 收敛为纯 KMP 模块:

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

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

    sourceSets {
        val jvmTest by getting {
            dependencies {
                implementation(kotlin("test"))
                implementation(libs.androidx.graphics.shapes)   // 真实库,只进测试
                implementation(libs.androidx.collection)
            }
        }
    }
}

实测:

./gradlew :super-rounded-corner-shape:compileKotlinOhosArm64 :super-rounded-corner-shape:compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew :super-rounded-corner-shape:jvmTest                                                                  # 16 项全绿

移植内核与公共入口全部在 commonMain,ohos 与 JVM 编译同一份代码。

2.4 测试依赖的处置:真实库只进 jvmTest

commonMain 零依赖意味着 ohos 编译永远不碰 androidx;真实 graphics-shapes 只挂在 jvmTest,承担"标准答案"职责。ohos 侧无测试运行器的问题沿用社区纪律:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:

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

第 3 阶段:几何内核移植(本次适配最重的一步)

3.1 移植了什么

从 graphics-shapes 1.1.0 的 sources.jar 里取出矩形平滑圆角路径的最小子集,落在 commonMain/…/geoshape/GraphicsShapesPort.kt(文件头注明 Apache-2.0 与来源):

原始符号移植为说明
Point(FloatFloatPair 别名)Pt运算符与求值顺序原样保留
Cubic(8-float 数组)ShapeCubicsplit/reverse/straightLine/circularArc/zeroLength 全保留
CornerRoundingRoundingSpecradius + smoothing
RoundedPolygon.Companion.rectangleroundedRectangleCubics() 第 1 步顶点序 [右下, 左下, 左上, 右上],默认中心 (0,0)
RoundedPolygon(vertices, …) 构造同上第 2-5 步RoundedCorner 三段曲线、cutAdjusts 逐边裁剪、Corner+Edge 特征表
transformed { +center }同上第 6 步逐点平移,顺序与上游 createOutline 一致
RoundedPolygon.cubics 扁平化同上第 7 步first/last 对半拆分、零长度曲线丢弃与锚点改写、闭合曲线
MutableFloatListFloatArray(2)摆脱 androidx.collection 依赖

不移植的只有 convex()(服务 Morph 动画)与 init 连续性校验(调试用)。

3.2 为什么必须逐表达式

移植的验收标准不是"看起来像 squircle",而是浮点一致。上游 createOutline 的调用序列是:

RoundedPolygon.rectangle(width, height, perVertexRounding = listOf(
    CornerRounding(adjustedBottomEnd, smooth), CornerRounding(adjustedBottomStart, smooth),
    CornerRounding(adjustedTopStart, smooth), CornerRounding(adjustedTopEnd, smooth)
)).transformed { ox, oy -> FloatFloatPair(ox + size.center.x, oy + size.center.y) }
    .cubics   // moveTo + cubicTo + close

两个容易踩的顺序细节:其一,transformed 平移发生在特征层、cubics 扁平化(含 first/last 对半拆分)发生在平移之后——拆分点 pointOnCurve(0.5) 对平移非线性,顺序颠倒会引入 1e-7 级偏差;其二,零长度曲线不是简单丢弃,上游会把前一条保留曲线的终点改写到最新锚点,漏掉这个分支,直角/混合角用例的段数就对不上。移植严格保持上游顺序,这也是 3.3 交叉验证敢用 1e-4 容差的底气。

3.3 公共入口与语义对齐

ContinuousCornerShapeGeometry 把内核包成语义与上游 createOutline 对齐的 API:

object ContinuousCornerShapeGeometry {
    fun cubics(
        width: Float, height: Float,
        topStart: Float, topEnd: Float, bottomEnd: Float, bottomStart: Float,
        smooth: Float, rtl: Boolean = false,
    ): List<ShapeCubic>          // 像素级圆角;rtl 镜像 start/end(上游 LayoutDirection.Rtl 分支)

    fun percentRadius(width: Float, height: Float, percent: Float): Float
                                 // 百分比圆角按 min(width, height) 解析(同 CornerSize(percent))

    fun svgPath(cubics: List<ShapeCubic>): String
                                 // "M … C … Z",ArkUI Path().commands() 直接可用
}

smooth == 0 上游走 Outline.Rounded 捷径,本内核统一走多边形路径:graphics-shapes 在 smoothing=0 时侧翼曲线退化为零长度、只留圆弧的三次贝塞尔近似,视觉等价,少一个分支且被交叉验证矩阵覆盖。

第 4 阶段:桥协议建模与原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libcontinuousshape_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoscontinuousshape.so)
  │ ContinuousShapeRender / ContinuousShapeVersion
  ▼
SVG path + UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
@CName("ContinuousShapeVersion")
fun continuousShapeVersion(): String = "1.0.6-ohos.1"

@CName("ContinuousShapeRender")
fun continuousShapeRender(request: String): String   // 一口进:解析 JSON → 内核 → 组装响应
C ABI 符号ArkTS 方法覆盖内核
ContinuousShapeRendershapeRender(req)角度解析(px/percent)+ RTL 镜像 + 平滑圆角内核 + SVG path 出口
ContinuousShapeVersionshapeVersion()fork 版本握手(真机版本行的数据来源)

请求与响应:

请求 : {"width":300,"height":300,"topStart":50,"topEnd":50,
        "bottomEnd":50,"bottomStart":50,"smooth":0.6,"rtl":false,"percent":false}
响应 : {"ok":true,"width":300,"height":300,"smooth":0.6,"rtl":false,
        "cubicCount":16,"path":"M150.35 2 C… Z"}
       {"ok":false,"error":"..."}

width/height 必须为正;smooth 越界自动夹取;半径负值截断为零、超过边长一半由内核逐边收敛。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"shapeRender", nullptr, NapiShapeRender, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"shapeVersion", nullptr, NapiShapeVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscontinuousshape_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscontinuousshape_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohoscontinuousshape SHARED IMPORTED)
set_target_properties(ohoscontinuousshape PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(continuousshape_napi SHARED napi_init.cpp)
target_link_libraries(continuousshape_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoscontinuousshape)

OHOS_ARCH 为空或 libohoscontinuousshape.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const shapeRender: (request: string) => string;
export const shapeVersion: () => string;

example/nativeApp 侧,双 ABI sharedLib 的 link 任务 finalizedBy 两个 Copy 任务,把 libohoscontinuousshape.so 与 konan 工具链里的 libc++_shared.so 一起回填到 harmonyApp/entry/libs/<abi>/。

第 5 阶段:ArkUI 宿主

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

private render(): void {
  const req: RenderRequest = {
    width: this.widthVp, height: this.heightVp,
    topStart: this.effectiveTopStart(), topEnd: this.effectiveTopEnd(),
    bottomEnd: this.effectiveBottomEnd(), bottomStart: this.effectiveBottomStart(),
    smooth: this.smooth, rtl: this.rtl
  };
  const res = JSON.parse(shapeRender(JSON.stringify(req))) as RenderResponse;
  this.apply(res);                  // pathStr 进 Path().commands(),info 行刷新
}

任何滑杆/开关的 onChange 都收敛到同一次 render(),Native 调用是纯计算(微秒级),UI 线程直调即可;对照层额外用 smooth: 0 再发一次请求拿红色描边的 path。

5.2 页面交互

形状预览 Tab 自上而下:预览卡片(形状 + info 行)、尺寸与平滑三滑杆、圆角半径区(联动开关切换"四角一致"或四条独立滑杆,上限 220vp 专门用来演示超限收敛)、外观区(5 色选择、仅描边、对照 smooth=0、RTL)、重置按钮。关于 Tab 展示版本握手行、库简介、ArkTS → N-API → C ABI → 移植内核 → SVG path 分层说明、验收要点与仓库/社区链接。

5.3 对比度和错误边界

主形状深色填充白底,对照描边红色 1.5vp;ok:false 时错误原文进 info 行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进 info 行,页面永不白屏。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoscontinuousshape)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/org/terminator/ohos/continuousshape/
│       └── Bridge.kt                         @CName × 2 + MiniJson(继承 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.continuousshape、versionName 1.0.6-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/libohoscontinuousshape_api.h
        ├── src/main/cpp/types/libcontinuousshape_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(形状游乐场 + 关于)
6.2 构建、签名和安装
./gradlew :super-rounded-corner-shape:jvmTest                                # 16 项交叉验证
./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 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 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.continuousshape -m entry

四、完整代码对照

4.1 整体架构

androidx.graphics:graphics-shapes 1.1.0(上游真实依赖,移植蓝本)
    │ RoundedPolygon.rectangle + CornerRounding + Cubic
geoshape/GraphicsShapesPort.kt(逐表达式移植,commonMain,零依赖)
    │ roundedRectangleCubics():RoundedCorner 三段曲线 + cutAdjusts + flattening
ContinuousCornerShapeGeometry.kt(公共入口)
    │ cubics()/percentRadius()/svgPath(),RTL 与上游 createOutline 对齐
Bridge.kt + MiniJson(example/nativeApp)
    │ ContinuousShapeRender / ContinuousShapeVersion
libcontinuousshape_napi.so
    │ shapeRender / shapeVersion
Index.ets
    │ JSON parse + Path().commands(path)
    ▼
ArkUI 真机页面(形状游乐场双 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_continuous_corner_shape、sample 移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + 测试用 androidx 两坐标
build.gradle.kts(根)版本 1.0.6-ohos.1、全局禁用 Ohos*Test* 任务
gradle/wrapper/gradle-wrapper.properties9.0.0 → 9.6.1
super-rounded-corner-shape/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,jvmTest 挂真实 graphics-shapes
…/src/composeMain/(4 文件)上游 Compose Shape API 原样迁移,零改动,不注册不编译
…/src/commonMain/…/geoshape/GraphicsShapesPort.ktgraphics-shapes 矩形路径逐表达式移植(Apache-2.0 注明)
…/src/commonMain/…/geoshape/ContinuousCornerShapeGeometry.kt公共入口与语义对齐
…/src/jvmTest/…/ContinuousCornerShapeGeometryParityTest.kt16 项逐浮点交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/…/Bridge.kt2 个 C ABI、MiniJson、错误通道
harmonyApp/…/cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/…/cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/…/ets/pages/Index.etsArkUI 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(16 节)

4.3 关键 API 对照

层次API作用
移植内核roundedRectangleCubics(w, h, [BE,BS,TS,TE], ox, oy)graphics-shapes 矩形管线的完整复刻
公共入口ContinuousCornerShapeGeometry.cubics(...)上游 createOutline 数学分支的纯几何等价物
公共入口percentRadius(w, h, percent)CornerSize(percent) 的 min(w,h) 解析语义
公共入口svgPath(cubics)Cubic 列表 → M/C/Z 指令串
NativeContinuousShapeRender / ContinuousShapeVersionC ABI JSON 导出
N-APIshapeRender(req) / shapeVersion()向 ArkTS 暴露能力
ArkTSrender() / apply(res)触发渲染并刷新 Path().commands()

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(shapeRender(JSON.stringify({
  width: 300, height: 300, topStart: 50, topEnd: 50,
  bottomEnd: 50, bottomStart: 50, smooth: 0.6, rtl: false
}))) as RenderResponse;
// res.cubicCount === 16;res.path 即 moveTo + cubicTo×16 + close 的绝对坐标指令串
Path().commands(res.path).width(300).height(300).fill(Color.Black)
val cubics = ContinuousCornerShapeGeometry.cubics(
    width = 300f, height = 300f,
    topStart = 50f, topEnd = 50f, bottomEnd = 50f, bottomStart = 50f,
    smooth = 0.6f)
val path = ContinuousCornerShapeGeometry.svgPath(cubics)

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

五、关键决策说明

决策 1:移植数学而不是重写或换库

graphics-shapes 没有 ohos 制品线,“换线不换版"失效后只剩三个选项:换第三方 squircle 库(几何语义漂移,上游行为不可复刻)、自己写近似(同样漂移)、按上游真实源码逐表达式移植。第三个选项把"漂移风险"转化为"移植保真问题”,而后者可以用真实库交叉验证闭环——JVM 上移植代码与真库输出逐浮点一致,ohos 与 JVM 又编译同一份 commonMain,推导链完整。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 升 9.6.1

qrose 的 2.2.21 旧线必须把 Gradle 降到 8.14.1;KStore 验证 2.4.20 线支持 Gradle 9.x。本仓上游 wrapper 本来就是 9.x,升到 9.6.1 与 KStore 已审核矩阵完全对齐,免降级。移植内核零依赖,不存在 kotlinx 制品线拖版本的因素,工具链选择完全由"已审核矩阵 + 上游 wrapper 形态"决定。

决策 3:根构建替换(cmp_qrose 模式),Compose API 隔离而非删除

上游根构建(AGP / Compose 编译器 / dokka / BCV / Maven Central 发布)对鸿蒙没有保留价值,整体替换为极简 ohos 构建。上游 4 个 Compose 文件 git mv 进 src/composeMain 不注册不编译——git 历史保留 rename 痕迹,文件零改动,compose.ui 出现 ohos 制品后注册回 sourceSets 即可接回。

决策 4:桥载荷是矢量几何(path)而不是位图

qrose 桥传 PNG Base64,因为条码本质是位图;形状的本质是矢量路径。SVG path 指令串让 ArkUI 用原生 Path() 绘制,无损、可缩放、可描边,cubicCount 还让几何结构直接可观测——位图会把这些全部抹掉。

决策 5:jvmTest 交叉验证,真实库只进测试 classpath

真实 graphics-shapes 的 desktop 变体是"标准答案",只挂在 jvmTest——ohos 编译永远不碰 androidx,依赖纪律与证据链各得其所。16 项用例跑的是移植内核与真库的同参数对拍,测试零改动零 mock,"内核没漂"因此有说服力。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、Temurin JDK 21、Kotlin/Native ohos 工具链(konan)、llvm-nm(cpf-llvm)、DevEco Studio 5.1.0 Release 与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :super-rounded-corner-shape:jvmTest

16 项用例全部通过(移植内核 vs 真实 graphics-shapes 1.1.0,逐 anchor/control 浮点对比,容差 1e-4):

用例类用例数钉住什么
基准形态(默认 0.6 / smooth=0 / smooth=1)3三档平滑的整条管线
不对称四角(80/40/10/120、0/70/0/25)2per-vertex 语义与零半径混合
空间裁剪(四角 200 超边长一半、混合 200/50/0/10)2cutAdjusts 逐边收敛
宽高比(640×120、100×400)2非正方形几何
RTL 镜像(60/80/60/80 + rtl)1LayoutDirection.Rtl 分支
尺度(1000×1000、2×2)2大小形状的数值稳定性
SVG path 结构 + 百分比解析4出口格式与 percentRadius 语义

6.3 链路验证

:super-rounded-corner-shape:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: libohoscontinuousshape.so + libc++_shared.so → entry/libs/<abi>/
llvm-nm -D: ContinuousShapeRender / ContinuousShapeVersion / libohoscontinuousshape_symbols 均为全局 T 符号(双 ABI)
hvigorw assembleHap → entry-default-signed.hap → hdc install → aa start:真机成功进入前台

6.4 功能用例

  1. 启动后「关于」页版本握手显示 1.0.6-ohos.1(来自真实 Native 调用,非静态文字);
  2. 形状预览页出现默认蓝色平滑圆角形状,info 行显示贝塞尔段数与尺寸(见 7.1 效果图);
  3. smooth 滑杆从 0 拖到 1,圆角从普通圆弧连续过渡到最大平滑角,无跳变;
  4. 打开「对照 smooth=0」,红色描边叠在填充形状上,外扩量即平滑角与普通圆角之差;
  5. 关闭四角联动,四条独立滑杆可做出不对称形状;info 行 cubicCount 随角形态变化(全直角 4、四角全圆角 16、混合居间);
  6. 半径拖过边长一半,形状自动收敛为半圆形态(与 Compose 行为一致);
  7. RTL 开关交换 start/end 角,镜像 ContinuousRoundedCornerShape 语义;
  8. ok:false 通道(如 width=0)错误原文进 info 行,页面不崩。

6.5 验证结论

移植几何内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,形状预览页展示了真实的几何会话:版本握手、连续平滑圆角渲染、贝塞尔段数观测、超限收敛与对照描边(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-continuous-rounded-corner-shape.git
cd compose-continuous-rounded-corner-shape
export JAVA_HOME="/path/to/jdk-21"
./gradlew :super-rounded-corner-shape:jvmTest                                # 16 项交叉验证
./gradlew :super-rounded-corner-shape:compileKotlinOhosArm64 :super-rounded-corner-shape:compileKotlinOhosX64
./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.continuousshape -m entry
hdc shell hilog | grep ContinuousShape

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让移植内核真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. graphics-shapes 是 androidx 官方库,没有也不可能有 ohos 制品线——先查 .module 变体矩阵再定路线,别把 KStore 的"换线不换版"当万能答案;
  4. androidx.collection.FloatFloatPair 不在 graphics-shapes desktop 变体的测试传递 classpath 里,import androidx.collection 直接 unresolved,jvmTest 要显式声明 androidx.collection:collection:1.5.0;
  5. Gradle 9.x 不允许 include 不存在的目录,:example:nativeApp 进 settings 前目录(含 build.gradle.kts)必须真实存在;
  6. Cubic.points 是 internal,交叉验证只能走 8 个 public 访问器拼 FloatArray——测试代码显式列出 anchor/control 顺序,顺便把语义写死;
  7. 平移与扁平化的顺序不能颠倒:transformed { +center } 在特征层、cubics 拆分在平移之后,顺序换了会引入 1e-7 级偏差,让"逐表达式一致"打折;
  8. 零长度曲线的锚点改写分支必须保留,否则直角/混合角用例的 cubic 数对不上;
  9. services.gradle.org 下载 wrapper 不稳定时,优先看本地 ~/.gradle/wrapper/dists 已缓存版本(本机有完整 9.6.1),wrapper 直接改指同版本,不要硬等 networkTimeout;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. abiFilters 必须双 ABI、每 ABI 三件套(NAPI so + KN so + libc++)缺一不可,缺 x86_64 so 真机安装报 9568347;
  12. build-profile.json5 的 signingConfigs 仓库内留空数组,DevEco 自动签名后 IDE 回填带 material 的完整配置,证书绝不进仓库(可 git update-index --skip-worktree 保护);
  13. .gitignore 根锚定盖不住嵌套目录,DevEco 生成与 Gradle 回填的 example/harmonyApp/{.idea,.hvigor,entry/build,entry/libs}、*.so 要单独追加。

8.2 已知问题

  • 上游 Compose Shape API(ContinuousRoundedCornerShape(...) 工厂、RoundedPolygon.toPath())不跨桥,鸿蒙侧交付物是几何(SVG path / Cubic 列表);
  • smooth=0 视觉等价但非 Outline.Rounded 捷径,圆弧为三次贝塞尔近似(误差远小于 1px);
  • SVG path 坐标保留 3 位小数(0.001px)控制载荷,逐浮点精确值由 JVM 测试与 cubics() API 承担;
  • graphics-shapes 的 Morph/星形/任意多边形能力未移植——上游只用到 rectangle;
  • android / js / wasmJs / apple 目标剥离(源文件留在 composeMain 与 sample),发布、dokka、BCV 一并剥离,本 fork 不发 Maven Central;
  • 上游 .github/workflows 指向 GitHub 体系,本 fork 不运行,ohos CI 待补;
  • ArkTS Demo 的 Path 坐标以 vp 传递,桥不感知 DPI(几何在相同单位下自洽)。

8.3 未来优化方向

  • 把桥扩成 .har 形态发布,让 ArkTS 工程直接依赖而不是手工拷 CMake 四件;
  • CI 加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
  • compose.ui / graphics-shapes 出现 ohos 制品后,评估把 composeMain 接回正式构建,桥降级为 fallback。

九、总结

9.1 核心难点回顾

上游 4 文件纯 commonMain(算法全在 graphics-shapes)
    → 验证 graphics-shapes 无 ohos 制品(.module 变体矩阵)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07 + Gradle 9.6.1(KStore 已审核矩阵)
    → 根构建替换(cmp_qrose 模式),Compose API 迁 composeMain 隔离
    → graphics-shapes 矩形路径逐表达式移植(零依赖 commonMain)
    → C ABI(@CName × 2)+ SVG path 载荷
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + Path().commands()
    → ArkUI 真机形状游乐场验收(smooth/四角/RTL/对照/版本握手)

9.2 封装层次

  • 移植层:graphics-shapes 1.1.0 的 RoundedCorner 三段曲线、逐边空间裁剪、特征扁平化,运算顺序与原库一致;
  • 入口层:ContinuousCornerShapeGeometry 对齐上游 createOutline 语义(RTL 镜像、百分比解析、SVG 出口);
  • Native 层:双 ABI 动态库,2 个 C ABI 输出 JSON;
  • N-API 层:参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:页面状态、JSON 组装、路径渲染、错误展示;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 依赖制品的"可达性"决定适配路线:KStore 能真编译是因为 kotlinx 补齐了 ohos 线;本库的算法依赖是 androidx 官方库,ohos 线永远不会来——路线只能切换到"移植 + 真库交叉验证"。动手前先用 .module 元数据把依赖的变体矩阵查清楚,是这批适配最便宜也最关键的一步;
  2. 移植的验收标准是浮点,不是肉眼:逐表达式保序(平移在特征层、拆分在平移后、零长度曲线改写锚点)让移植代码与真库在 1e-4 容差内一致;交叉验证跑在 JVM、产物跑在 ohos,靠的是 commonMain 同源——证据链每一环都要能独立复核;
  3. 形状类库的桥载荷选矢量不选位图:SVG path 让 ArkUI 原生 Path() 绘制,无损缩放、可描边、可观测段数;位图是条码库的正确答案,却是几何库的错误答案。

9.4 适配成果

当前适配已完成:

  • graphics-shapes 1.1.0 矩形平滑圆角数学的双 ABI Kotlin/Native 编译(逐表达式移植,零第三方依赖,Apache-2.0 出处注明);
  • 16 项 JVM 交叉验证全绿(移植内核 vs 真实 graphics-shapes 1.1.0,容差 1e-4);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证双 ABI 全局 T 符号);
  • DevEco Stage ArkUI 形状游乐场(smooth 连续过渡、四角联动/独立、超限收敛、RTL 镜像、smooth=0 对照、版本握手、贝塞尔段数观测);
  • HAP 构建、签名、设备安装和真机验证(见 7.1 效果图);
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐