【HarmonyOS 7新能力|019】平行视界入门实战:从能力边界到最小可运行链路

平行视界不是简单地把手机页面横向复制两份,而是让同一个应用在大屏上同时呈现相互关联的主内容与从内容。典型场景是左侧列表、右侧详情:用户选择列表项后,详情区更新;窗口收窄时,两栏又应自然回到单栏导航。
真正的工程难点不在"画两个容器",而在状态连续性。选中项、返回行为、窗口变化、空详情和进程恢复必须保持一致。本文建立"读取窗口信息---选择单双栏模式---同步主从状态---恢复导航位置---验证返回链路"的最小实现。文中的类型与服务是应用侧示例,不冒充 HarmonyOS 7 官方接口;具体能力、适配范围和 API 请以当前 SDK 与华为开发者官方资料为准。
一、先定义平行视界的业务边界
适合双栏的页面通常存在稳定的主从关系,例如会话与消息、商品与详情、目录与正文。两个区域若彼此独立,只是碰巧并排,普通响应式布局可能更合适。
第一版应满足四个条件:大窗显示主从两栏;小窗保持正常单栏;选中项在模式切换后不丢失;系统返回始终符合用户预期。不要把"双栏出现"当成完成,因为业务状态错位比布局未展开更影响使用。
二、用窗口模式而不是设备名称做判断
折叠屏展开、平板分屏和 2in1 窗口缩放都会改变可用空间。布局策略应依赖当前窗口宽度与项目验证过的断点,而不是写死"平板一定双栏"。
ts
type PaneMode = 'single' | 'dual'
interface WindowSnapshot {
widthVp: number
heightVp: number
updatedAt: number
}
function resolvePaneMode(snapshot: WindowSnapshot, dualMinWidth: number): PaneMode {
if (!Number.isFinite(snapshot.widthVp)) return 'single'
return snapshot.widthVp >= dualMinWidth ? 'dual' : 'single'
}
示例中的断点必须通过目标设备、字体缩放和真实内容验证,不能从其他项目直接复制。
三、主从状态需要单一事实来源
列表页和详情页不能各自保存一份选中项,否则窗口切换后很容易出现左侧高亮 A、右侧显示 B。把选择状态放在编排层,由两个区域共同读取。
ts
interface SplitViewState {
mode: PaneMode
selectedId: string | null
detailStatus: 'idle' | 'loading' | 'ready' | 'error'
revision: number
}
function selectItem(state: SplitViewState, id: string): SplitViewState {
return {
...state,
selectedId: id,
detailStatus: 'loading',
revision: state.revision + 1
}
}
页面只提交"选择某项"的意图,不直接修改另一栏组件。这样单栏页面跳转和双栏局部更新可以复用同一状态模型。
四、模式切换是一条状态迁移链路

从单栏进入双栏时,如果已经有选中项,应直接恢复对应详情;如果没有选中项,可以保留空态,而不是擅自选择第一条。双栏退回单栏时,要根据当前焦点决定展示列表还是详情,并保留用户可理解的返回路径。
ts
interface NavigationSnapshot {
route: 'list' | 'detail'
selectedId: string | null
}
function collapseToSingle(state: SplitViewState): NavigationSnapshot {
return state.selectedId
? { route: 'detail', selectedId: state.selectedId }
: { route: 'list', selectedId: null }
}
窗口变化可能连续触发,编排层应对等价模式去重。宽度仍处于双栏范围时,不要反复重建详情页。
五、详情加载要防止旧结果覆盖
用户快速选择多条记录时,较早请求可能最后返回。无论数据来自本地还是网络,都应以修订号确认结果仍属于当前选择。
ts
class DetailCoordinator {
private revision: number = 0
async load(id: string, fetcher: (id: string) => Promise<string>): Promise<string | null> {
const current = ++this.revision
const result = await fetcher(id)
return current === this.revision ? result : null
}
cancel(): void {
this.revision += 1
}
}
这段代码只展示结果去重思路。真实项目还需要在生命周期结束时取消任务、释放监听,并把错误映射为明确页面状态。
六、分层架构隔离平台变化

