01 把 TwinShelf 最基础的平行视界链路做稳了。

宽窗口下:

NavigationMode.Auto
→ Split

左侧文章目录持续存在,右侧显示 article_042;整个应用仍然只维护一份 NavPathStack 和一份 selectedArticleId。连续点击同一详情时,重复路由被拦截,滚动位置 612vp 也没有因为分栏出现而重置。

第二篇开始,我遇到一个更像真实阅读产品的问题:

右栏不是终点。

用户在 article_042 详情里继续点“相关推荐”,会进入 related_107,然后又打开 related_205。这时系统负责把页面放到右侧,但“右栏到底有几层、返回应该退谁、左栏高亮是否变化”仍然属于应用自己的 Navigation 状态。

同时 HarmonyOS 7 当前官方月刊已经明确:平行视界在原有大屏分栏基础上新增 1:2 与 2:1 分栏比例,并新增多种路由配置字段,用于覆盖更多大屏业务场景。

所以 02 把两个问题放在一起处理:

分栏比例可以变化;右栏路由也可以继续变深;但左栏主选择与返回语义必须稳定。

本轮统一数据:

taskId:
parallel_20261003_02

window:
1440 × 900vp

routeMode:
NAVIGATION

ratioSequence:
1:2 → 2:1 → 1:2

ratioChanges:
3

ratioProjectSnapshot:
480:960
→ 960:480
→ 480:960

selectedCategory:
技术实践

selectedArticleId:
article_042

relatedRoute1:
related_107

relatedRoute2:
related_205

rightStackDepth:
1 → 2 → 3 → 2 → 1

backEvents:
2

leftSelectionRetained:
true

detailScrollOffset:
612vp

duplicateRouteBlocked:
2

routeRestoreCount:
1

avgRatioSwitchCost:
3.1ms

finalRatio:
1:2

finalRightRoute:
article_042

status:
ROUTE_RATIO_STABLE

一、先说清楚:比例由平行视界负责,路由仍然由 Navigation 负责

HarmonyOS 7 的平行视界可以提供:

1:1
1:2
2:1

等大屏分栏关系。

但 TwinShelf 不把:

1:2

理解成新的路由模式。

无论比例怎么变,当前业务路由仍然是:

ArticleList
→ ArticleDetail(article_042)
→ ArticleDetail(related_107)
→ ArticleDetail(related_205)

比例只是:

左右区域怎么分配空间。

这两个维度必须拆开。

二、1:2 和 2:1 的项目调试数据只是“结果快照”

当前测试窗口:

1440×900vp

为了让调试数据直观,TwinShelf 把系统最终分栏结果记录成近似项目快照:

1:2
约 480 : 960

2:1
约 960 : 480

这些数用于当前 Demo 验收。

真实系统还可能受到:

窗口边距
系统分割条
最小内容宽度
设备形态

影响。

所以文章里不会写成:

1:2 永远严格等于 480 / 960vp。

应用真正要关心的是:

在 1:2 或 2:1 下,自己的内容层级和路由状态是否仍然正确。

三、右栏连续打开相关内容,先做路由去重

本轮右栏:

article_042
→ related_107
→ related_205

如果用户快速点两次同一相关推荐,还是会出现 01 的重复路由问题。

第一段代码继续复用 RouteCoordinator:

export class RouteCoordinator {
  constructor(
    private stack:
      NavPathStack
  ) {}

  openRelated(
    articleId:
      string
  ): boolean {
    const routes =
      this.stack.getPathStack()

    const top =
      routes[
        routes.length - 1
      ]

    const param =
      top?.param as
        Record<string, Object>

    const topId =
      param?.articleId as string

    if (
      top?.name ===
        'ArticleDetail' &&
      topId === articleId
    ) {
      return false
    }

    this.stack.pushPath({
      name:
        'ArticleDetail',
      param: {
        articleId,
        source:
          'related'
      }
    })

    return true
  }
}

本轮故意做了两次重复点击:

duplicateRouteBlocked=2

所以右栏深度真实变化:

1
→ 2
→ 3

没有多出来的幽灵页面。

四、左栏选中项不应该随着 related 路由变化

右栏已经显示:

related_205

但左栏仍然高亮:

article_042

因为左栏表示:

当前主阅读入口。

