〖共创稿事节〗HarmonyOS 7 新特性实战(59):平行视界 EasyGo 从生效到下线——forceSplit、比例阈值与满屏层声明
# 〖共创稿事节〗HarmonyOS 7 新特性实战(59):平行视界 EasyGo 从生效到下线——forceSplit、比例阈值与满屏层声明
「山海万灵」在迁移 Navigation 的过程中给平板上过一次平行视界(EasyGo):四个里程碑全部验收通过,门禁 43/43,平板上左栏壳层、右栏路由页的分栏形态稳定运行。三天后,我们把 easy_go.json 从工程里删掉了,换成应用层自绘分栏,门禁新增一条断言:module.json5 从此不许再出现 "easyGo"。
这不是「试了不行就撤」的失败案例。四天里我们搞清了 EasyGo 的生效机制、比例语义、满屏层声明和退化边界,每一层都有 dumpLayout 和 hilog 的实测数字;最后下线的理由同样是实测出来的——它的拖动实现方式在应用层无解。把这套完整生命周期写下来,比只写「怎么开」更有参考价值:开得明白,关得也明白。
一、生效:三处配置,缺一不可
EasyGo 的配置散在三处,少任何一处系统都找不到要分栏的 Navigation:
// 1) entry/src/main/module.json5 —— module 级字段指向 profile
"easyGo": "$profile:easy_go"
// 2) resources/base/profile/easy_go.json —— 设备档位与分栏参数
{
"tablet": {
"displayModeOptions": {
"wideWindowMode": "navigationSplit",
"squareWindowMode": "navigationSplit",
"navigationSplitOptions": {
"homePage": "navBar",
"homeNavigationId": "shanhaiMainNav",
"disablePlaceholder": true
}
}
}
}
// 3) Index.ets —— Navigation 挂 id,与 homeNavigationId 逐字一致
const MAIN_NAV_ID: string = 'shanhaiMainNav';
Navigation(this.pageStack) { /* … */ }.id(MAIN_NAV_ID)
三个容易踩的细节。其一,homePage: "navBar" 是官方写法,意思是把 Navigation 首页当主页(左栏),路由页自然落右栏;homeNavigationId 与 .id() 只要差一个字母系统就静默找不到目标。其二,故意不配 common 档:common 对所有设备类型生效,而手机横屏窗口也可能满足 wideWindowMode 的 600vp 门槛,配了 common 手机就会被劈成两栏——我们只想给平板分栏。其三,disablePlaceholder: true 关掉系统占位页(居中一个应用图标),代价是右栏空态变纯白,这两个都不理想,后面 M5 会再遇到它。
配置生效的判据不在截图,在日志。hilog 里出现系统侧解析记录,随后 force split 由 WMS 接管:
A04200/WMS SCBForceSplitManager --> setForceSplitEnable enable:true needUpdateViewport:false
C03922/AceNavigation --> calc splitMode, isNavBarValid:1, navBarIsHome:1, forceSplitSuccess:1, forceSplitUseNavBar:1
uitest dumpLayout 的布局树给出决定性验证——Navigation 吃满窗口,NavBar 正好一半,分割线居中:
Navigation [0,86][2880,1920]#shanhaiMainNav
NavBar [0,86][1440,1920]
Divider [1440,86][1441,1920]
这里有一个反直觉的实测结论:NavigationMode 不用改。此前 M1/M2 把 Navigation 钉在 NavigationMode.Stack,理由是默认 Auto 在宽窗口会把 navBar 挤成 240vp;本以为开分栏必须改成 Split,实测 .mode(Stack) + EasyGo 配置分栏正常,换成 .mode(Split) 画面与 Stack 完全一致。原因在系统侧:EasyGo 走的是 SCB force split(见上面日志的 SCBForceSplitManager),不是 ArkUI 自己的 NavigationMode.Split。于是钉 Stack 的决定保留——非 EasyGo 的宽窗口继续走单栏,不会被退化成 240vp 侧栏;EasyGo 生效时分栏由系统接管。

