一、为什么个人项目也要认真做适配
个人开发者最容易把"多设备适配"当成大厂的事——反正用户主要在手机上。但 module.json5 里一行 deviceTypes 就决定了你的应用出现在哪些设备上:写了三个设备却只按手机调布局,等于自愿在平板和电脑上呈现半成品。而且鸿蒙的生态现实是折叠屏、平板、2in1 的用户重合度越来越高,适配欠账迟早要还。
"健康菜谱助手"的 module.json5 里声明了三种设备:
"deviceTypes": ["phone", "tablet", "2in1"]这一行的含义是:应用包要能在手机、平板、2in1(鸿蒙电脑形态)上安装运行。声明的瞬间,适配就从"可选项"变成了"义务"——2in1 上窗口可以自由拉伸,平板上有分屏,手机上有折叠屏展开态。这篇讲我怎么用一个不到 70 行的断点系统接住这三种形态,以及哪些账到现在还没还完。
二、断点系统本体:70 行接住三种形态
适配的核心是BreakpointSystem.ets(common/utils/ 下),基于 ArkUI 的 mediaquery 能力:
staticregister(uiContext:UIContext):void{BreakpointSystem.unregister()constmediaQuery=uiContext.getMediaQuery()BreakpointSystem.smListener=mediaQuery.matchMediaSync('(width<=600vp)')BreakpointSystem.mdListener=mediaQuery.matchMediaSync('(600vp<width<=840vp)')BreakpointSystem.lgListener=mediaQuery.matchMediaSync('(840vp<width)')// ……change 回调里 AppStorage.setOrCreate('currentBreakpoint', bp)}三个设计决定,各有理由:
单点分发。三个 listener 的 change 回调都只做一件事:把断点写进 AppStorage 的currentBreakpoint。所有页面通过@StorageLink('currentBreakpoint')订阅,没有任何页面自己监听媒体查询。好处是断点逻辑只有一处,坏处是所有页面都被一个字符串驱动——这是下文魔法数问题的根源。
先注销再注册。register 开头先调 unregister,逐个 off 掉旧 listener 再建新的。这行防御来自一个真实的担忧:Entry 页面的 aboutToAppear 在热重载和窗口重建时可能被多次触发,listener 泄漏会让 change 回调叠加执行。
初始断点用 if 链兜底。listener 注册完后立刻探测一次当前态(sm.matches → md.matches → 否则 lg),保证首帧就有正确断点,不依赖第一次窗口变化事件才收敛。
三、四处消费点与一个魔法数问题
页面侧的消费方式统一是 @StorageLink 加 if 链。四个真实消费点:
- Index.ets
getContentMaxWidth():lg→1180、md→960、sm→100000(等于不限制) - HomePage.ets
getBannerHeight():lg→260、md→220、sm→Sizes.BANNER_HEIGHT - CategoryPage:网格列数和间距随断点变化
- ReadingPage:朗读页排版宽度
跑起来都正常,但写专栏复盘的时候我盯着这四处 if 链看出了问题:断点阈值 600/840 只存在于 BreakpointSystem 里,而每个消费点的数值(1180、960、260、220)散落在各自的 if 链里。想新增一个 xl 断点,或者把 840 调成 900,要改 N 个文件,漏一处就是某个页面在大屏上比例失调。最初做适配时只想着"让平板不丑",真正回头看才发现:阈值和数值分家,才是维护成本的开端。
解法很朴素——断点决策收敛成一张常量表:
exportclassBreakpointPolicy{staticreadonlyCONTENT_MAX_WIDTH:Record<string,number>={'sm':Number.MAX_SAFE_INTEGER,'md':960,'lg':1180}staticreadonlyBANNER_HEIGHT:Record<string,number>={'sm':180,'md':220,'lg':260}}页面只读表不写分支。这个改造还没落地,是我给自己排的下一步——专栏连载里也公开记了这笔账。
四、两笔还没还的账
诚实说,当前 1.0.5 的适配是"骨架完整、细节欠账"。
账一:2in1 的自由窗口没有约束。module.json5 里没有配置 supportWindow 相关的窗口尺寸限制,2in1 上用户可以把窗口拉到很扁——断点会掉回 sm,内容宽度不限制,banner 压到 180。能用,但不好看。窗口最小尺寸约束是接下来要补的配置。
账二:折叠屏展开态只当大屏处理。当前实现里折叠屏展开就是 lg,没有用 display 的折叠状态 API(foldStatus)做展开态专属布局。菜谱场景其实有明确的机会:展开态是"左边步骤、右边计时器"的天然双栏场景。这是 1.0.6 的候选特性。
两笔账的优先级我按"用户感知频率"排:自由窗口约束影响所有 2in1 用户的每一次使用,先还;折叠屏双栏是增量体验,后还。
五、给同样要适配三种设备的你四条原则
- 声明 deviceTypes 之前先想清楚适配范围——声明了就要负责,少声明一个设备比多声明后适配烂更体面。
- 断点阈值只允许存在一处,消费点全部读表;魔法数散落是适配烂掉的开始。
- mediaquery 手写方案和 GridRow/GridCol 声明式方案二选一即可:页面少、断点行为独特(如 banner 高度)选手写;网格类页面用 GridRow 更省。
- 适配不是一次性的,每个版本迭代把"新页面是否处理了 md/lg"加进发布检查清单——我的 release-notes 里已经有这一项。
适配的尽头不是"每个设备都好看",而是"用户换设备时体验不断":手机上收藏的菜谱,到平板上还在收藏夹里;2in1 上拉窄窗口,断点平滑降级回手机布局。做到这一步,三种设备才真正是一个应用,而不是三个移植版本。
参考资料
- HarmonyOS 应用自适应布局开发指导(断点)
- ArkUI 媒体查询(mediaquery)文档
- 工程源码:BreakpointSystem.ets、Index.ets、HomePage.ets、CategoryPage.ets、ReadingPage.ets
#鸿蒙 #HarmonyOS 7(API 26)