相关推荐只是:

右栏内部继续浏览。

如果每次 push related 都改:

selectedArticleId

左栏会突然滚去找一个原本不在当前列表位置的文章,阅读上下文反而乱了。

本轮:

leftSelectionRetained=true

就是专门验证这件事。

五、NavigationMode 的变化也不应该清空右栏栈

用户在 2:1 比例下已经打开:

related_205

如果系统窗口临时缩窄进入 Stack,再恢复 Split,业务栈还是:

article_042
related_107
related_205

不应该:

一变成单栏
就 resetPathStack

Navigation 的显示模式变化是表现层变化。

路由栈本身才是业务访问历史。

TwinShelf 只在“业务要求重新开始”时重置栈。

六、返回只退右栏,不重置左栏主选择

第二段代码解决 02 最核心的行为:

handleBack(): boolean {
  const routes =
    this.pathStack
      .getPathStack()

  if (routes.length > 1) {
    this.pathStack.pop()

    this.rightStackDepth =
      routes.length - 1

    this.backEvents++

    return true
  }

  return false
}

第一次 Back:

related_205
→ related_107

第二次 Back:

related_107
→ article_042

最终:

rightStackDepth=1
finalRightRoute=article_042

整个过程中:

selectedArticleId=article_042

一直不变。

七、为什么 rightStackDepth 只是项目概念,不是第二个 NavPathStack

这里最容易误解。

TwinShelf 日志写:

rightStackDepth

只是为了描述:

当前主详情之后
右侧连续浏览了几层内容。

它不代表项目真的创建:

rightNavPathStack

实际只有一个:

NavPathStack

这点必须和 01 保持一致。

否则左右两个独立栈的返回关系会很难收口。

八、比例切换时不要用“重新 push 当前页”刷新右栏

第一版我在 Ratio 改变时做过:

重新 push 当前 route
让右栏重新布局

结果比例每改一次,栈就多一层。

真正正确的做法是:

比例变化
→ 只更新布局状态 / 等待系统布局结果

路由不变

这也是本轮:

ratioChanges=3
routeRestoreCount=1

而不是:

ratioChanges=3
routePushes=3

的原因。

九、第三段代码只记录比例结果,不接管系统平行视界

export type SplitRatio =
  '1:2' | '2:1'

export interface RatioSnapshot {
  ratio: SplitRatio
  leftVp: number
  rightVp: number
}

export class SplitRatioProbe {
  samples:
    RatioSnapshot[] = []

  record(
    ratio:
      SplitRatio,
    leftVp:
      number,
    rightVp:
      number
  ): void {
    this.samples.push({
      ratio,
      leftVp,
      rightVp
    })
  }
}

这段代码只是调试探针。

真正的平行视界 1:2 / 2:1 能力由系统配置与当前平台能力负责。

TwinShelf 只把最终观测值留下来,用于后续回归。

十、为什么 2:1 适合某些文章场景,但不是默认结论

阅读应用里:

左侧目录
右侧正文

一般右侧需要更宽,所以:

1:2

更自然。

但某些场景:

左侧复杂筛选 / 知识图谱
右侧只看摘要

2:1 也可能合理。

HarmonyOS 7 增加比例能力的价值就在这里:

系统提供更多大屏分配方式,业务根据内容层级选最合适的比例。

TwinShelf 当前默认:

1:2

只是项目选择。

十一、比例变化不能改变详情滚动位置

本轮:

detailScrollOffset=612vp

从:

1:2
→ 2:1
→ 1:2

全过程不重置。

否则用户只是把左右空间调了一下,却突然回到文章顶部,会非常明显。

滚动位置继续属于:

ArticleSessionStore

不是 Ratio 状态。

十二、右栏 Back 的判断不能只看“当前是不是 Split”

窄窗 Stack 模式里,同样可能存在:

article_042
→ related_107
→ related_205

所以 Back 行为应该看:

NavPathStack

而不是:

if mode == Split

这一点看起来很小,却决定了平行视界和手机单栏是否真正共享一套导航语义。

TwinShelf 的 RouteCoordinator 不读设备类型,也不读“是不是平板”,只读当前业务栈。

十三、比例变化时列表滚动和分类也必须连续

左栏当前分类:

技术实践

