把一套手机界面等比例放大到平板或折叠屏上,通常不是“多端适配”的完成,而只是把原来的拥挤换成了大片空白。真正需要处理的是窗口不断变化后,信息密度、阅读顺序、操作位置和用户正在进行的任务能否保持连贯。设备型号可以帮助测试,却不该成为布局决策的主轴;同一设备也可能进入全屏、分屏、自由窗口或旋转后的不同尺寸。
我更倾向于把这件事理解成“为一组窗口状态设计”,而非“为几个设备截图设计”。下面的做法以 ArkUI 声明式 UI 为例,讨论窗口尺寸、折叠状态与状态连续性。文中的阈值、页面结构和数据均为演示,不对应任何项目;具体 API 是否可用,应以目标 SDK 的 API 参考和设备能力为准。
一、先让窗口尺寸成为布局的唯一事实来源
页面最初获得的屏幕宽度,只能反映启动时刻。用户拖动自由窗口、进入分屏、旋转设备或展开折叠屏后,真正决定可用空间的是当前窗口尺寸。因此布局层应监听窗口变化,并把像素值转换为逻辑像素 vp 后再参与断点判断。不要用设备型号、分辨率名单或“是否平板”的布尔值代替窗口宽度。
以下片段展示了一个页面级尺寸观察器。它假设运行在 Stage 模型的 ArkUI 页面中;windowSizeChange 回调具体可用性须与目标 SDK 核对。为了能在销毁时解除监听,回调保存为同一个函数引用。
import { window } from '@kit.ArkUI';
@Entry
@Component
struct ArticleListPage {
@State private widthVp: number = 0
@State private heightVp: number = 0
private appWindow?: window.Window
private readonly sizeListener: (size: window.Size) => void = (size) => {
this.widthVp = px2vp(size.width)
this.heightVp = px2vp(size.height)
}
async onPageShow(): Promise<void> {
this.appWindow = await window.getLastWindow(getContext(this))
const size = await this.appWindow.getWindowProperties()
this.widthVp = px2vp(size.windowRect.width)
this.heightVp = px2vp(size.windowRect.height)
this.appWindow.on('windowSizeChange', this.sizeListener)
}
aboutToDisappear(): void {
this.appWindow?.off('windowSizeChange', this.sizeListener)
}
build() {
if (this.widthVp >= 840) {
Row() {
this.MasterList()
Divider().vertical(true)
this.DetailPane()
}.height('100%')
} else {
this.MasterList()
}
}
@Builder MasterList() {
Text('列表内容由页面的任务状态提供')
}
@Builder DetailPane() {
Text('详情由当前选中条目提供')
}
}
示例中的 840vp 不是平台规定的数字,而是内容能够同时容纳列表、详情和必要操作区的一个演示断点。用 vp 判断的好处是它随显示密度换算,便于按视觉尺寸思考;但文字缩放、语言长度和辅助功能仍可能让“刚好放下”的设计溢出。断点附近要做的不是卡住在两种布局之间,而是让容器和文本有合理的最小、最大与换行策略。
二、断点应该服务于信息层级,而不是制造两套页面
响应式页面常见的第一个误区,是手机一套、平板再复制一套。两份结构很快会在筛选条件、空状态、权限控制和埋点上分叉。更稳妥的方式是保留同一份业务状态,根据窗口宽度改变呈现关系:窄窗口优先展示主任务;宽窗口把上下文同时露出。
以带详情的列表为例,可以先定义内容约束,再选取区间:
| 窗口宽度(演示) | 页面组织 | 设计重点 |
|---|---|---|
| 小于 600vp | 单列列表或单列详情 | 保留清晰返回路径,避免双栏挤压 |
| 600–839vp | 列表为主、详情按需进入 | 筛选可收进菜单,操作保持可触达 |
| 不小于 840vp | 列表与详情分栏 | 两栏宽度有下限,详情不抢走主导航 |
同样的思路适用于网格:不要因宽窗口就无限增加列数。卡片宽度、文本可读行长、图片比例和点击热区共同决定上限。使用弹性布局或网格时,应为卡片设定合理的最小宽度,而不是根据某个设备的“应该有四列”写死分支。测试也要覆盖从 599 到 600、从 839 到 840 这类临界变化,尤其是中文标题、长英文词和大字体设置。
三、折叠状态是特性信号,不是常规布局开关
折叠设备会让人想先判断“展开还是折叠”,再选择整套界面。这往往把硬件姿态当成窗口尺寸的替身:完全展开后的窄分屏窗口,未必适合双栏;折叠状态下的外屏窗口,也未必一定只能显示手机布局。常规列表、导航和信息密度应仍由窗口宽高决定。
折叠状态更适合做少数与姿态直接相关的特性分支。例如支持悬停形态时,可把视频预览留在上半区、把评论和控制区放在下半区;如果窗口 API 能提供折痕或不可用区域,双栏之间可避开这条区域,避免输入框或关键按钮被折痕压住。没有折痕信息时,不要凭设备规格猜测坐标;把布局退回普通的尺寸响应式方案更安全。
在采用这类增强前,需要逐层确认:目标系统版本是否提供折叠状态或可折叠区域能力、设备是否实际支持、模拟器和真机的回调是否一致,以及状态变化时是否会短暂重复触发。能力缺失时应保持可用的默认布局,而不是让页面等待一个永远不会到来的姿态事件。涉及 display.getFoldStatus() 或折叠屏管理器的调用,也应按目标 SDK 的版本说明做能力判断与错误处理;它们不应成为基础页面的启动前置条件。
四、连续性比“切换动画漂亮”更重要
窗口从单栏变为双栏时,用户不希望丢掉刚看完的条目、输入到一半的文字,或正在展开的筛选条件。响应式切换的核心因此不是重新创建一页,而是让持久业务状态独立于布局状态。
可以把状态粗分成三类:
- 布局状态:窗口宽高、当前断点、横竖屏和可折叠区域。这些状态可以频繁变化,通常不需要持久化。
- 任务状态:当前选中的条目、导航栈、筛选条件、搜索关键字和草稿内容。这些状态在单栏与双栏间应保持同一份数据源。
- 视图位置状态:列表锚点、详情阅读位置、焦点控件和键盘可见性。它们不总能完全无感恢复,却应优先保留能让用户继续任务的最小信息。
例如在窄窗口点击某条目,页面进入详情;窗口变宽后,同一个 selectedId 可以让详情在右栏出现,而列表仍停在原来的锚点。若用户正在编辑,草稿应由上层状态持有,输入框只是它的投影;布局重组时不要以新组件的默认空值覆盖草稿。对于滚动位置,优先记录稳定的业务锚点(如首个可见条目的 ID 与偏移)而非仅记录像素坐标,因为重排后相同像素不再代表相同内容。
折叠/展开还可能伴随窗口尺寸快速连续变化。不要在每次回调中同时发网络请求、重建数据源和重置滚动。尺寸监听只负责更新布局所需的最小状态;昂贵操作应由明确的业务事件触发,必要时做合并或节流。这样既减少界面抖动,也避免把硬件姿态变化误判为一次新的业务进入。
五、把多设备检查放到真实任务里
适配验收不宜只看“横屏截图是否好看”。选择一个完整任务,例如搜索、打开详情、编辑一段文字、改变窗口尺寸、再提交或返回,然后逐步检查:
- 变更前后,当前条目、筛选和草稿是否还在?
- 单栏与双栏是否共享同一选择状态,返回语义是否仍清晰?
- 临界断点、长文本、字体放大、分屏和自由窗口是否溢出或遮挡?
- 支持悬停或折痕能力的设备上,关键输入和操作是否避开不可用区域?能力缺失时能否平稳降级?
- 尺寸连续变化期间,列表是否闪回顶部、焦点是否异常丢失、重复请求是否出现?
多设备适配并不要求每一种窗口都长得一样,而是要求用户在窗口变化后仍认得当前任务、找得到下一步、不会丢失已经完成的输入。让窗口尺寸承担普通布局判断,让折叠姿态只承担真正需要它的特性判断,再把任务状态从视图结构中抽出来,页面才有机会在设备形态变化时保持连续。