另有一次验证路径的误判值得记下来:第一轮验证点「图鉴 → 白泽」,看到详情渲染在左栏、右栏空白,一度以为「EasyGo 只劈了窗口、没把 destination 送到右栏」。实际上 openBeast() 走的是壳层内联详情,根本不进栈——行为完全正确,是验证路径选错了。判据很硬:dumpLayout 里没有 NavDestination 节点,就是栈里没东西。换成会 pushPath 的入口(篇章阅读)后右栏立刻出现封面页。
二、比例标定:比例会跨过阈值,改变壳层形态
wideSplit.ratio 只是个字符串,但它决定的不只是视觉比例。三档实测(平板 2880×1920):
| 配置 | NavBar 实测 | 左栏 vp | 壳层形态 | |
|---|---|---|---|---|
| 不配(默认 1:1) | [0,86][1440,1920] | 720 | 底部 tab | |
| `"1 | 2"` | [0,78][960,1920] | 480 | 底部 tab |
| `"2 | 1"` | [0,78][1919,1920] | 960 | 左侧竖排导航 |
壳层侧导航的阈值是 840vp:1:1 分栏后左栏 720vp,竖排导航判假,退成底部 tab 挤在半屏里;2:1 左栏 960vp 过阈值,保住平板原本的竖排导航形态。所以「分栏比例」实际是在决定分栏后平板还是不是平板——1:2 会让首页三张统计卡的文字挤到极小,1:1 会把平板降级成手机形态。定稿 2 | 1:左栏保竖排导航,右栏 480vp 与手机竖屏宽度相当,篇章封面居中排布毫无压力。

两个工程坑,都属于「文档一句话、实测才放心」:
- 改
easy_go.json必须卸载重装。覆盖安装后配置进了包(intermediates 里确认过)、aa force-stop也做了,NavBar 仍是旧比例——系统侧缓存了兼容配置,覆盖安装不重新解析。卸载重装立刻生效。这条坑在四天后一次全新模拟器实例上独立复现,可以确认不是特例。 ratio与isDraggable互斥(schema 强制,两个一起写校验失败);ratio写法是"2 | 1"(数字 + 空格 + 竖线 + 空格),不是1:1。isDraggable: true实测在 1:2 / 1:1 / 2:1 三档之间拖动吸附,没有中间值——记住这句,第五节要回来收账。
三、满屏层:DIALOG destination 在分栏态只铺右栏
分栏上线后第一个真实回归来自主题弹层和超分查看器。M2 迁移时这两个浮层改成了 NavDestinationMode.DIALOG 的 destination,单栏态相安无事;分栏态实测,它们只铺满右栏:
| 状态 | NavBar | NavigationContent | 判读 |
|---|---|---|---|
| 分栏基线 | [0,86][1919,1920] | 无 | 2:1 成立 |
| 改前开主题弹层 | [0,86][1919,1920] | [1920,78][2880,1920] | 弹层在右栏,左栏仍亮、仍可点 |
| 改后开主题弹层 | [0,86][2880,1920] | [0,86][2880,1920] | 退出分栏,遮罩盖全屏 |
| 改前开超分查看器 | [0,86][1919,1920] | [1920,78][2880,1920] | 整张超分图压进 960px |
| 关闭满屏层后 | [0,86][1919,1920] | 无 | 分栏恢复 |
| 篇章页(未声明) | [0,86][1919,1920] | [1920,78][2880,1920] | 仍分栏,未误伤 |
修法是官方 schema 里的一个字段,声明哪些 destination 要以非分栏模式显示:
"navigationSplitOptions": {
"homePage": "navBar",
"fullScreenPages": ["ThemeModeSheet", "SuperResolution"]
}
名字必须与 pushPath 用的 destination 名逐字一致。fullScreenPages 的官方语义见 Navigation 组件参考:声明后这些 destination 会以非分栏模式显示,覆盖整个窗口而不是落进右栏。这里还撞上一个反直觉的观察:NavBar 铺满全宽 ≠ 分栏坏掉。退出分栏时读到的边界是 NavBar 与 NavDestination 两个都铺满全宽,光看宽度会误判成「分栏还在,只是两层重叠」。判据要加一条:有没有分割线。分栏态下 dumpLayout 里有一条 Divider [1919,78][1920,1920],退出分栏后它消失。判分栏成立的条件是「NavBar 右边界 < 窗口宽」且「存在该处的 Divider」。
门禁侧做了件更有价值的事:原来只校验「fullScreenPages 里写的名字是真 destination」,防写错不防漏写——而 M5 之前的实际状态正是漏写(一个都没声明),门禁照样 PASS。新增的断言从源码推导清单而不是手写清单:扫 Index.ets 里所有 @Builder 块,块内出现 NavDestinationMode.DIALOG 即为满屏层,经 AppPageMap 配对映射回 destination 字面值,断言每个满屏层都出现在每个分栏档的声明里。反向验证过:把声明删掉,门禁直接红出「it would be squeezed into the right pane」。以后再加一个 DIALOG 满屏层忘了声明,连模拟器都不用开。

