HarmonyOS 7 新特性5:平行视界------轻视界文集的两栏阅读与窗口状态边界矩阵真机实测
文章目录
- [HarmonyOS 7 新特性5:平行视界------轻视界文集的两栏阅读与窗口状态边界矩阵真机实测](#HarmonyOS 7 新特性5:平行视界——轻视界文集的两栏阅读与窗口状态边界矩阵真机实测)
-
- 1、引言
- 2、效果展示与工程结构
- [3、Kit 与 API:一份配置与三个运行时接口](#3、Kit 与 API:一份配置与三个运行时接口)
- 4、逻辑流
- 5、实战:四组读数与一张矩阵
-
- [5.1 平板形态:分栏生效与跨页同步](#5.1 平板形态:分栏生效与跨页同步)
- [5.2 分割线三档吸附](#5.2 分割线三档吸附)
- [5.3 PC 形态:单窗退化](#5.3 PC 形态:单窗退化)
- [5.4 边界矩阵](#5.4 边界矩阵)
- 6、避坑
-
- [6.1 Scroll 内容短于视口默认居中,列表悬空在半腰](#6.1 Scroll 内容短于视口默认居中,列表悬空在半腰)
- [6.2 isEasySplit 在页面创建瞬间读永远 false](#6.2 isEasySplit 在页面创建瞬间读永远 false)
- [6.3 平行视界两栏是两个页实例,模块级单例不共享](#6.3 平行视界两栏是两个页实例,模块级单例不共享)
- 7、总结,以及这次没测到的部分
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」。同一份页面代码,容器换了布局分支。

工程结构三个页面一个持有器:
text
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:
json5
"easyGo": "$profile:easy_go"
easy_go.json 的本篇配置(schema 合规版):
json
{
"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、逻辑流
分栏态与单窗态的决策流:
#mermaid-svg-AZLUYlh2U1URmZBa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AZLUYlh2U1URmZBa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AZLUYlh2U1URmZBa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AZLUYlh2U1URmZBa .error-icon{fill:#552222;}#mermaid-svg-AZLUYlh2U1URmZBa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AZLUYlh2U1URmZBa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AZLUYlh2U1URmZBa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AZLUYlh2U1URmZBa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AZLUYlh2U1URmZBa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AZLUYlh2U1URmZBa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AZLUYlh2U1URmZBa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AZLUYlh2U1URmZBa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AZLUYlh2U1URmZBa .marker.cross{stroke:#333333;}#mermaid-svg-AZLUYlh2U1URmZBa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AZLUYlh2U1URmZBa p{margin:0;}#mermaid-svg-AZLUYlh2U1URmZBa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AZLUYlh2U1URmZBa .cluster-label text{fill:#333;}#mermaid-svg-AZLUYlh2U1URmZBa .cluster-label span{color:#333;}#mermaid-svg-AZLUYlh2U1URmZBa .cluster-label span p{background-color:transparent;}#mermaid-svg-AZLUYlh2U1URmZBa .label text,#mermaid-svg-AZLUYlh2U1URmZBa span{fill:#333;color:#333;}#mermaid-svg-AZLUYlh2U1URmZBa .node rect,#mermaid-svg-AZLUYlh2U1URmZBa .node circle,#mermaid-svg-AZLUYlh2U1URmZBa .node ellipse,#mermaid-svg-AZLUYlh2U1URmZBa .node polygon,#mermaid-svg-AZLUYlh2U1URmZBa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AZLUYlh2U1URmZBa .rough-node .label text,#mermaid-svg-AZLUYlh2U1URmZBa .node .label text,#mermaid-svg-AZLUYlh2U1URmZBa .image-shape .label,#mermaid-svg-AZLUYlh2U1URmZBa .icon-shape .label{text-anchor:middle;}#mermaid-svg-AZLUYlh2U1URmZBa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AZLUYlh2U1URmZBa .rough-node .label,#mermaid-svg-AZLUYlh2U1URmZBa .node .label,#mermaid-svg-AZLUYlh2U1URmZBa .image-shape .label,#mermaid-svg-AZLUYlh2U1URmZBa .icon-shape .label{text-align:center;}#mermaid-svg-AZLUYlh2U1URmZBa .node.clickable{cursor:pointer;}#mermaid-svg-AZLUYlh2U1URmZBa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AZLUYlh2U1URmZBa .arrowheadPath{fill:#333333;}#mermaid-svg-AZLUYlh2U1URmZBa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AZLUYlh2U1URmZBa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AZLUYlh2U1URmZBa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AZLUYlh2U1URmZBa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AZLUYlh2U1URmZBa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AZLUYlh2U1URmZBa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AZLUYlh2U1URmZBa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AZLUYlh2U1URmZBa .cluster text{fill:#333;}#mermaid-svg-AZLUYlh2U1URmZBa .cluster span{color:#333;}#mermaid-svg-AZLUYlh2U1URmZBa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AZLUYlh2U1URmZBa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AZLUYlh2U1URmZBa rect.text{fill:none;stroke-width:0;}#mermaid-svg-AZLUYlh2U1URmZBa .icon-shape,#mermaid-svg-AZLUYlh2U1URmZBa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AZLUYlh2U1URmZBa .icon-shape p,#mermaid-svg-AZLUYlh2U1URmZBa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AZLUYlh2U1URmZBa .icon-shape .label rect,#mermaid-svg-AZLUYlh2U1URmZBa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AZLUYlh2U1URmZBa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AZLUYlh2U1URmZBa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AZLUYlh2U1URmZBa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} true 分栏态
false 单窗态
页面 aboutToAppear
首帧后读 isEasySplit
ListPage 只渲染列表
ListPage 渲染列表+内嵌正文
点条目: setCurrent + router.push
点条目: setCurrent + 原地刷新内嵌文
ArticlePage onPageShow 读当前 id 重载
内嵌文组件读状态刷新
跨页数据流只有一条通道:
#mermaid-svg-tNjjvn3WVP0D6yp8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tNjjvn3WVP0D6yp8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tNjjvn3WVP0D6yp8 .error-icon{fill:#552222;}#mermaid-svg-tNjjvn3WVP0D6yp8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tNjjvn3WVP0D6yp8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tNjjvn3WVP0D6yp8 .marker.cross{stroke:#333333;}#mermaid-svg-tNjjvn3WVP0D6yp8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tNjjvn3WVP0D6yp8 p{margin:0;}#mermaid-svg-tNjjvn3WVP0D6yp8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tNjjvn3WVP0D6yp8 .cluster-label text{fill:#333;}#mermaid-svg-tNjjvn3WVP0D6yp8 .cluster-label span{color:#333;}#mermaid-svg-tNjjvn3WVP0D6yp8 .cluster-label span p{background-color:transparent;}#mermaid-svg-tNjjvn3WVP0D6yp8 .label text,#mermaid-svg-tNjjvn3WVP0D6yp8 span{fill:#333;color:#333;}#mermaid-svg-tNjjvn3WVP0D6yp8 .node rect,#mermaid-svg-tNjjvn3WVP0D6yp8 .node circle,#mermaid-svg-tNjjvn3WVP0D6yp8 .node ellipse,#mermaid-svg-tNjjvn3WVP0D6yp8 .node polygon,#mermaid-svg-tNjjvn3WVP0D6yp8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tNjjvn3WVP0D6yp8 .rough-node .label text,#mermaid-svg-tNjjvn3WVP0D6yp8 .node .label text,#mermaid-svg-tNjjvn3WVP0D6yp8 .image-shape .label,#mermaid-svg-tNjjvn3WVP0D6yp8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-tNjjvn3WVP0D6yp8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tNjjvn3WVP0D6yp8 .rough-node .label,#mermaid-svg-tNjjvn3WVP0D6yp8 .node .label,#mermaid-svg-tNjjvn3WVP0D6yp8 .image-shape .label,#mermaid-svg-tNjjvn3WVP0D6yp8 .icon-shape .label{text-align:center;}#mermaid-svg-tNjjvn3WVP0D6yp8 .node.clickable{cursor:pointer;}#mermaid-svg-tNjjvn3WVP0D6yp8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tNjjvn3WVP0D6yp8 .arrowheadPath{fill:#333333;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tNjjvn3WVP0D6yp8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tNjjvn3WVP0D6yp8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tNjjvn3WVP0D6yp8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tNjjvn3WVP0D6yp8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tNjjvn3WVP0D6yp8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tNjjvn3WVP0D6yp8 .cluster text{fill:#333;}#mermaid-svg-tNjjvn3WVP0D6yp8 .cluster span{color:#333;}#mermaid-svg-tNjjvn3WVP0D6yp8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tNjjvn3WVP0D6yp8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tNjjvn3WVP0D6yp8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-tNjjvn3WVP0D6yp8 .icon-shape,#mermaid-svg-tNjjvn3WVP0D6yp8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tNjjvn3WVP0D6yp8 .icon-shape p,#mermaid-svg-tNjjvn3WVP0D6yp8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tNjjvn3WVP0D6yp8 .icon-shape .label rect,#mermaid-svg-tNjjvn3WVP0D6yp8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tNjjvn3WVP0D6yp8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tNjjvn3WVP0D6yp8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tNjjvn3WVP0D6yp8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} setCurrent id
push 触发 onPageShow
单窗态原地读
单窗态 router.back
ListPage 点条目
AppStorage
ArticlePage 读 id 重载正文
内嵌正文刷新
ArticlePage 返回键
两张图里刻意没有「两页互持引用」的边------平行视界两栏是两个独立 @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 居中、读时机、跨页单例)都有同一个特征:文档不写、编译不报、只有真机截图会说------文集选短文、选纯两栏,就是为了让截图说话说得清楚。在这里插入图片描述