这次不写“平行视界怎么打开”,而是把问题往真实阅读类 App 里再推进一步:左边文章列表固定,右边详情可以继续点相关推荐;当右栏已经压入第二层详情以后,用户执行全局返回,应该只退右栏,不应该把左边选中的文章一起重置。

我给这次 Demo 起名叫 ParallelReaderLab,页面叫 Article Desk。

为了让正文、日志和图片完全对得上,这次固定一组运行状态:

  • Mode:NAVIGATION
  • Split Ratio:1:2
  • Selected:article_20261001_12
  • Right Stack:2 → 1
  • Related:related_20261001_03
  • Last Back:2 → 1

HarmonyOS 7(API 26)的平行视界当前提供购物模式和导航模式两类典型交互。导航模式的特点很适合阅读、邮件、IM 这类场景:左侧导航或列表保持稳定,右侧内容区继续切换;官方说明里也特别强调了全局返回时右侧页面按层级回退,以及 1:1、1:2、2:1 等分栏比例。

我这篇真正关心的不是“页面能不能分成左右两块”,而是三个状态能不能拆开:

左栏选中态、右栏路由栈、系统平行视界布局状态。

一、先把“左边选中了谁”从右栏页面里拿出来

最开始的实现很普通。

点击列表项以后,直接 push 一个详情页:

private openArticle(articleId: string): void {
  this.navPathStack.pushPathByName('ArticleDetail', {
    id: articleId
  })
}

手机单栏时看不出问题。

但进入平行视界以后,左边列表和右边详情同时存在。此时 ArticleDetail 如果自己维护“当前文章 ID”,左边 ListItem 又维护一份 selected 状态,两边很快就会漂移。

例如左边已经点中 article_20261001_12,右边详情继续点相关推荐 related_20261001_03。此时右栏路由已经两层,但左栏仍然应该高亮原始入口文章,而不是突然跟着右栏相关推荐切换。

所以第一步,我把左栏入口状态单独放进一个轻量状态模型。

export class ArticleRouteState {
  selectedArticleId: string = 'article_20261001_12';
  rightDepth: number = 1;

  selectFromLeft(articleId: string): void {
    this.selectedArticleId = articleId;
  }

  updateDepth(depth: number): void {
    this.rightDepth = depth;
  }
}

这里的关键不是这个类有多复杂,而是它明确了一件事:

selectedArticleId 描述的是左栏当前入口,不等于右栏栈顶页面 ID。

这两个值在第一层详情时可能相同,进入相关推荐以后就不一定相同。

如果一开始把两个概念混成一个变量,后面处理返回时一定会出问题。

二、Navigation 只管理右栏层级,不替左栏决定高亮项

平行视界官方示例本身就是基于 Navigation 路由来承载应用页面关系。

我这次也沿用这个方向,但给右侧单独维护 NavPathStack。

这段代码解决的问题,是左栏点击文章以后更新入口状态,同时把右侧详情放进路由栈。

@Entry
@Component
struct ArticleDesk {
  private navPathStack: NavPathStack = new NavPathStack();

  @State private selectedArticleId: string =
    'article_20261001_12';

  private openFromLeft(articleId: string): void {
    this.selectedArticleId = articleId;

    this.navPathStack.clear();
    this.navPathStack.pushPathByName(
      'ArticleDetail',
      { id: articleId }
    );

    console.info(
      `Selected: ${articleId}, depth=1`
    );
  }

  build() {
    Row() {
      ArticleList({
        selectedId: this.selectedArticleId,
        onSelect: (id: string) => {
          this.openFromLeft(id);
        }
      })

      Navigation(this.navPathStack) {
        ArticleEmpty()
      }
    }
  }
}

我在“左栏重新选择文章”时会清掉右栏原来的相关推荐层级。

这是一个产品判断。

如果用户已经在文章 A 的相关推荐第三层,此时直接从左边点文章 B,我希望右边重新从 B 的第一层详情开始,而不是让 B 接在 A 的旧路由链后面。

正式项目当然也可以保留历史,但必须把行为定义清楚。

三、右侧继续点相关推荐时,不改左侧 selected

接下来就是这次最核心的边界。

article_20261001_12 的详情页里又点击了 related_20261001_03。

这段代码只做右侧 push:

private openRelated(relatedId: string): void {
  this.navPathStack.pushPathByName(
    'RelatedDetail',
    { id: relatedId }
  );

  console.info(
    `Route push: ${relatedId}, ` +
    `depth=${this.navPathStack.size()}`
  );
}

此时运行状态变成:

Selected: article_20261001_12
Right top: related_20261001_03
Right depth: 2

注意,我没有写:

this.selectedArticleId = relatedId

因为相关推荐是右栏内部的继续阅读,不是用户从左栏重新选择入口。

这个区别放到小屏手机里没那么明显,但进入平行视界以后,左边一直在用户眼前,高亮错一次就会非常突兀。

四、全局返回真正应该回的是右栏层级

HarmonyOS 7 平行视界导航模式的一个重要体验,就是全局返回与右侧页面层级保持一致。