页面层负责列表、详情与设置的显示;状态编排层维护选中项、导航快照和窗口模式;布局策略层选择单栏或双栏;平台适配层读取真实窗口信息与生命周期事件。业务层不直接依赖具体窗口对象。
ts
interface WindowModeAdapter {
current(): Promise<WindowSnapshot>
subscribe(listener: (snapshot: WindowSnapshot) => void): () => void
}
interface SplitViewPolicy {
resolve(snapshot: WindowSnapshot): PaneMode
}
适配层返回取消订阅函数,页面退出时必须调用,避免重复监听导致模式判断多次执行。
七、返回行为要按用户焦点设计
双栏状态下,详情区出现不一定意味着新增了一层全屏路由。返回键若直接退出应用,用户会感觉内容被突然关闭;若永远只清空详情,又可能破坏已有导航习惯。
建议记录"进入详情的来源"和当前窗口模式:单栏详情返回列表;双栏详情优先保持主列表,并按照产品规则清除焦点或回到上一个选中项;根页面再次返回才交给系统。每个分支都要用真实按键和手势返回验证。
ts
function handleBack(state: SplitViewState): 'clear-detail' | 'show-list' | 'system' {
if (state.mode === 'dual' && state.selectedId) return 'clear-detail'
if (state.mode === 'single' && state.selectedId) return 'show-list'
return 'system'
}
这只是可测试的策略示例,最终规则应与项目的导航栈保持一致。
八、空态、错误态和加载态都要双栏化
右栏没有选中内容时,应显示轻量空态并说明操作方式;加载时保持左栏可用;详情失败时提供局部重试,而不是让整个大屏页面变成错误页。数据被删除或权限变化时,清理过期选中项并回到安全状态。
字体放大后要检查列表标题、详情工具栏和空态文案是否溢出。双栏宽度不足时,应优先切回单栏,而不是把两个区域压缩到无法操作。
九、模式恢复不能只保存宽度
进程恢复时,窗口尺寸可能已经改变。持久化业务选择可以,但不要把上次的 dual 模式当成永久配置。启动后重新读取当前窗口,再把可恢复的选中项映射到新模式。
ts
interface RestorableSplitState {
selectedId: string | null
lastRoute: 'list' | 'detail'
}
function restore(
saved: RestorableSplitState,
snapshot: WindowSnapshot,
dualMinWidth: number
): SplitViewState {
return {
mode: resolvePaneMode(snapshot, dualMinWidth),
selectedId: saved.selectedId,
detailStatus: saved.selectedId ? 'loading' : 'idle',
revision: 0
}
}
十、最小测试矩阵
- 手机竖屏从列表进入详情并正常返回。
- 大窗双栏选择不同记录,左右状态始终一致。
- 双栏缩小为单栏后仍显示当前详情,返回可到列表。
- 单栏放大为双栏后恢复选中项,不重复加载旧数据。
- 无选中项、详情删除、加载失败都有明确回退。
- 连续拖动窗口时不反复创建页面或闪烁。
- 字体放大、长标题、深浅色模式下内容仍可读。
- 页面退出后窗口监听与异步任务都被释放。
十一、常见误区
不要在每次尺寸变化时清空导航栈;不要把窗口宽度写进业务数据;不要在没有选择时强制加载第一条;不要让左右组件各自维护选中状态;也不要用截图看起来是双栏就代替真实返回、旋转、缩放和恢复测试。
十二、结语
平行视界的核心是"同一业务状态在不同窗口模式下连续呈现"。先统一选中项和导航快照,再把单双栏判断放入策略层,通过平台适配层读取窗口信息,最后验证返回、异常与恢复链路,才能让大屏空间真正提升效率,而不是简单拉宽页面。
参考资料: