# 〖共创稿事节〗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 生效时分栏由系统接管。

三处配置与 SCB force split:系统接管分栏,NavigationMode 不用改

另有一次验证路径的误判值得记下来:第一轮验证点「图鉴 → 白泽」,看到详情渲染在左栏、右栏空白,一度以为「EasyGo 只劈了窗口、没把 destination 送到右栏」。实际上 openBeast() 走的是壳层内联详情,根本不进栈——行为完全正确,是验证路径选错了。判据很硬:dumpLayout 里没有 NavDestination 节点,就是栈里没东西。换成会 pushPath 的入口(篇章阅读)后右栏立刻出现封面页。

二、比例标定:比例会跨过阈值,改变壳层形态

wideSplit.ratio 只是个字符串,但它决定的不只是视觉比例。三档实测(平板 2880×1920):

配置NavBar 实测左栏 vp壳层形态
不配(默认 1:1)[0,86][1440,1920]720底部 tab
`"12"`[0,78][960,1920]480底部 tab
`"21"`[0,78][1919,1920]960左侧竖排导航

壳层侧导航的阈值是 840vp:1:1 分栏后左栏 720vp,竖排导航判假,退成底部 tab 挤在半屏里;2:1 左栏 960vp 过阈值,保住平板原本的竖排导航形态。所以「分栏比例」实际是在决定分栏后平板还是不是平板——1:2 会让首页三张统计卡的文字挤到极小,1:1 会把平板降级成手机形态。定稿 2 | 1:左栏保竖排导航,右栏 480vp 与手机竖屏宽度相当,篇章封面居中排布毫无压力。

比例跨越 840vp 阈值:480/720vp 底部 tab,960vp 左侧竖排导航

两个工程坑,都属于「文档一句话、实测才放心」:

  • 改 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,单栏态相安无事;分栏态实测,它们只铺满右栏:

状态NavBarNavigationContent判读
分栏基线[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 满屏层忘了声明,连模拟器都不用开。

满屏层:改前挤在右栏左栏可点,改后铺满全屏;判分栏认 Divider

四、退化场景:模拟器上「不分栏」大多是预期

M6 验收了三类退化场景,结论先说:全部「单栏不破版」,而且大多数「不分栏」不是回归,是官方设计。

场景实测窗口形态关键信号
2in1 自由窗口2090×1394左竖排导航 + 右内容isSupportCompatibleMode: true 但会话置 false
2in1 缩窗912×1394(≈480vp)底部 tab 单栏同上,not in compatible mode
2in1 最大化≈1642vp单栏 + 竖排导航同上
折叠屏展开横屏2120×1320单栏底 tabisSupportCompatibleMode: false
折叠屏方窗980×980单栏底 tabsquareWindowMode 无对应窗口可验
横屏请求(超分查看器)自动转横屏满屏渲染与 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 生命周期:生效、标定、满屏层、退化四步验收全绿,最终因拖动白屏无解而下线

六、如果现在要上平行视界

把四天的结论压缩成一张决策清单:

问题答案
要「左栏壳层 + 右栏路由」且不拖动?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 下线后这两条随方案一并关闭,不再补。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