在我的业务状态里,这意味着:

depth = 2 时返回,右栏从相关推荐回到入口文章;左栏 selected 不变。

我给页面内部的路由收口写成一个方法,方便调试和测试。

private backRightPane(): boolean {
  const before = this.navPathStack.size();

  if (before <= 1) {
    return false;
  }

  this.navPathStack.pop();

  const after = this.navPathStack.size();

  console.info(
    `GlobalBack: depth ${before} -> ${after}`
  );
  console.info(
    `Selected kept: ${this.selectedArticleId}`
  );

  return true;
}

这段代码不是用来“替代系统平行视界返回机制”。

系统侧是否触发分栏回退、页面如何呈现,仍然由当前 EasyGo / 平行视界配置和 Navigation 路由共同决定。

我这里做的是业务收口:右栏路由变化以后,明确保证左栏入口状态不被顺手重置。

最终这次日志固定为:

EasyGo mode: NAVIGATION
Split ratio: 1:2
Selected: article_20261001_12
Route push: related_20261001_03 depth=2
GlobalBack: depth 2 -> 1
Selected kept: article_20261001_12

DevEco 图里我特意让四块信息同时出现。

左边是 ParallelReaderLab 工程结构,中间是 ArticleDesk.ets,右边模拟器已经处于平行视界阅读状态,底部 HiLog 则直接验证 depth 2 → 1 以后 selected 仍然没变。

五、为什么我没有把平行视界配置 JSON 大段贴出来

这次我刻意没有把文章写成 EasyGo 配置字段说明书。

原因是 HarmonyOS 7 官方当前已经提供专门的平行视界示例,示例以 Navigation 路由的购物类应用为载体,覆盖主页与关联页、路由模式、过渡页面、全屏请求、横屏请求、虚拟容器以及应用内分屏等典型场景。

这些配置应该直接以当前 SDK 和官方样例为准。

本文更想处理的是配置完成以后,应用内部仍然需要自己维护的业务状态。

很多适配问题并不是 EasyGo 没生效,而是:

  • 左栏 selected 跟右栏栈顶绑定;
  • 每次路由变化都刷新整份列表;
  • 返回时把详情和入口状态一起清空;
  • 大屏有双栏,小屏又维护另一套完全不同的数据模型。

真正难维护的是这些。

六、1:2 分栏只是显示策略,不应该写进业务逻辑

这次 Demo 固定使用 1:2。

左边是文章目录,右边是长文阅读,右侧需要更大的内容宽度,所以这个比例比较自然。

HarmonyOS 7 平行视界当前也支持 1:1、1:2、2:1 等比例以及拖拽调整。

但我不会在业务层写:

if (ratio === '1:2') {
  // selected 逻辑
}

比例只应该影响布局。

文章是谁、右栏深度多少、返回以后哪个 ID 继续高亮,不应该因为用户拖了一下分割线就重新计算。

这也是我现在判断多设备适配质量的一个标准:

视觉状态变化可以很多,业务状态变化要尽量少。

七、最终运行图验证的不是“左右两栏”,而是返回以后状态没乱

最终手机 / 折叠屏运行图固定在一次全局返回之后。

此时调试卡片里显示:

Mode: NAVIGATION
Ratio: 1:2
Right depth: 1
Selected: article_20261001_12
Last back: 2 -> 1

右边已经从 related_20261001_03 回到 article_20261001_12 的正文。

左边高亮仍然是同一篇文章。

如果只看“页面是不是双栏”,这个 Demo 十几分钟就能搭出来。

真正跑通以后,我反而更在意这几次操作:

  1. 左栏连续点三篇文章;
  2. 右栏连续进入两级相关推荐;
  3. 全局返回两次;
  4. 调整分栏比例;
  5. 再从左栏选择新文章。

每一步都检查 selected、right depth 和当前页面 ID。

只要三者能一直对上,平行视界才不是“把两张页面摆在一起”。

八、这次真正留下的是两套互不抢状态的导航

写到最后,我把整个页面理解成两套层级。

第一套是左侧入口选择:

article_20261001_12

它代表用户从目录里选中了什么。

第二套是右侧内容路由:

ArticleDetail
  └── RelatedDetail

它代表用户在当前入口里读到了哪一层。

EasyGo / 平行视界负责把这种关系变成适合大屏的系统级分栏体验,Navigation 负责页面路由,而应用自己要保证两套状态不要互相覆盖。

以后如果继续扩展,我会再补两个测试:

  • 全局返回到 depth=1 后再返回,验证是否退出当前详情或回到上一级业务入口;
  • 从平行视界切回单栏以后,验证同一份 selected 和 right path 能否继续使用。

这两项比再调一个漂亮分栏比例更值得做。

参考资料

  • HarmonyOS 7(API 26)平行视界能力解读
    https://developer.huawei.com/consumer/cn/forum/topic/0201221235973021541
  • HarmonyOS 官方示例代码:平行视界(Navigation 路由)
    https://developer.huawei.com/consumer/cn/samples/
  • 多设备通用适配指南
    https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/
Logo

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

更多推荐