Compose Multiplatform 三方库 compose-icons(Octicons)的 OpenHarmony 鸿蒙化适配实战(fill 模式图标包验证:一套 shim 平移,踩中 ArkUI 椭圆弧大坑)
库版本:compose-icons(Octicons 包,414 个图标)|验证环境:Compose Multiplatform 生态 / Kotlin 2.2.21-1.0.0(鸿蒙定制版)|DevEco Studio 26.0.0|DevEco 模拟器|HarmonyOS 7.0.0(API 26)
我之前Tabler Icons 适配验证了「shim 记录几何数据 → JSON → ArkUI Path() 渲染」这条路径的可行性,但 Tabler 是stroke 模式图标(线条描边),文末 FAQ 留了一个问题:fill 模式(实心填充)图标包怎么办?本文就是那个问题的答案——把同一套 shim 平移到 GitHub 官方图标库Octicons(414 个图标,16px + 24px 双尺寸),验证「数据定义类库」适配路径对填充型图标包的通用性。
结论先行:414 个 Octicons 图标源码零修改,整条链路(Kotlin/Native.so→ NAPI → ArkTS → ArkUIPath()fill 渲染)最终跑通。但与 Tabler 版不同,本次踩中一个 Tabler 没暴露的大坑——ArkUIPath().commands()不支持 SVG 椭圆弧命令A/a,而 Octicons 几乎每个图标都靠它画圆形/圆角,导致首版渲染里所有含圆弧的图标全部变形(Search 放大镜变成实心圆)。最终在 shim 的arcTo出口处按 SVG 1.1 规范把椭圆弧展平成多条三次贝塞尔C曲线解决——上游图标源码依然零修改,改动全部收敛在 shim 一个文件里,架构的可复用性反而得到了更扎实的验证。
先睹为快:DevEco 模拟器实测,414 个 Octicons 图标(16px + 24px 双尺寸)由 Kotlin/Native 侧导出几何数据、ArkUI Path() 按 fill 模式渲染——Search 放大镜空心圆镂空、CheckCircle/PlusCircle 等圆形图标全部正确
一、适配目标与整体链路
目标:在鸿蒙模拟器里跑一个 ArkTS 应用,页面加载时真实调用Kotlin/Native 里的 compose-icons Octicons 图标构建代码,把精选图标的几何数据(SVG path 字符串 + fill 颜色 + alpha)拉到 ArkTS,用 ArkUIPath()组件按fill 模式渲染成图标网格——验证同一套 shim 对填充型图标包的通用性。
整体链路(与 Tabler 版完全一致,仅数据模式不同):
ArkTS (Index.ets) │ import icons_napi from 'libicons.so' ▼ NAPI 调用 getFeaturedIcons() / getIconCount() libicons.so ← C++ NAPI 薄层(entry/src/main/cpp/napi_init.cpp) │ ▼ extern "C" 调用 libohosicons.so ← Kotlin/Native (ohosArm64 / ohosX64) │ ▼ octicons 模块 → compose-icons 上游源码 + 自研 androidx.compose.ui shim(零修改)适配目标的最终效果:
二、Octicons 与 Tabler 的差异:为什么值得单独适配
Octicons 是 GitHub 的官方图标库,在 compose-icons 里的形态与 Tabler 相同(Kotlin 源码 + ImageVector DSL),但数据模式有三个关键差异:
| 维度 | Tabler Icons | Octicons |
|---|---|---|
| 渲染模式 | stroke(线条描边,stroke = SolidColor(...), fill = null) | fill(实心填充,fill = SolidColor(Color(0xFF000000)), stroke = null) |
| 尺寸体系 | 单一 24px viewport | 16px + 24px 双尺寸并存(Home16/Home24) |
| 图标数量 | 185 个(当前收录) | 414 个(16/24 两版合计) |
| 几何命令 | 纯直线/贝塞尔(M/L/C/Q/Z),零arcTo | **大量arcTo/arcToRelative(400+ 处)**画圆形/圆角 |
这三个差异恰好覆盖了 Tabler 版 FAQ 里预留的全部扩展点:
- fill 模式:Tabler 版 ArkTS 用
stroke()+fillOpacity(0)渲染线条;Octicons 必须反过来——fill()+strokeOpacity(0)渲染实心形状。shim 的PathData早已记录了fill: Color?/fillAlpha字段,JSON 契约只需把这两个字段真正用起来。 - 双尺寸:Octicons 的
AllIcons里同一个图标有 16px 和 24px 两个版本,viewport 不同(16 vs 24),序列化时各自独立成条目——天然验证了 JSON 契约对多 viewport 的兼容性。 - 数量翻倍:414 个图标的 JSON 达 154KB(Tabler 精选 185 个约 67KB),验证链路在更大数据量下的表现——实测依然毫秒级。
第四个差异是本次踩坑的根源:Tabler 的 185 个图标里没有任何一个用到arcTo(SVGA/a椭圆弧命令),而 Octicons 几乎每个图标都用它画圆形和圆角——这个差异在 Tabler 版完全没暴露,到 Octicons 才引爆(详见第五章踩坑)。
三、工程结构
octicons-ohos-demo/ ├── octicons/ # 库模块:shim + 上游图标源码 │ └── src/commonMain/kotlin/ │ ├── androidx/compose/ui/ # shim(仅 ImageVector.kt 一处改动:椭圆弧展平) │ │ ├── graphics/Color.kt # 与 Tabler 版相同 │ │ ├── graphics/vector/ImageVector.kt # ★ 本次唯一改动文件(arcTo → C 曲线展平) │ │ ├── graphics/vector/Path.kt # 与 Tabler 版相同 │ │ └── unit/Dp.kt # 与 Tabler 版相同 │ ├── compose/icons/__Octicons.kt # AllIcons(414) / AllIconsNamed 注册表 │ └── compose/icons/octicons/*.kt # 414 个上游图标文件(零修改) ├── example/ │ ├── nativeApp/ # Kotlin/Native 桥接层 → libohosicons.so │ │ └── src/ │ │ ├── commonMain/kotlin/IconBridge.kt # FEATURED 列表 + fill 模式 JSON │ │ └── ohosMain/kotlin/IconExport.kt # @CName 导出(与 Tabler 版相同) │ └── ohosApp/ # ArkTS 鸿蒙应用(bundleName: com.example.octiconsdemo) │ └── entry/src/main/ │ ├── cpp/napi_init.cpp # C++ NAPI 薄层(与 Tabler 版相同) │ ├── ets/pages/Index.ets # ArkTS fill 模式渲染 │ └── libs/{arm64-v8a,x86_64}/ # 双 ABI so(4.6MB / 4.1MB) └── settings.gradle.kts / build.gradle.kts与 Tabler 版的差异点只有四处:库模块名(octicons)、IconBridge.kt的 FEATURED 列表与 JSON 序列化(fill 字段)、Index.ets的渲染模式,以及shim 的ImageVector.kt一处椭圆弧展平——C++ NAPI 薄层、@CName出口全部原样复用。
四、适配过程:三个关键步骤
4.1 Gradle 工程配置(与 Tabler 版同构)
鸿蒙定制工具链(2.2.21-1.0.0)的pluginManagement仓库配置不变,模块名从tabler-icons换成octicons:
// settings.gradle.ktspluginManagement{repositories{maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")// 必须第一位mavenCentral()gradlePluginPortal()}}dependencyResolutionManagement{repositories{/* 同上 */}}rootProject.name="octicons-ohos-demo"include(":octicons",":example:nativeApp")4.2 核心:fill 模式的 JSON 序列化(IconBridge)
上游图标源码零修改,桥接层新增 fill 字段。Octicons 的图标定义长这样(注意fill有值、stroke = null,且大量使用arcToRelative):
publicvalOcticons.Search24:ImageVectorget(){_search24=Builder(name="Search24",defaultWidth=24.dp,defaultHeight=24.dp,viewportWidth=24.0f,viewportHeight=24.0f).apply{path(fill=SolidColor(Color(0xFF000000)),stroke=null,...,pathFillType=EvenOdd){moveTo(14.53f,15.59f)arcToRelative(8.25f,8.25f,0.0f,true,true,1.06f,-1.06f)// ← 放大镜外圆lineToRelative(5.69f,5.69f)...moveTo(2.5f,9.25f)arcToRelative(6.75f,6.75f,0.0f,true,true,11.74f,4.547f)// ← 放大镜内圆(镂空)...close()}}.build()return_search24!!}IconBridge.iconToJson()相比 Tabler 版新增两个字段:
funiconToJson(name:String,icon:ImageVector):String=buildString{append("{")append("\"name\":\"").append(name).append("\"")append(",\"viewportWidth\":").append(icon.viewportWidth)// 16 或 24(双尺寸)append(",\"viewportHeight\":").append(icon.viewportHeight)append(",\"paths\":[")icon.paths.forEachIndexed{i,p->if(i>0)append(",")append("{")append("\"d\":\"").append(escape(p.svgPath)).append("\"")append(",\"fill\":\"").append(colorHex(p.fill)).append("\"")// 新增:fill 颜色append(",\"fillAlpha\":").append(p.fillAlpha)// 新增:fill alphaappend(",\"strokeWidth\":").append(p.strokeLineWidth)// ... strokeCap / strokeJoin / fillRuleappend("}")}append("]")append("}")}colorHex把Color(0xFF000000)转成#000000(commonMain 里没有String.format,手动按位转 hex)。FEATURED 列表精选 207 个图标,16px 与 24px 混排,覆盖导航、Git 工作流、文件、安全、品牌(LogoGithub/MarkGithub/Octoface)等分类。
一个 Kotlin 语义细节:Octicons 的图标属性是Octicons对象的扩展属性(val Octicons.Home24),桥接层引用时必须写Octicons.Home24而非裸Home24,且需要import compose.icons.AllIcons(同样是扩展属性)——这是初次编译报 200+ 个receiver type mismatch的原因。
4.3 ArkTS 渲染:fill 模式 + 居中缩放
Index.ets的渲染核心与 Tabler 版对称——fill 与 stroke 互换。另一个细节是缩放:Kotlin 导出的 path 坐标是相对 viewport(0~16 或 0~24)的,必须先按 viewport 尺寸布局、再绕中心缩放到目标像素尺寸,否则scale默认绕左上角会导致放大后偏移:
// Tabler 版(stroke 模式)Path().commands(...).fillOpacity(0).stroke('#4FC3F7').strokeWidth(this.strokeScale(icon))// Octicons 版(fill 模式 + 居中缩放)Path().commands(icon.paths.map((p:PathJson)=>p.d).join(' ')).fill(tint)// JSON 里的 fill 颜色,回退主题色.fillOpacity(this.iconFillAlpha(icon))// JSON 里的 fillAlpha.strokeOpacity(0).width(icon.viewport)// 先按 viewport 尺寸布局.height(icon.viewport).scale({x:sizePx/icon.viewport,y:sizePx/icon.viewport,centerX:'50%',centerY:'50%'})// 绕中心放大到 56px,不偏移C++ NAPI 薄层(napi_init.cpp)、@CName出口(IconExport.kt)与 Tabler 版逐字节相同——C ABI 符号名(OhosIconsFeatured/OhosIconsCount/OhosIconsLastError/OhosIconsFree)不变,so 名(libohosicons.so)不变,两个应用甚至可以并排装在同一台模拟器上(bundleName 分别为com.example.composeiconsdemo/com.example.octiconsdemo)。
五、踩坑记录(4 个,第 1 个是本次核心)
| 坑 | 现象 | 解法 |
|---|---|---|
ArkUIPath.commands不支持 SVGA/a椭圆弧命令(核心坑) | 首版渲染:Search 放大镜变成实心圆,CheckCircle/XCircle/PlusCircle 等所有含圆弧的图标全部变形,纯直线图标(Check/X/Plus/Dash)正常 | 在 shim 的arcTo/arcToRelative出口处按 SVG 1.1 规范(endpoint → center 参数化)把椭圆弧展平成多条三次贝塞尔C曲线,导出的 JSON 只剩M/L/C/Q/Z——ArkTS 侧零改动 |
| 扩展属性 receiver 丢失 | 桥接层裸引用Home24编译报 200+ 个receiver type mismatch | Octicons 图标是Octicons对象的扩展属性,引用必须带 receiver(Octicons.Home24),AllIcons同理需单独 import |
| 部分图标无 24 版 | Unresolved reference 'LogoGithub24'等 14 个报错 | Octicons 部分图标只有 16px 版(LogoGithub/MarkGithub/Markdown/ThreeBars…),FEATURED 列表改用 16 版 |
ArkUIscale默认绕左上角 | 图标放大到 56px 后在卡片里偏移、不居中 | 先按 viewport 尺寸布局,再.scale({ ..., centerX: '50%', centerY: '50%' })绕中心缩放 |
椭圆弧展平的核心实现(shim 唯一改动)
为什么 Tabler 没踩这个坑?因为它的 185 个图标没有任何一个用arcTo(stroke 线条图标用直线/贝塞尔就够了);而 Octicons 的圆形、圆角全靠椭圆弧——arcTo/arcToRelative在 414 个图标文件里出现了400+ 处。当Path().commands(...)解析到A命令时中断,后续的内圆镂空路径被丢弃,EvenOdd 填充退化成只画外轮廓——放大镜就成了实心圆。
修复方案是改 shim 而不是改上游图标(414 个文件零修改的承诺不破):在PathBuilder的arcTo/arcToRelative里直接输出贝塞尔曲线。算法是 SVG 1.1 规范附录 F.6 的标准转换——endpoint 参数化转 center 参数化,再按每段 ≤90° 切成多条三次贝塞尔:
// ImageVector.kt —— PathBuilder 内部funarcTo(rx:Float,ry:Float,theta:Float,isMoreThanHalf:Boolean,isPositiveArc:Boolean,x1:Float,y1:Float):PathBuilder{appendArcAsCubics(rx,ry,theta,isMoreThanHalf,isPositiveArc,x1,y1)currentX=x1;currentY=y1returnthis}privatefunappendArcAsCubics(rx,ry,xAxisRotationDeg,largeArc,sweep,endX,endY){// 1. (x1,y1) → (x1',y1') 变换到旋转前坐标系// 2. 半径过小则按 sqrt(lambda) 放大校正// 3. 求中心点 (cx', cy') → 变回原坐标系 (cx, cy)// 4. 求起始角 θ1 与扫掠角 Δθ(按 sweep 修正到正确象限)// 5. 按 ≤90° 分段,每段用一条三次贝塞尔逼近:valt=(4f/3f)*tan(delta/4f)// 控制点系数// 单位圆控制点 → 缩放 + 旋转 + 平移映射回椭圆curveTo(mapX(p1x,p1y),mapY(p1x,p1y),mapX(p2x,p2y),mapY(p2x,p2y),mapX(p3x,p3y),mapY(p3x,p3y))}改完后导出的 JSON 里 Search24 的d字段从M14.53 15.59A8.25 8.25 ...(含A)变成纯M...C...C...C...Z(贝塞尔展开),ArkUI 原样吃下。这个改动对 Tabler 版零影响(它本来就不含arcTo),且对未来的 Feather/FontAwesome/Material 等图标包自动生效——圆形元素多的图标包都能直接受益。
六、运行效果(DevEco 模拟器实测)
Demo 深色图标网格页:顶部标题 + 状态栏(图标数/耗时)+ 图标按 56px 四列排布,fill 模式实心渲染。
首屏加载即拉取全部精选图标:
向下滚动查看 Git 工作流与品牌图标区:
继续滚动到底部时间与表情图标区:
真实性验证(hilog)——页面加载有日志铁证:
ComposeIconsNapi: getIconCount=414 ComposeIconsNapi: getFeaturedIcons: count=207 ComposeIconsNapi: getFeaturedIcons len=157772 head=[{"name":"Home","viewportWidth":24.0,"viewportHeight":24.0,"paths":[{"d":"M11.03...渲染正确性验证——椭圆弧展平前后对比:
| 图标 | 修复前(含A命令) | 修复后(贝塞尔展开) |
|---|---|---|
| Search | 实心圆(镂空丢失) | 放大镜:空心圆 + 斜柄 |
| CheckCircle | 实心圆 | 圆形轮廓 + 内部对勾 |
| XCircle / PlusCircle / NoEntry | 实心圆 | 圆形镂空 + 内部符号 |
| Check / X / Plus / Dash(纯直线) | 正常 | 正常(不受影响) |
每个像素都来自 Kotlin/Native 侧导出的真实几何数据,非 mock——414 个图标的注册表全量可访问,FEATURED 精选 207 个,单次拉取 30ms。
七、FAQ
Q1:这次还是"shim 零修改"吗?不是了。Tabler 版的四文件 shim 在 Octicons 工程里改了一个文件(ImageVector.kt):arcTo/arcToRelative从直接透传A/a命令改为椭圆弧展平成贝塞尔曲线。其余三文件(Color.kt/Dp.kt/Path.kt)依然零修改。这次改动恰恰说明:shim 的抽象边界是对的——fill/stroke 模式、双尺寸都能零改动吃下,唯一的缺口是 ArkUI 渲染端对 SVG 命令集的支持度(缺A),而补齐这个缺口只需要在 shim 的几何出口处做一次命令降级,上游 414 个图标文件和 ArkTS 渲染侧都不用动。
Q2:16px 和 24px 双尺寸怎么处理?各自独立成 JSON 条目(viewport 16 或 24),ArkTS 侧先按 viewport 尺寸布局、再绕中心缩放到 56px 显示尺寸——同一套渲染代码对任意 viewport 通用。
Q3:fill 颜色都是黑色,JSON 里带颜色有意义吗?有。上游Color(0xFF000000)是图标的默认色,真实业务里会按主题重着色。JSON 契约保留颜色字段后,多色图标包(如 FontAwesome 的品牌色)可以零改动接入。
Q4:414 个图标全序列化会怎样?当前 FEATURED 精选 207 个(154KB JSON,30ms)。全量 414 个约 300KB,单次拉取依然是毫秒级;如果追求极致,可以走"编译期预生成 rawfile"路线(见 Tabler 版扩展方向)。
Q5:下一个图标包还需要做什么?按本次经验,平移一个新图标包(Feather / FontAwesome / Material…)的工作量 = 换 FEATURED 列表 + 确认渲染模式(fill 或 stroke)+ ArkTS 三行渲染参数。shim(含椭圆弧展平)、NAPI、出口层全部不动——而且经过 Octicons 这次,shim 对含大量圆弧的图标包也验证过了,后续平移的意外成本更低。
八、总结与参考
Octicons 适配把 Tabler 版的"数据定义类库"路径从单模式验证推进到模式覆盖验证:stroke 与 fill 两种渲染模式、16/24 双尺寸体系、414 个图标的数据量、400+ 处椭圆弧几何,同一套架构全部吃下。更重要的是,本次踩中并修复了 Tabler 没暴露的ArkUIA命令坑——这恰好证明了「shim 记录几何 → JSON 契约传输 → ArkUI 原生渲染」这条链路的可调试性与可收敛性:渲染端的命令支持缺口,可以在 shim 的几何出口处一次性补齐(椭圆弧 → 贝塞尔降级),而不需要触碰上游任何一个图标文件。
至此 compose-icons 的适配方法论已经收敛:shim(含命令降级)记录几何 → JSON 契约传输 → ArkUI 原生渲染。剩余的图标包(Feather、FontAwesome、Material、Simple Icons…)都是这条路径上的重复劳动,可以按需批量平移。
- 上游库:https://github.com/DevSrSouza/compose-icons
- 鸿蒙定制仓库:https://maven.eazytec-cloud.com/nexus/repository/maven-public/
- OpenHarmony 三方库社区:https://atomgit.com/oh-tpc
- 欢迎加入 KMP&CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP
- 本问适配源码仓库:https://atomgit.com/oh-tpc/compose-icons