比例从 1:2 切到 2:1 时,左栏变宽,但它不应该:

自动回全部文章;
滚动回顶部;
重新选择第一篇。

当前仍保持:

selectedCategory=技术实践
selectedArticleId=article_042

这样用户把空间调给左栏以后,看到的是同一上下文的更多内容,而不是重新开始浏览。

十四、DevEco 图里把路由栈和比例变化放在同一条日志里

开发图:

HiLog:

taskId=
parallel_20261003_02

window=
1440x900vp

mode=
NAVIGATION

ratio=
1:2

selected=
article_042

push related_107
rightDepth=2

ratio:
1:2 → 2:1

push related_205
rightDepth=3

back
3 → 2

back
2 → 1

finalRoute=
article_042

duplicateBlocked=
2

restoreCount=
1

avgRatioSwitch=
3.1ms

status=
ROUTE_RATIO_STABLE

这条日志比三张静态双栏图更有用。

十五、运行图把“比例”和“路由”分成两块看

最终运行图:

上半部分:

1:2
→ 2:1
→ 1:2

中间:

article_042
→ related_107
→ related_205
→ back
→ back

下方:

leftSelectionRetained=true

duplicateRouteBlocked=2

avgRatioSwitchCost=3.1ms

finalRightRoute=article_042

最终:

ROUTE_RATIO_STABLE

十六、HarmonyOS 7 的 1:2 / 2:1 更适合被理解成“内容策略”

官方 2026 年 6 月开发者月刊明确提到,HarmonyOS 7 平行视界在大屏分栏基础上新增 1:2 和 2:1,并新增多种路由配置字段。

对应用来说,最值得利用的不是“数字多了两个”。

而是可以把:

导航型
阅读型
对比型
筛选型

不同内容结构映射到更合适的分栏关系。

TwinShelf 02 先从阅读场景验证最基础的一组。

十七、02 最后固定八组反向测试

第一组,1:2 初始显示 article_042。

第二组,打开 related_107,右栏深度 2。

第三组,切 2:1,路由栈不变化。

第四组,打开 related_205,右栏深度 3。

第五组,连续点击 related_205,不重复入栈。

第六组,两次 Back 后回到 article_042。

第七组,左栏 selectedArticleId 全程保持 article_042。

第八组,详情滚动位置 612vp 不因比例变化重置。

全部通过以后:

ROUTE_RATIO_STABLE

才成立。

十八、下一篇开始处理“临时离开分栏”

右栏继续浏览以后,真实产品还会遇到:

图片预览要全屏;
视频要横屏;
登录 / 支付页不适合分栏;
过渡页只应该短暂存在。

如果临时全屏前没有保存右栏快照,回来后很容易:

AttachmentPreview
→ ArticleList

把右栏阅读上下文弄丢。

03 会继续同一个 TwinShelf:

Transition Page
Fullscreen
Route Snapshot
Split Restore

把“暂时离开平行视界,再准确回来”做成一条完整工程链。

十九、02 真正稳定的是“空间变化不改导航事实”

到这一篇以后,TwinShelf 已经可以明确区分:

空间层:
1:2 / 2:1

导航层:
article_042 → related_107 → related_205

主选择:
article_042

三者可以同时变化,也可以各自保持。

只要这个分层清楚,后面做临时全屏、横屏和应用内分屏时,就不会因为“画面看起来换了位置”而误操作业务路由栈。

二十、路由快照比“当前页面名”更适合做恢复

临时切换窗口、进入后台、或者后面 03 打开全屏页时,只有:

currentPage=related_205

是不够的。

因为要恢复完整上下文,至少需要知道:

article_042
related_107
related_205

整个访问链。

所以 TwinShelf 定义轻量快照:

export interface RouteSnapshot {
  selectedArticleId:
    string

  routeNames:
    string[]

  routeParams:
    Record<string, Object>[]

  detailScrollOffset:
    number
}

保存时从:

NavPathStack.getPathStack()

提取业务字段。

恢复时再用:

setPathStack()

一次性恢复合法栈。

这比“恢复时连着 push 三次”更不容易出现中间态和动画抖动。

二十一、返回键真正要防的是“右栏已经到底,还继续误退左栏”

当右栏只剩:

article_042

时,TwinShelf 认为已经到当前主阅读入口。

