HarmonyOS 7 新特性5:平行视界——轻视界文集的两栏阅读与窗口状态边界矩阵真机实测
HarmonyOS 7 新特性5:平行视界——轻视界文集的两栏阅读与窗口状态边界矩阵真机实测
文章目录
1、引言
我读长文有个改不掉的习惯:左边开着目录,右边开着正文,眼睛在两者之间来回跳。手机上这个习惯被迫戒了——屏幕只容得下一栏,看目录就得离开正文,回来再找读到哪。平板和 PC 形态把屏幕还给了我,但「两栏」这件事谁来管?应用自己写 Row split 是一种管法,系统托管是另一种。HarmonyOS 给的答案叫平行视界(easyGo):一份 JSON 配置声明「哪两个页面左右分栏」,系统负责分栏的挂载、分割线、窗口几何,应用侧写的就是两个普普通通的 @Entry 页面。
这个能力的吸引力在于「声明式」三个字——我不想在每个阅读类应用里重写一遍分栏布局。但声明式的代价是行为黑盒:分栏什么时候生效、什么时候不生效、两栏之间数据怎么通,文档给的是配置格式而不是行为边界。这篇就用一个文集应用把行为边界实测出来:四组真机读数凑一张矩阵,三个坑各给一个跑通。
环境声明:HarmonyOS 7.0.0.107 (API 26) Release,实测工程轻视界(com.qingkouwei.lightview);DevEco Studio 26.0.0(SDK 26.0.0.105 Release),真机 MatePad Edge(QXS-W00,双模态(平板/PC 可切换),3120×2080)。代码在该环境编译签名部署运行,读数全部真机实测。
2、效果展示与工程结构
轻视界是一个极简文集应用:12 篇合成短文分三类栏目(工程笔记/阅读摘录/生活杂记),左栏是分类列表,右栏是正文。选文集做载体是刻意的——阅读类应用的两栏语义最纯粹:列表与正文之间只有「当前选中」一个耦合点,正好用来检验平行视界的跨页通道干不干净。
平板形态下的分栏首屏:左栏列表贴顶排列、当前条目橙色高亮,右栏正文与左栏同屏,状态行报「分栏 · 1418vp」。

点左栏「论中断的成本」,右栏同帧换成对应正文,左栏高亮同步移动——跨页只传了一个文章 id。

分割线往左拖,三档吸附落到 1:2 档:左栏变窄只留列表,右栏正文重排变宽。注意状态行的窗口宽度读数不变(1418vp)——分栏比例改的是两页的分配,不是窗口几何,这个语义后面矩阵要用。

PC 形态(自由多窗浮窗)不分栏,退化为单窗内双栏:列表 260vp 固定宽加内嵌正文,状态行报「单窗 · 1100vp」。同一份页面代码,容器换了布局分支。

工程结构三个页面一个持有器:
entry/src/main/ets/
├── pages/
│ ├── Index.ets // 重定向页(replaceUrl 到 ListPage)
│ ├── ListPage.ets // homePage:分栏纯列表 / 单窗列表+内嵌文
│ └── ArticlePage.ets // relatedPage:正文页 / 单窗 push 页
└── features/view/model/
├── ArticleStore.ets // AppStorage 持有文集与当前 id
└── articles.json // 合成文集 12 篇 3 类
Index 为什么是重定向页而不是直接挂 ListPage?因为平行视界配置里 homePage 指向 ListPage,系统启动分栏时直接加载它;Index 作为 entryName 入口只负责把单窗冷启动也引到同一页。replaceUrl 不留残栈,返回键语义干净。
3、Kit 与 API:一份配置与三个运行时接口
平行视界不走代码 API,走资源配置。module.json5 里挂一个 profile:
"easyGo": "$profile:easy_go"
easy_go.json 的本篇配置(schema 合规版):
{
"tablet": {
"displayModeOptions": {
"wideWindowMode": "routerSplit",
"squareWindowMode": "routerSplit",
"routerSplitOptions": {
"homePage": "pages/ListPage",
"relatedPage": "pages/ArticlePage",
"wideSplit": { "isDraggable": true },
"mode": 1
}
}
}
}
两个 schema 约束是编译期撞出来的,写在这里省读者三轮构建:其一,wideWindowMode 这些二层字段必须包在 displayModeOptions 里,直接平铺会 schema 校验失败;其二,wideSplit 里 isDraggable: true 与 ratio 字段互斥(schema 的 if/then 分支),要可拖拽就不能写比例——分割线比例改由系统三档吸附接管(1:2/1:1/2:1),本篇实测正是这个行为。官方 schema 文件就在 SDK 里(default/hms/toolchains/modulecheck/easy_go.json),拿不准时直接对照比猜快。
运行时接口只有三个用得上的:
| 接口 | 作用 | 本篇用法 |
|---|---|---|
UIContext.isEasySplit() | 当前是否处于分栏态 | 决定列表页走「纯列表」还是「列表+内嵌文」分支 |
window.getLastWindow() + getWindowProperties() | 窗口几何 | 状态行报窗口宽(vp),矩阵读数来源 |
UIContext.px2vp() | 像素转 vp | 窗口宽换算,官方换算口自带形态密度感知 |
isEasySplit() 是整篇的行为开关,也是第一个坑的源头(第 6 节详述)。px2vp 走 UIContext 而不是手除 density,是因为双模态设备两种形态的密度不同(PC 形态 1.9、平板形态 2.2,本卷前篇实测),手除会在形态切换后算错窗口宽——读数错一位,矩阵就废一行。
4、逻辑流
分栏态与单窗态的决策流:
跨页数据流只有一条通道:
两张图里刻意没有「两页互持引用」的边——平行视界两栏是两个独立 @Entry 实例,互相拿不到对方的组件引用,想通数据只能走进程级通道。AppStorage 是官方给的进程级键值通道,setOrCreate 一个 id 数字进去,两页各自读,耦合点收敛到一个键。这条结构约束决定了文集模型的持有方式(第 6 节坑三)。
5、实战:四组读数与一张矩阵
5.1 平板形态:分栏生效与跨页同步
平板形态冷启动即分栏:左栏 1418vp(全屏 2836vp 的一半,1:1 档),状态行「分栏 · 1418vp」,右栏 ArticlePage 报「右栏」。点左栏第六条「论中断的成本」,右栏同帧换文——这里没有做任何「通知右栏刷新」的代码,ArticlePage 的 onPageShow 在 push 时触发,从 AppStorage 读 id 重载,同步是路由语义自带的。
5.2 分割线三档吸附
拖分割线从 1:1 到 1:2:左栏收窄后列表文字不换行不截断(条目本身是短标题,天然耐窄),右栏正文重排每行字数变多。窗口宽读数保持 1418vp 不变——分栏比例是两页之间的分配,窗口几何归系统管,应用读到的窗口宽不受分割线影响。这个语义让「窗口宽」可以作为矩阵的稳定列,不随用户拖拽抖动。
5.3 PC 形态:单窗退化
切到 PC 形态(自由多窗浮窗),分栏不生效,ListPage 走单窗分支:260vp 列表加内嵌正文,状态行「单窗 · 1100vp」。点条目不 push(分栏态才 push),原地刷新内嵌文——同一个 onClick 回调里一个 if 分叉,两态共用全部业务代码。退化不是降级:单窗双栏在 1100vp 宽下阅读体验完整,只是少了「点目录不离开正文」的同屏性。
5.4 边界矩阵
四组读数加上本卷前篇诊断工程的读数,凑成矩阵:
| 窗口状态 | 窗口宽读数 | isEasySplit | 来源 |
|---|---|---|---|
| 平板形态(无自由多窗管理) | 1418vp/栏 | true | 本篇 5.1 |
| PC 形态 FULL_SCREEN | 1418vp/栏 | true | 本卷开源中国篇实测 |
| PC 形态 FLOATING(自由多窗浮窗) | 1100vp | false | 本篇 5.3 |
| PC 形态 MAXIMIZE | 1642vp | false | 本卷诊断工程 |
| FLOATING(曾分栏后退回) | 1418vp/栏 | true(残留) | 本卷开源中国篇实测 |
矩阵读出的结论修正了一句流传的单维表述:官方文档「自由多窗模式下暂不支持平行视界」里的「自由多窗模式」,指窗口处于自由多窗管理态(FLOATING/MAXIMIZE,带标题栏与窗口 dock),不是设备形态——PC 形态全屏脱离管理态后分栏照样生效。几何门槛(宽窗、宽高比)在所有行都满足,所以不生效的行不是几何原因,是管理态本身关了分栏开关。最后一行的残留态是状态机的不对称边:进分栏再退回浮窗,分栏不释放,应用侧只能让位不能纠正——纠正意味着与系统抢窗口状态,抢赢一次输十次。
矩阵的使用方式也留一句:写环境声明时报「窗口状态 × isEasySplit」整行,不报「某设备支持/不支持」单格。单格结论活不过下一次固件更新,矩阵只会随新状态增行。本篇四行里有两行来自本卷其他篇的诊断读数——矩阵天然是跨篇累积的资产,这也是把它放在收官阶段统一整理的原因。
6、避坑
6.1 Scroll 内容短于视口默认居中,列表悬空在半腰
现象:首版装机截图里,12 条列表悬在左栏垂直居中位置,上下各留一大片空白;右栏三行正文同样浮在半空。第一眼以为布局高度算错。
根因:Scroll 组件在内容高度小于视口时,默认对齐方式是居中而非顶对齐。列表页的内容确实短(12 条条目约 400vp,视口 1900vp),于是整体居中。
方案对比:A 给内容 Column 加 .height('100%') 撑满(内容组件被迫声明视口高度,语义别扭);B 给 Scroll 加 .align(Alignment.Top)(一行属性,语义直白)。
最佳实践:选 B。列表与正文两个 Scroll 都加,注释写明「内容短于视口时默认居中」。
验证:重装机截图,列表与正文均贴顶排列,视觉恢复正常。这个坑在内容长的应用里永远不会暴露——文集刻意选短文,才把它撞出来。
6.2 isEasySplit 在页面创建瞬间读永远 false
现象:aboutToAppear 里读 isEasySplit 决定布局分支,分栏态下右栏正常、左栏却渲染了单窗分支的「列表+内嵌文」,两栏内容叠在一起。
根因:分栏挂载发生在页面创建之后的系统侧流程里,aboutToAppear 时刻分栏尚未就绪,读到的永远是 false。
方案对比:A 监听窗口事件推断分栏(间接,事件与分栏状态不严格同步);B 延迟到首帧后读(setTimeout 300ms,直接读真值)。
最佳实践:选 B。300ms 是本篇实测足够的首帧余量;读到 true 后布局分支切换一次,用户感知为加载过程中的正常重排。
验证:延迟读后分栏态左栏只渲染列表,与右栏正文同屏正确。
6.3 平行视界两栏是两个页实例,模块级单例不共享
现象:文集模型最初用模块级 let 单例持有,左栏点条目后右栏读到的还是旧 id——两栏各持一份模型实例,setCurrent 写进左栏那份,右栏那份没动。
根因:routerSplit 的两栏是两个独立 @Entry 页面实例,模块作用域在各实例里各有一份,let 单例的「单」只在单页内成立。
方案对比:A 把模型塞进 AppStorage(官方进程级通道,对象引用可直接存);B 用 emitter 事件同步(多一条通知链,且首屏还要另拉一次初值)。
最佳实践:选 A。文集数组与当前 id 都放 AppStorage,两页 aboutToAppear 各自 get,耦合点收敛到键名。
验证:改后点左栏条目右栏同帧换文(5.1 读数),单窗态原地刷新同样走该通道,两态行为统一。
7、总结,以及这次没测到的部分
平行视界把「一窗两页」从应用代码里拿走,交给一份配置和系统托管。这篇实测下来,它真正省下的不是分栏布局那几十行代码,而是分割线拖拽、窗口几何联动、形态切换重排这些「写起来琐碎、测起来没完」的系统语义——应用侧剩下的只有两个页面、一个 AppStorage 键、和一个 isEasySplit 分支。
边界矩阵是这篇的主要产出:把「支持/不支持」的单维问题换成「哪种窗口状态下是什么读数」的矩阵问题,官方那句自由多窗限制才有了可操作的表述,残留态那行也提醒我们状态机的边不一定对称。三个坑(Scroll 居中、读时机、跨页单例)都有同一个特征:文档不写、编译不报、只有真机截图会说——文集选短文、选纯两栏,就是为了让截图说话说得清楚。在这里插入图片描述
更多推荐


所有评论(0)