HarmonyOS 多设备开发实践:我把 SysCap、断点和折叠屏适配放到了同一套架构里
HarmonyOS 多设备开发实践
- HarmonyOS 多设备开发实践:我把 SysCap、断点和折叠屏适配放到了同一套架构里
- 一、为什么要做鸿蒙原生
- 二、第一个坑:不要判断设备,要判断能力
- 三、第二个坑:窗口才是不变的锚点
- 四、第三层:状态连续比布局适配更容易被忽略
- 五、最终架构长什么样
- 六、几个关键代码片段
- 七、实际效果和一点感想
一、为什么要做鸿蒙原生
去年下半年,我把之前用 React + Pixi.js 写的一个消除小游戏《叠叠消》搬到了鸿蒙 NEXT 上。
说实话,一开始我觉得这事不复杂。游戏逻辑是现成的,ArkUI 声明式写 UI 和 React 差别也没那么大,Canvas 渲染路径也有现成的方案。最大的"挑战"无非就是适配——手机、折叠屏、平板,三种屏幕,响应式布局搞定,对吧?
等真正上手写了两个月,我才发现自己想简单了。
问题从来不在 UI 怎么画,而在于:你用什么维度来判断当前是什么环境?
设备类型?窗口宽度?还是能力?每一项选错了,后面就是一串 if-else 补丁。这篇文章就把我在这条路上踩过的坑和最终的架构方案梳理一下。
二、第一个坑:不要判断设备,要判断能力
我一开始的写法,和大多数人一样:
if(isPhone){// 显示扫码入口}后来上了折叠屏,加了isFoldable。又考虑平板,加了isTablet。再后来鸿蒙设备越来越多——折叠屏还有折叠态和展开态,每个态的设备特征还不一样。if-else 越堆越多,每次新设备发版我都要改一遍代码。
这个问题的根源在于:我们在用设备类型去代理本该由能力来决定的事情。
HarmonyOS 提供了一套叫 SysCap(System Capability)的机制,核心就是一个 API——canIUse()。
// 不需要判断是不是手机// 直接问:有没有摄像头?if(canIUse('SystemCapability.Multimedia.Camera')){// 显示扫码入口}我在项目里遇到三个真实场景:
Camera 扫码。游戏里有个扫码兑换礼包的功能,折叠屏在折叠态下摄像头位置变了,但这不是代码该关心的事。canIUse('Camera')返回 true,入口就亮,返回 false 就隐藏。
SoftBus 附近联机。鸿蒙的分布式软总线可以实现附近设备联机对战,但不是所有设备都支持。用 SysCap 检测一下,支持就显示联机入口,不支持就隐藏。
NFC 碰一碰。同理,有没有 NFC 能力,一查便知。
这套逻辑想通之后,我删掉了项目里所有isPhone、isFoldable、isTablet之类的判断。代码量没少多少,但心理负担少了一大半——以后出什么新设备,都不需要改我这层代码了。
三、第二个坑:窗口才是不变的锚点
如果说 SysCap 解决的是"有没有能力"的问题,那布局适配解决的是"屏幕有多大"的问题。
官方文档反复强调一句话:页面适配,本质上是窗口适配。我当时没太当回事,直到在折叠屏上翻了车。
我一开始的做法是监听display.on('foldStatusChange'),展开时切双列布局,折叠时切单列布局。看起来挺合理的,直到我遇到了三个场景:
- 分屏模式:窗口宽度缩到一半,foldStatus 没变,布局没反应
- 悬浮窗:窗口变成小方块,foldStatus 还是展开态,内容全挤在一起
- 外接显示器:根本不是折叠屏,更不会触发
foldStatusChange告诉我的是"设备的物理形态变了",而布局适配需要知道的是"当前窗口给我留了多少空间"。这是两件完全不同的事。
后来改成了这套链路:
Window → Breakpoint → Layout核心就是监听windowSizeChange,然后通过断点(Breakpoint)映射到布局:
// 监听窗口变化display.on('windowSizeChange',(size:window.Size)=>{constbreakpoint=getBreakpoint(size.width);AppStorage.Set('breakpoint',breakpoint);});// 断点映射functiongetBreakpoint(width:number):string{if(width<520)return'sm';if(width<840)return'md';return'lg';}断点信息通过AppStorage全局广播,所有页面自动刷新布局。具体的布局逻辑交给了LayoutManager,游戏引擎本身完全不知道外面是手机还是折叠屏。
以《叠叠消》为例,手机竖屏(~360vp)是上下结构:
┌──────────┐ │ Board │ │ 图案堆区 │ ├──────────┤ │ Slot │ │ 收集槽 │ └──────────┘折叠屏展开(~800vp)变成左右结构:
┌──────────┬──────────┐ │ │ Tool │ │ Board │ 道具栏 │ │ 图案堆 ├──────────┤ │ │ Slot │ │ │ 收集槽 │ └──────────┴──────────┘GameEngine 只处理消除逻辑,不需要知道外面是哪种布局。LayoutManager 拿到断点后决定排布方式,丢到 GridRow/GridCol 里就行。
分屏、悬浮窗、PC 大屏,全部走同一套逻辑,不需要单独处理。
四、第三层:状态连续比布局适配更容易被忽略
UI 适配做完了,但还有一个问题:用户手机玩一半,换平板登录,进度能续上吗?
多设备开发如果只适配了屏幕,没适配状态,用户感知到的就不是"多端无缝",而是"换个设备从头再来"。
这个项目的方案是用鸿蒙 CloudDB:
// CloudDB 监听云端变更cloudDB.on('snapshot',(snapshot:databases.Snapshot)=>{if(snapshot.eventType===databases.EventType.MONITOR_SNAPSHOT){// 云端数据变了,合并到本地mergeProgress(snapshot.data);}});// 本地数据变更时写入云端functionsaveProgress(level:number,stars:number){preferences.put('level',level);preferences.put('stars',stars);cloudDB.executeUpsert('user_progress',{level,stars});}策略是本地优先写入,云端冲突时以云端版本为准。实际体验下来,换设备登录华为账号后,5 秒内进度完全恢复。
少写一个后端服务,而且天然支持多设备同步。如果自己搭服务器做进度同步,光是联调测试就要花不少时间。
五、最终架构长什么样
把前面三层的逻辑串起来,最终的架构是这样的:
Device(手机 / 折叠屏 / 平板) │ ┌────────┴────────┐ │ │ SysCap Window (能力检测) (窗口状态) │ │ └──────┬───────────┘ │ Breakpoint (sm / md / lg) │ LayoutManager (布局决策) │ GameEngine (游戏逻辑——完全不变) │ GameState / ArkUI (渲染层)每层的职责:
- SysCap 层:回答"有没有能力"——Camera、SoftBus、NFC 等,与 UI 无关
- Window 层:回答"窗口多大"——监听
windowSizeChange,不关心设备类型 - Breakpoint 层:把窗口宽度映射为 sm/md/lg,全局广播
- LayoutManager:根据 breakpoint 决定布局方案
- GameEngine:纯游戏逻辑,对设备、窗口、布局一概不知
这套架构跑通之后,后面再增加新设备形态——比如鸿蒙车机、智慧屏——只需要在 LayoutManager 里加一组新的断点映射就行,其他层完全不动。
六、几个关键代码片段
所有代码里,真正核心的就这么几段:
能力检测:
// 动态检测——扫码入口要不要显示if(canIUse('SystemCapability.Multimedia.Camera')){// 显示扫码按钮}断点监听与全局广播:
constlistener=mediaquery.matchMediaSync('(min-width: 600vp)');listener.on('change',(result:mediaquery.MediaQueryResult)=>{constbp=result.matches?'md':'sm';AppStorage.Set('breakpoint',bp);});窗口变化驱动断点重算:
display.on('windowSizeChange',(size:window.Size)=>{constbp=size.width<520?'sm':size.width<840?'md':'lg';AppStorage.Set('breakpoint',bp);});ArkUI 属性动画(收集槽图案归位):
// 收集槽相同图案自动聚合,属性动画驱动位移animateTo({duration:150,curve:Curve.FastOutSlowIn},()=>{this.slotPositions=computeAggregatedPositions(this.slots);});ArkUI 的animateTo直接对状态变量做补间,不用操作 DOM 元素。对于收集槽频繁的位移动画来说,这比手动算 transform 省心多了。
七、实际效果和一点感想
项目最终跑起来的效果:
- 手机 ✓、折叠屏 ✓、平板 ✓
- 窗口缩放适配 ✓
- 状态跨设备连续 ✓
- 能力动态适配 ✓
- 包体 28MB,冷启动 1.8s,游戏页稳定 60fps
做这个项目之前,我以为多设备开发最大的工作量是 UI 适配。做下来发现,UI 适配反而是最后一步。真正决定代码能不能维护下去的,是前期有没有把SysCap(能力层)、Breakpoint(窗口层)和状态连续放进同一套架构里想清楚。
现在回头看,那个用if (isPhone)开头的版本,其实不是在适配设备,是在逃避思考。设备是无限的,窗口是有规律的,能力才是真正需要关心的。想明白这三层的关系,后面加什么设备都不慌。
你的项目里,真的需要判断设备类型吗?
参考资料
- HarmonyOS 开发文档 - SysCap
- HarmonyOS 开发文档 - 多设备布局
- HarmonyOS 开发文档 - CloudDB