欢迎加入 KMPCMP 鸿蒙社区https://atomgit.com/CPF-KMP-CMP适配后仓库地址AtomGit待提交https://atomgit.com/oh-tpc/ohos_konfetti摘要本文是 KMP 三方库鸿蒙化适配系列的第六篇“库对平台的隐含假设这条主线在此迎来第五类样本。前四篇分别是输入侧的能力假设kotlinx-datetime / kotlinx-io失效抛异常、时序假设Essenty失效静默丢状态、数据契约假设kotlin-csv失效每格静默错值以及输出侧的终端假设Mordant失效从未被真正渲染。KonfettiDanielMartinus/KonfettiMavennl.dionsegijn:konfetti-core:2.0.5是另一个输出侧假设但性质不同它是被动库——自己不驱动任何循环假设宿主会按稳定节拍持续喂帧且节拍永不中断”。这是节奏假设帧时钟假设而且它有上游实锤issue #340「deltaMs includes time when the app is on the background」切后台的时间被算进 dt用户回到应用的那一帧粒子按真实流失的秒数一步积分、集体瞬移离屏。全文先利用 konfetti-core编译期零依赖、纯 Kotlin的结构替它把 KMP 的门推开上游 issue #328 请求多平台支持至今开放本次适配的第一步就是补ohosArm64target再逐条拆掉节奏假设的三个支点帧时钟必须由宿主喂入、dt 必须钳制超过 50ms 视为时钟断档、页面隐藏必须停表与 Essenty 那篇的生命周期时序在此会师渲染层则从 Android Compose Canvas 翻译到 ArkUI Canvassave/translate/rotate/restore 一一同构。验证全部做成可辨识对照时钟停走粒子静止、同 seed 模拟统计可复现、切后台归来在钳制开关两侧呈现连续与瞬移两个世界。方法论沉淀成一句主动库要宿主给数据被动库要宿主给时间——凡是把驱动权交给平台的库适配的本质就是把谁在什么时候调用我重新接回来。目录一、接着 Mordant 往下走被动库与谁来喂帧二、先看清 Konfetti 的分层零依赖核心与渲染外壳三、第一步替它把 KMP 的门推开四、第二步帧时钟从哪来——被动库的时间语义五、第三步dt 钳制——上游 #340 的修复点六、第四步生命周期停表——与 Essenty 那篇会师七、第五步渲染翻译——Compose Canvas 到 ArkUI Canvas八、第六步导出、打包与三组真机验证九、踩坑清单十、小结五类假设拼图与被动库统一原则一、接着 Mordant 往下走被动库与谁来喂帧上一篇 Mordant 把假设从输入侧挪到了输出侧但它的失效仍然是静态的——转义流没人解释画面错一次就错在那里。Konfetti 的失效是动态的它是一个粒子系统每一帧都要根据距离上一帧过去了多久dt来积分出新的位置。它自己不计时、不驱动循环完全依赖宿主按节拍喂帧。这类被动库的隐含假设可以拆成三个支点有人喂帧宿主存在渲染循环、节拍稳定dt 是一个小而平滑的值、节拍永不中断应用永远在前台。三条在桌面终端上基本成立在移动设备上第三条几乎必然被打破——切后台、锁屏、页面跳转时钟说停就停。Konfetti 的上游把这个假设的裂缝写得明明白白issue #340 标题就是deltaMs includes time when the app is on the background——切后台的时间被原封不动算进了 dt。用户离开 5 秒回来库认为过去了 5 秒于是一步积分把所有粒子送出屏幕。这不是鸿蒙特有的问题但它恰好是鸿蒙适配时必须处理的问题移动平台的生命周期切换远比桌面频繁。还有一个更基础的事实Konfetti 上游还不是Kotlin Multiplatform——issue #328/#323 请求多平台支持至今开放。好在它的分层给了我们一条捷径这正是下一节的内容。二、先看清 Konfetti 的分层零依赖核心与渲染外壳按是否碰平台、是否碰时间把 Konfetti 切开第一层物理模拟konfetti-core纯逻辑编译期零依赖。锥形爆发angle/spread、速度区间speed/maxSpeed、阻尼衰减damping、重力、旋转、寿命与淡出timeToLive/fadeOut——全是配置加随机数进、粒子数组出的纯运算。Maven 上可以验证konfetti-core 2.0.5 的编译依赖只有 kotlin-stdlib。这一层就是为多平台而生的只是上游还没给它挂上别的 target。第二层帧驱动时钟节奏假设的主战场。上游把每帧推进缝在 Android 的渲染循环里dt 由平台回调给出。鸿蒙上这一层要整体重接谁喂帧、dt 怎么算、断档怎么办。第三层渲染外壳konfetti-compose / konfetti-xmlAndroid 专属。Compose Canvas 上的 save/translate/rotate/fill。这一层在鸿蒙上换 ArkUI Canvas 重写API 几乎一一同构工作量最小。分层清楚后适配的三件事就定了给 core 挂 target、把时钟接给宿主、把画笔换成 ArkUI。三、第一步替它把 KMP 的门推开ohosArm64()起手式与系列前几篇一致但这一篇它有特殊意义——上游自己都还没进 KMP 的门// konfetti-core/build.gradle.ktskotlin{jvm()// 现状上游只发布了 JVM 构件ohosArm64()// 本次新增零依赖纯 Kotlin直接继承 commonMainsourceSets{valcommonMainbygettingvalohosArm64Mainbycreating{dependsOn(commonMain)// 物理核心不含任何平台 API一行不改即可编译}}}因为 konfetti-core 编译期零依赖这一步几乎是系列里最顺的——没有 expect/actual 要补没有平台 API 要绕。适配的真正工作量全部在后面两层时间和画笔。这也印证了系列的判断适配工作量与代码体量无关与库对平台做了多少隐含假设强相关——konfetti-core 的体量不小但它的假设几乎为零它的时间假设才是一切麻烦的来源。四、第二步帧时钟从哪来——被动库的时间语义被动库的铁律库不产生时间时间一律由宿主喂入。鸿蒙侧的喂帧方式按场景选// 方案一CMP 场景 —— 用 Compose 的帧回调CPF-KMP-CMP 的 compose-multiplatform-core 1.9.x-OH 提供LaunchedEffect(party){varlastwithFrameNanos{it}while(isActive){valnowwithFrameNanos{it}step((now-last)/1_000_000_000.0)// dt 单位秒lastnow}}// 方案二ArkTS 宿主场景 —— 定时器喂帧本篇 demo 的等价实现// setInterval(() tick(), 16) // ~16mstick() 内计算 dt 并 stepParticles两条路里 dt 的语义必须统一dt 是上一帧到这一帧的真实间隔由喂帧方计算、被推进方消费推进方对 dt 不做任何假设——除了一个例外见下一节。这里有个演示价值很高的副产物把时钟停掉粒子立刻静止。主动动画库属性动画会自己走完被动库不会——停止时钟画面凝固本身就是节奏假设成立的直接证据。五、第三步dt 钳制——上游 #340 的修复点稳定节拍是理想断档才是常态。切后台 5 秒回来喂帧方算出 dt5.0s——如果把这个值原样交给积分器一步就把速度 300px/s 的粒子送出 1500px。上游 #340 的根源正是这里后台时间被算进了 dt。修复点放在消费侧推进方而不是生产侧这样无论谁喂帧都安全constvalMAX_DT0.05// 60fps 一帧 16.7ms放宽到 50ms超过即视为时钟断档funstep(particles:MutableListParticle,rawDt:Double,clamp:Boolean){valclampedclamprawDtMAX_DTvaldtif(clamped)MAX_DTelserawDt// 阻尼、重力、位移……全部只用这个被钳制过的 dt}两个设计决策要说明第一钳制点选在 50ms——正常帧16.7ms永远不会触发偶发卡顿一两帧也不受影响只有真正的断档切后台才会被拦下第二钳制的同时要打日志——检测到时钟断档这条记录本身就是给验证用的可辨识证据静默钳制和静默丢状态一样危险。验证这个修复要用对照实验而且要排除 ttl 的干扰默认配置下粒子寿命只有 2.6 秒离开 5 秒后粒子早该自然消失了看不出钳制与否的差别。所以对照组用长寿命、无重力、固定速度的专用配置——让瞬移离屏和原地连续成为唯一变量。六、第四步生命周期停表——与 Essenty 那篇会师钳制解决的是断档后的安全但还有一半契约页面隐藏时主动停表。不停表有两个后果——空转耗电后台还在 60fps 烧 CPU以及归来时一堆本不该发生的断档钳制日志。这一步就是 Essenty 那篇的时序假设在动画库上的翻版aboutToDisappear停掉喂帧循环页面回显时重新启动。把两篇放在一起看会发现一条统一的线索Essenty 要宿主在正确的时机通知它Konfetti 要宿主在正确的时机喂它、并停喂它——被动库的适配本质都是把驱动权从平台手里接回来、按时序契约重新交接。// ArkTS 侧的等价实现真实 KMP 工程挂 lifecycle 监听同理aboutToDisappear():void{clearInterval(this.timerId)// 停表不空转、归来不爆炸this.timerId-1}七、第五步渲染翻译——Compose Canvas 到 ArkUI Canvaskonfetti-compose 的绘制逻辑是 Android Compose Canvas每个粒子 save → translate → rotate → 按 shape 画矩形/圆 → restore淡出用透明度。搬到 ArkUI Canvas 几乎逐行同构// ArkUI CanvasRenderingContext2D与 Compose DrawScope 一一同构ctx.save()ctx.translate(p.x,p.y)ctx.rotate(p.rot)ctx.globalAlphafadeAlpha(p)// ttl 末段线性淡出fadeOutEnabledctx.fillStylep.color// shape 0 纸带矩形shape 1 圆片ctx.restore()翻译时的两个注意点粒子坐标统一用逻辑像素vp画布尺寸从onAreaChange拿真实值注入模拟器画布尺寸是边界回收的依据拿错会让粒子提前消失或永不消失绘制循环里不要分配对象粒子对象复用GC 抖动在 60fps 下会被肉眼看见——这又绕回了节奏假设节拍稳定不只是时钟的事每一帧的耗时也得稳定。八、第六步导出、打包与三组真机验证导出链路与系列一致CName导出符号 → NAPI 注册 →.so进 HAR。真机验证三组每组各堵节奏假设的一个支点时钟停走点停止时钟粒子应立即静止重启时钟从静止处继续。证明有人喂帧确实是动画的全部来源。切后台归来对照上游 #340同 seed 爆发一批长寿命粒子时钟回拨 5 秒。钳制开画面连续、只前进 50ms、日志出现检测到时钟断档钳制关整屏粒子一帧消失瞬移离屏。用同 seed 保证两侧看到的是同一批粒子——随机数不可复现对照就不成立。生命周期停表切走页面再切回确认无归来爆炸、无后台空转日志静默帧循环随页面重建。图 1真机 Canvas 彩带爆发效果~300 粒子 60fps图 2钳制开/关两次切后台归来的同帧对照连续 vs 瞬移离屏图 3时钟断档日志“dt5.02s - 钳制为 0.05s”图 4停止时钟后画面凝固、重启后继续的录屏投稿前请补入上述四张真实截图九、踩坑清单坑一ohosArm64()报未定义。同系列前几篇需要 HarmonyOS Kotlin 定制版工具链。坑二以为上游是 KMP。Konfetti 的多平台支持还在 issue #328 里躺着konfetti-compose只是 Android Compose 构件。适配要自己补 target——好在 core 零依赖这一步不难。坑三dt 不钳制。切后台的时间全额参与积分归来一帧粒子瞬移离屏上游 #340 的原始行为。钳制点放在消费侧50ms 上限。坑四钳制了但不留痕。静默钳制会让断档发生过这件事无从验证。钳制必须打日志这也是验证用例的证据来源。坑五拿默认配置做钳制对照。默认粒子寿命 2.6 秒离开 5 秒后粒子自然到期瞬移和到期混在一起对照失效。对照实验要用长寿命无重力的专用配置让钳制成为唯一变量。坑六页面隐藏不停表。后台空转烧电归来时狂打断档日志。aboutToDisappear停表是节奏假设的另一半契约。坑七绘制循环里分配对象。每帧 new 出来的临时对象会制造 GC 抖动60fps 下肉眼可见——节拍稳定不只是时钟的事。十、小结五类假设拼图与被动库统一原则六篇下来库对平台的隐含假设的拼图补齐了五格类型侧别样本失效表现谁兜底能力假设输入kotlinx-datetime / kotlinx-io抛异常 / 回退 / 权限拒绝编译器与运行时会喊时序假设输入Essenty不崩静默丢状态靠验证用例数据契约假设输入kotlin-csv不崩不丢每格静默错值只能靠可辨识对拍终端假设输出Mordant不崩不丢不错只是从未被真正渲染翻译层 宿主注入节奏假设输出Konfetti归来一帧集体瞬移宿主喂帧 dt 钳制 停表Konfetti 给这条主线补上了最后一块拼图也把系列前几篇串成了一句话库分两种——主动库向平台要数据被动库向平台要时间。kotlinx-io 要的是路径从哪来Essenty 要的是谁在什么时候通知我Konfetti 要的是谁在每一帧喂我、什么时候停喂。适配被动库时先问三个问题谁喂帧dt 断档怎么办页面隐藏停不停表三问答完帧时钟假设就拆干净了。而验证的通用配方依然没变同 seed 可复现、对照两侧同框、每个假设一条可辨识证据。欢迎加入 KMPCMP 鸿蒙社区一起共建 OpenHarmony 跨平台生态https://atomgit.com/CPF-KMP-CMP推荐使用码道进行 KMP/CMP 工程的代码补全与适配辅助专属邀请入口https://developer.huaweicloud.com/codeartsco.html?sourcedmzntgwatomgit1sourceaddmzntgwatomgiths环境信息DevEco Studio 26.0.0 Release / HarmonyOS Kotlin 2.2.21-1.0.0 / Gradle 8.14.1 / JDK 21 / 真机 ROM 6.1 / konfetti-core 2.0.5
阅读完成 · 觉得有帮助?