HarmonyOS 多设备开发实践:我把 SysCap、断点和折叠屏适配放到了同一套架构里 原创

小时代的大玩家
发布于 2026-7-29 16:30
浏览
0收藏

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 能力,一查便知。

这套逻辑想通之后,我删掉了项目里所有 isPhoneisFoldableisTablet 之类的判断。代码量没少多少,但心理负担少了一大半——以后出什么新设备,都不需要改我这层代码了。


三、第二个坑:窗口才是不变的锚点

如果说 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) 开头的版本,其实不是在适配设备,是在逃避思考。设备是无限的,窗口是有规律的,能力才是真正需要关心的。想明白这三层的关系,后面加什么设备都不慌。

你的项目里,真的需要判断设备类型吗?


参考资料

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
2条回复
按时间正序
/
按时间倒序
Datcon
Datcon

状态跨设备同步这块能复用 CloudDB 确实省事,不用自己搭后端。我有个疑问,如果游戏在折叠屏展开态和分屏模式下窗口宽度一样,但用户期望的交互密度不同,你们目前的断点方案能区分这两种场景吗

回复
2026-7-29 17:43:26
小草飞上天
小草飞上天

对的SysCap这个是最棒的,只要有能力就可以实现。其实针对不同设备也就UI显示不同,其他的就判断能力就行。


回复
2026-7-29 21:42:26
回复
    相关推荐