四、退化场景:模拟器上「不分栏」大多是预期
M6 验收了三类退化场景,结论先说:全部「单栏不破版」,而且大多数「不分栏」不是回归,是官方设计。
| 场景 | 实测窗口 | 形态 | 关键信号 |
|---|---|---|---|
| 2in1 自由窗口 | 2090×1394 | 左竖排导航 + 右内容 | isSupportCompatibleMode: true 但会话置 false |
| 2in1 缩窗 | 912×1394(≈480vp) | 底部 tab 单栏 | 同上,not in compatible mode |
| 2in1 最大化 | ≈1642vp | 单栏 + 竖排导航 | 同上 |
| 折叠屏展开横屏 | 2120×1320 | 单栏底 tab | isSupportCompatibleMode: false |
| 折叠屏方窗 | 980×980 | 单栏底 tab | squareWindowMode 无对应窗口可验 |
| 横屏请求(超分查看器) | 自动转横屏 | 满屏渲染 | 与 EasyGo 两套机制,互不干扰 |
自由多窗是官方明文「暂不支持平行视界」的场景——isSupportCompatibleMode 配置层是 true,场景会话实际置 false,日志给出 not in compatible mode。折叠屏方窗对应 squareWindowMode,需要方形窗口,三台模拟器都没有。横屏请求由页面自己 setPreferredOrientation 触发(横图宽 > 高×1.2),与分栏机制无关。这三个场景在模拟器上只能验「单栏不破版」;要验「分栏在这些形态下是否成立」,必须等真机。
真机补验(手机 HBN-AL80,API 26)拿到了模拟器验不到的那块价值:端侧超分入口在真机存在,进入查看器自动转横屏、显示端侧 4 倍重建后的高清图、无降级文案——这条能力链模拟器上永远走降级分支。
五、下线:拖动白屏在应用层无解,整套配置删掉
四天验收全绿,为什么删?因为用户要「拖动手柄连续调宽度」,而 EasyGo 给不了。
isDraggable: true 的实测语义是三档吸附(1:2 / 1:1 / 2:1,没有中间值)。更大的问题是拖动过程:布局其实在实时跟随(dump 能 dump 到中间值),但绘制层整片不出内容——拖动时两侧白屏。定位实验给了决定性证据:在 navBar 最上层盖一块纯色 Column,拖动时连这块纯色也不上屏。内容都不渲染,就不是应用层能修的 bug;官方口径也一致,isDraggable 的语义就是三档吸附。
于是只剩一条路:不用系统的分栏,自己分。左栏宽度绑一个 @State,自绘手柄拖动就是改这个值,布局引擎重排两栏,内容逐帧跟随、无极、不白屏。替换涉及三处删除与一处门禁反转:
// scripts/verify-split-layout-contract.mjs 第 ① 条:EasyGo 保持停用
// 一旦有人把 `"easyGo": "$profile:easy_go"` 写回去,系统就会接管分栏:
assert.ok(!/"easyGo"\s*:\s*"\$profile:/.test(stripLineComments(moduleJson)),
'module.json5 must NOT enable easyGo — it takes over the drag (3-step snapping, '
+ 'frozen drawing) and fights the self-drawn handle');
assert.ok(!existsSync(easyGoProfile),
'easy_go.json must stay deleted — keeping an unreferenced profile keeps the door open');
module.json5 删字段(留注释说明为什么不能写回)、easy_go.json 删文件(留着一份没人引用的配置等于给「重新打开」留门)、verify-easygo-contract.mjs 整个删除,由 verify-split-layout-contract.mjs 取代。旧门禁守过的东西没有丢:分栏比例范围、满屏层、退化形态,在新门禁里全部有对应断言,其中满屏层那条从「声明 fullScreenPages」换成了「hideNavBar 必须由满屏层可见性与分栏判定共同驱动」——同一个坑,换了一套解法,门禁跟着换。
有意思的是 M4 那句「记住第五节要回来收账」:当时 isDraggable 三档吸附只是个观察,五天后它成了整个方案下线的直接原因。评审时看着对的东西,都要在门禁里留一条会红的断言;而「观察项」里随手记的一句话,常常是下一个重大决定的伏笔。

