HarmonyOS 多设备开发实践:我把 SysCap、断点和折叠屏适配放到了同一套架构里 原创
HarmonyOS 多设备开发实践:SysCap、断点和折叠屏适配
@[TOC](HarmonyOS 多设备开发实践)
一、为什么要做鸿蒙原生
去年下半年,我把之前用 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) => {
const breakpoint = getBreakpoint(size.width);
AppStorage.Set('breakpoint', breakpoint);
});
// 断点映射
function getBreakpoint(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);
}
});
// 本地数据变更时写入云端
function saveProgress(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')) {
// 显示扫码按钮
}
断点监听与全局广播:
const listener = mediaquery.matchMediaSync('(min-width: 600vp)');
listener.on('change', (result: mediaquery.MediaQueryResult) => {
const bp = result.matches ? 'md' : 'sm';
AppStorage.Set('breakpoint', bp);
});
窗口变化驱动断点重算:
display.on('windowSizeChange', (size: window.Size) => {
const bp = 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) 开头的版本,其实不是在适配设备,是在逃避思考。设备是无限的,窗口是有规律的,能力才是真正需要关心的。想明白这三层的关系,后面加什么设备都不慌。
你的项目里,真的需要判断设备类型吗?
参考资料




















状态跨设备同步这块能复用 CloudDB 确实省事,不用自己搭后端。我有个疑问,如果游戏在折叠屏展开态和分屏模式下窗口宽度一样,但用户期望的交互密度不同,你们目前的断点方案能区分这两种场景吗
对的SysCap这个是最棒的,只要有能力就可以实现。其实针对不同设备也就UI显示不同,其他的就判断能力就行。