开源鸿蒙平台 CMP 三方库「compose-continuous-rounded-corner-shape」适配全流程
本文记录
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(...),再把结果三次贝塞尔转成 ComposePath。本次适配与社区前两批路线(qrose 的"Painter 隔离 + 自研光栅化"、KStore 的"kotlinx 制品线补齐后真编译整库")都不同:上游唯一的算法依赖androidx.graphics:graphics-shapes是 androidx 官方库,不存在也不会出现 ohos 制品线——因此本仓把该库的矩形平滑圆角路径数学逐表达式移植为纯 KotlincommonMain内核,并用真实 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 Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向本机 Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 9.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 product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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.shapes | RoundedPolygon.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 数组) | ShapeCubic | split/reverse/straightLine/circularArc/zeroLength 全保留 |
CornerRounding | RoundingSpec | radius + smoothing |
RoundedPolygon.Companion.rectangle | roundedRectangleCubics() 第 1 步 | 顶点序 [右下, 左下, 左上, 右上],默认中心 (0,0) |
RoundedPolygon(vertices, …) 构造 | 同上第 2-5 步 | RoundedCorner 三段曲线、cutAdjusts 逐边裁剪、Corner+Edge 特征表 |
transformed { +center } | 同上第 6 步 | 逐点平移,顺序与上游 createOutline 一致 |
RoundedPolygon.cubics 扁平化 | 同上第 7 步 | first/last 对半拆分、零长度曲线丢弃与锚点改写、闭合曲线 |
MutableFloatList | FloatArray(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 方法 | 覆盖内核 |
|---|---|---|
ContinuousShapeRender | shapeRender(req) | 角度解析(px/percent)+ RTL 镜像 + 平滑圆角内核 + SVG path 出口 |
ContinuousShapeVersion | shapeVersion() | 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.toml | Kotlin 2.4.20-0.1.0-07 + 测试用 androidx 两坐标 |
build.gradle.kts(根) | 版本 1.0.6-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle/wrapper/gradle-wrapper.properties | 9.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.kt | graphics-shapes 矩形路径逐表达式移植(Apache-2.0 注明) |
…/src/commonMain/…/geoshape/ContinuousCornerShapeGeometry.kt | 公共入口与语义对齐 |
…/src/jvmTest/…/ContinuousCornerShapeGeometryParityTest.kt | 16 项逐浮点交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/…/Bridge.kt | 2 个 C ABI、MiniJson、错误通道 |
harmonyApp/…/cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/…/cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/…/ets/pages/Index.ets | ArkUI 验收页面 |
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 指令串 |
| Native | ContinuousShapeRender / ContinuousShapeVersion | C ABI JSON 导出 |
| N-API | shapeRender(req) / shapeVersion() | 向 ArkTS 暴露能力 |
| ArkTS | render() / 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) | 2 | per-vertex 语义与零半径混合 |
| 空间裁剪(四角 200 超边长一半、混合 200/50/0/10) | 2 | cutAdjusts 逐边收敛 |
| 宽高比(640×120、100×400) | 2 | 非正方形几何 |
| RTL 镜像(60/80/60/80 + rtl) | 1 | LayoutDirection.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.0.6-ohos.1(来自真实 Native 调用,非静态文字); - 形状预览页出现默认蓝色平滑圆角形状,info 行显示贝塞尔段数与尺寸(见 7.1 效果图);
- smooth 滑杆从 0 拖到 1,圆角从普通圆弧连续过渡到最大平滑角,无跳变;
- 打开「对照 smooth=0」,红色描边叠在填充形状上,外扩量即平滑角与普通圆角之差;
- 关闭四角联动,四条独立滑杆可做出不对称形状;info 行
cubicCount随角形态变化(全直角 4、四角全圆角 16、混合居间); - 半径拖过边长一半,形状自动收敛为半圆形态(与 Compose 行为一致);
- RTL 开关交换 start/end 角,镜像
ContinuousRoundedCornerShape语义; 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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让移植内核真正编译成
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- graphics-shapes 是 androidx 官方库,没有也不可能有 ohos 制品线——先查
.module变体矩阵再定路线,别把 KStore 的"换线不换版"当万能答案; androidx.collection.FloatFloatPair不在 graphics-shapes desktop 变体的测试传递 classpath 里,import androidx.collection直接 unresolved,jvmTest 要显式声明androidx.collection:collection:1.5.0;- Gradle 9.x 不允许 include 不存在的目录,
:example:nativeApp进 settings 前目录(含 build.gradle.kts)必须真实存在; Cubic.points是 internal,交叉验证只能走 8 个 public 访问器拼 FloatArray——测试代码显式列出 anchor/control 顺序,顺便把语义写死;- 平移与扁平化的顺序不能颠倒:
transformed { +center }在特征层、cubics拆分在平移之后,顺序换了会引入 1e-7 级偏差,让"逐表达式一致"打折; - 零长度曲线的锚点改写分支必须保留,否则直角/混合角用例的 cubic 数对不上;
services.gradle.org下载 wrapper 不稳定时,优先看本地~/.gradle/wrapper/dists已缓存版本(本机有完整 9.6.1),wrapper 直接改指同版本,不要硬等networkTimeout;- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); abiFilters必须双 ABI、每 ABI 三件套(NAPI so + KN so + libc++)缺一不可,缺 x86_64 so 真机安装报9568347;build-profile.json5的signingConfigs仓库内留空数组,DevEco 自动签名后 IDE 回填带material的完整配置,证书绝不进仓库(可git update-index --skip-worktree保护);.gitignore根锚定盖不住嵌套目录,DevEco 生成与 Gradle 回填的example/harmonyApp/{.idea,.hvigor,entry/build,entry/libs}、*.so要单独追加。
8.2 已知问题
- 上游 Compose
ShapeAPI(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 三条经验
- 依赖制品的"可达性"决定适配路线:KStore 能真编译是因为 kotlinx 补齐了 ohos 线;本库的算法依赖是 androidx 官方库,ohos 线永远不会来——路线只能切换到"移植 + 真库交叉验证"。动手前先用
.module元数据把依赖的变体矩阵查清楚,是这批适配最便宜也最关键的一步; - 移植的验收标准是浮点,不是肉眼:逐表达式保序(平移在特征层、拆分在平移后、零长度曲线改写锚点)让移植代码与真库在 1e-4 容差内一致;交叉验证跑在 JVM、产物跑在 ohos,靠的是 commonMain 同源——证据链每一环都要能独立复核;
- 形状类库的桥载荷选矢量不选位图: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。
参考文档
更多推荐


所有评论(0)