六、如果现在要上平行视界
把四天的结论压缩成一张决策清单:
| 问题 | 答案 |
|---|---|
| 要「左栏壳层 + 右栏路由」且不拖动? | EasyGo 合适,三处配置 + homePage: "navBar",NavigationMode 保持 Stack |
| 分栏后壳层形态重要吗? | 重要。比例跨过侧导航阈值会改变壳层形态,按左栏 vp 反推 ratio |
| 有 DIALOG 满屏层吗? | 必须声明 fullScreenPages,且门禁应从源码推导清单防漏写 |
| 要无极拖动吗? | 别用 EasyGo。三档吸附 + 拖动白屏是系统实现,应用层改不了,自己分栏 |
| 改配置后验证不生效? | 先卸载重装——系统缓存兼容配置,覆盖安装不重新解析 |
| 判分栏成立? | NavBar 右边界 < 窗口宽 且 存在 Divider,只看宽度会误判 |
EasyGo 本身没有问题——它把「平板分栏」从一周的适配压缩成三处配置,覆盖了不需要拖动的绝大多数场景。我们的需求里「拖动手柄」恰好在它的能力边界之外,而且是在验收全绿之后才被用户明确提出来的。这大概是这条流水线上最贵的一课:能力边界要在写代码之前问清楚,而不是验收之后。
附:本篇证据清单
- 验收记录:M3 生效(
acceptance/2026-10-25-m3-easygo,平板分栏 + forceSplit 日志 + 2in1 未生效排除链);M4 比例标定(2026-10-25-m4-split-ratio,三档 dumpLayout 数据);M5 满屏层(2026-10-25-m5-fullscreen-pages,改前改后边界对照 + 门禁反向验证);M6 退化场景(2026-10-25-m6-degradation,10 张证据含真机端侧超分横屏)。 - 白屏定位与下线决策:
acceptance/2026-10-26-parallel-view-draggable(纯色 Column 决定性实验)、acceptance/2026-10-27-app-level-split(自绘分栏替换全记录)。 - 代码现状:
easy_go.json已删、verify-easygo-contract.mjs已删,verify-split-layout-contract.mjs第 ① 条禁止easyGo写回(当前全量门禁 PASS)。 - 关联稿件:Navigation 迁移的 M1/M2 见系列第 56 篇;自绘分栏的完整实现见系列第 39 篇。
- 尚未覆盖:
squareWindowMode/squareSplit从未标定(无方形窗口设备),折叠屏真机分栏行为未验——EasyGo 下线后这两条随方案一并关闭,不再补。
更多推荐


所有评论(0)