这时如果大屏仍处于分栏:

再 Back

不应该先把左栏 selectedArticleId 清空。

项目会把下一步交给更高层返回策略:

退出当前阅读上下文;
回到上一业务入口;
或者让系统处理。

也就是说,右栏内部 Back 和应用全局 Back 不是同一个层级。

02 先只处理:

3 → 2 → 1

后续 03 全屏恢复时再继续扩展。

二十二、分栏比例变化不能触发图片和正文重新解码

阅读详情里通常有:

大图
富文本
代码块
媒体资源

如果 1:2 → 2:1 时因为父布局宽度变化就重新拉取、重新解码所有资源,比例切换会明显变卡。

TwinShelf 当前只重新做:

布局测量
文本换行
图片区尺寸

不会:

重新 fetch
重新 decode
重新创建文章 ViewModel

所以本轮 avgRatioSwitchCost=3.1ms 只代表项目侧状态与布局提交,并不把网络与大图加载混进来。

这个口径后面做性能回归时会保留。

二十三、长标题与窄右栏是 2:1 模式下必须额外测的边界

2:1 时右侧会明显变窄。

related_205 当前标题较短,没有问题。

但真实文章可能有:

60 字标题
长作者名
多标签
收藏 / 分享 / 更多按钮

TwinShelf 在 2:1 验收时额外要求:

标题最多两行;
关键按钮仍可见;
正文宽度不小于项目下限;
超长元信息允许折叠。

比例适配不只是“系统能把两栏画出来”。

内容密度也要跟着变化。

二十四、比例切换时 Toolbar 与目录也需要自己的响应策略

当前 1:2:

右栏宽
→ 收藏 / 分享 / 更多 全显示

2:1:

右栏窄
→ 保留收藏
→ 分享 / 更多收进菜单

但这种工具栏变化仍然不修改:

NavPathStack
selectedArticleId
detailScrollOffset

它只是密度策略。

这和前面 FlexDesk 系列的经验类似,但这里的主线已经换成平行视界的“系统分栏 + 路由连续”。

二十五、路由去重最好在 Coordinator 层统一,而不是每个卡片自己判断

如果:

推荐卡片 A
推荐卡片 B
目录点击
搜索结果点击

每个入口都自己写一遍:

if currentId == targetId

迟早会有一个入口忘记。

TwinShelf 把所有详情进入统一收口到:

RouteCoordinator.openArticle / openRelated

页面只发:

我要打开哪个 articleId

至于:

是否重复
应该 push 还是 replace
是否需要恢复 selected

都由 Coordinator 决定。

到 03 临时全屏时,这个中心化路由层会更重要。

二十六、后台恢复时先恢复路由,再恢复滚动

如果应用在:

related_205
612vp

位置进入后台,回来以后先设置 scrollOffset,却还没有把正确 ArticleDetail 恢复出来,滚动恢复可能作用到错误页面。

所以恢复顺序固定:

恢复 selectedArticleId

→ setPathStack

→ 等对应 Detail 可用

→ 恢复 detailScrollOffset

这个顺序已经写入 TwinShelf 的恢复协议。

当前本轮没有触发后台恢复,但 routeRestoreCount=1 已经用“比例 + 窗口形态恢复”验证同一套流程。

二十七、ROUTE_RATIO_STABLE 的完整含义

到 02 结束时,这个状态至少代表:

系统分栏比例变化不会制造新路由;

右栏 related 链能连续压栈;

重复详情会被 Coordinator 拦截;

Back 只按业务栈逐级回退;

左栏主选择 article_042 不漂移;

详情滚动 612vp 不重置;

最终仍能回到 1:2 + article_042;

内容没有因为比例变化重建。

这组基线会直接成为 03 临时全屏前的“正确现场”。

只有先知道正常分栏应该是什么样,下一篇才能判断全屏回来以后到底恢复对没有。

参考资料

  • HarmonyOS 7 2026 年 6 月开发者月刊:
    https://developer.huawei.com/consumer/cn/monthly/202606
  • HarmonyOS 7 平行视界官方示例:
    https://developer.huawei.com/consumer/cn/samples/
  • Navigation 分栏开发:
    https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode
  • Navigation / NavPathStack API:
    https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-basic-components-navigation
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