👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
  
  🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
  🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
  💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
  
   如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀

前言

同样是把一个应用拆成左右两栏,平行视界和 Navigation 的 Split 模式在表现上非常相似——都能在折叠屏展开态呈现"左列表 + 右详情"的布局效果。但这两种方案的出发点、控制权归属和适用边界其实相差很远。本文用同一个列表详情场景,分别用两种方式实现,对比它们在路由结构、窗口适配和业务条件上的差异,帮助你在选型时有一个更清晰的判断依据。

一、为什么两者看起来很像

视觉上的相似来自于一个共同目标:在宽屏下更充分地利用横向空间。

折叠屏展开态或平板横屏时,屏幕宽度通常超过 800vp,单页全屏显示会让内容显得稀疏,"左侧展示列表、右侧展示详情"几乎是这类设备上最自然的布局形式。Navigation Split 和平行视界都可以实现这个效果,所以容易被认为是同一类东西。

真正的区别在于:谁负责把屏幕分成两半,谁负责决定两半里分别显示什么。

  • Navigation Split:开发者在应用层自己设计分栏,组件的显示模式、宽度比例、路由关系都由应用代码控制。
  • 平行视界:分屏这件事由系统完成,开发者的主要工作是声明"我支持平行视界",以及保证自己的布局在窗口尺寸减半时还能正常工作。

这个控制权的差别,决定了两者适用的场景和需要处理的问题完全不同。

二、Navigation 分栏是什么

Navigation 是 ArkUI 提供的导航容器组件,从 API Version 8 开始支持,从 API Version 11 起推荐使用 Navigation + NavDestination + NavPathStack 的组合方式管理路由。

Navigation 通过 .navigationMode() 属性控制显示模式,共有三种:

模式说明
NavigationMode.Stack单栏堆栈,同时只显示一个页面
NavigationMode.SplitNavBar(主栏)和 NavDestination(内容栏)并排显示
NavigationMode.Auto宽度 ≥ 600vp 时自动切换 Split,< 600vp 时切换 Stack

Split 模式下,Navigation 内部会渲染两个区域:左侧是 NavBar(主栏,始终可见),右侧是 NavDestination(内容区,显示路由栈顶的页面)。这个分栏完全在应用内部完成,不依赖折叠屏,也不需要系统介入。

Auto 模式是实际项目里更常用的选择,它可以让同一套代码在手机竖屏(Stack)和折叠屏展开态/平板横屏(Split)下分别呈现合适的交互形态。

三、平行视界解决什么问题

平行视界(Parallel View)是 HarmonyOS 面向折叠屏展开态设计的系统级能力,其定义来自官方文档中的表述:在折叠屏展开态下,系统将同一个应用的两个页面同时显示在屏幕左右两侧。

几个关键特点值得单独说明:

1. 系统级,不是应用级
分屏的窗口管理由系统完成。应用实际持有的是一个完整窗口,但该窗口的尺寸在平行视界激活后变为整屏宽度的约一半。开发者不需要也无法直接管理"左半屏"和"右半屏"两个独立窗口。

2. 同进程、同应用实例
平行视界不是分屏多任务,两个区域属于同一个应用进程,数据状态可以直接共享,不需要跨进程通信。

3. 折叠屏专属
官方文档明确:平行视界仅适用于折叠屏手机和折叠屏平板,且仅在展开态下生效。折叠时应用自动回退为单页面显示。

4. 需要开发者声明
应用需要在 module.json5 中声明支持平行视界,系统才会在满足条件时提供入口。声明本身不代表平行视界一定会触发,触发逻辑由系统决定。

四、用同一个列表详情案例分别设计

场景描述:一个新闻列表应用,左侧显示文章标题列表,点击后右侧显示文章正文详情。

方案 A:用 Navigation Split 实现

这是一个纯应用层方案,核心在于 Navigation 组件的配置和 NavPathStack 的管理。

页面结构

├── pages/
│   └── Index.ets      // 包含 Navigation 组件,承载主栏(列表)
├── components/
│   ├── ArticleList.ets // 列表组件,放在 Navigation NavBar 区域
│   └── ArticleDetail.ets // 详情组件,作为 NavDestination

Index.ets:Navigation 组件配置

import { NavPathStack } from '@kit.ArkUI';

// 路由注册:将 NavDestination 对应的组件与路由名称绑定
@Builder
function pageMap(name: string, param: ESObject) {
  if (name === 'ArticleDetail') {
    ArticleDetail()
  }
}

@Entry
@Component
struct Index {
  // 创建路由栈,通过 @Provide 向子组件共享
  @Provide('pathStack') pathStack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.pathStack) {
      // NavBar 区域:主栏内容
      ArticleList()
    }
    .navDestination(pageMap)
    // Auto 模式:宽屏自动分栏,窄屏自动单栏
    .navigationMode(NavigationMode.Auto)
    // 主栏宽度,仅 Split 模式下生效
    .navBarWidth('40%')
    .width('100%')
    .height('100%')
  }
}

ArticleList.ets:列表组件,触发路由跳转

@Component
struct ArticleList {
  // 消费上层提供的路由栈
  @Consume('pathStack') pathStack: NavPathStack;

  private articles: string[] = ['文章一', '文章二', '文章三'];

  build() {
    List() {
      ForEach(this.articles, (item: string) => {
        ListItem() {
          Text(item)
            .width('100%')
            .padding(16)
            .onClick(() => {
              // 按名称压栈,触发右侧 NavDestination 显示
              this.pathStack.pushPathByName('ArticleDetail', { title: item });
            })
        }
      })
    }
  }
}

ArticleDetail.ets:详情页,作为 NavDestination

@Component
struct ArticleDetail {
  // 通过 NavDestination 的 onReady 回调获取路由参数
  @State title: string = '';

  build() {
    NavDestination() {
      Column() {
        Text(this.title)
          .fontSize(20)
          .padding(24)
      }
      .width('100%')
      .height('100%')
    }
    .onReady((ctx: NavDestinationContext) => {
      // 获取压栈时传入的参数
      const param = ctx.pathInfo.param as Record<string, string>;
      this.title = param?.title ?? '';
    })
  }
}

这段代码真正需要关注的是三个地方:第一,NavigationMode.Auto 让分栏时机由组件根据窗口宽度自动决定,不需要手动监听折叠状态;第二,路由栈通过 @Provide/@Consume 跨组件传递,列表组件无需持有 Navigation 引用;第三,详情页通过 NavDestinationContext 拿参数,而不是依赖全局状态。

方案 B:用平行视界实现同一场景

平行视界方案的核心工作不在 UI 组件层,而在声明 + 布局响应。

步骤一:module.json5 声明支持平行视界

{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "metadata": [
          {
            "name": "ohos.extension.parallelview",
            "value": "true"
          }
        ]
      }
    ]
  }
}

这一步是告诉系统"这个应用支持平行视界",系统才会在折叠屏展开时向用户提供入口。这里容易有一个理解偏差:加了这个声明,不等于应用一定会进入平行视界,最终触发与否由系统决定。

步骤二:监听窗口尺寸变化,响应分屏状态

平行视界激活后,应用窗口的实际宽度会变为原来的约一半。应用不会收到"平行视界已激活"这样的专项通知,而是通过窗口尺寸变化来感知状态变化。

import { window } from '@kit.ArkUI';

// 在 EntryAbility 的 onWindowStageCreate 中注册监听
onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.getMainWindow((err, windowObj) => {
    if (err.code !== 0) {
      return;
    }
    windowObj.on('windowSizeChange', (size: window.Size) => {
      // size.width 单位为 px,需要转换为 vp 再用于布局判断
      const widthVp = px2vp(size.width);
      // 根据 widthVp 动态调整 AppStorage 或全局状态,通知页面更新布局
      AppStorage.setOrCreate('windowWidthVp', widthVp);
    });
  });
}

步骤三:页面层根据窗口宽度自适应布局

平行视界激活后,每个分屏区域的宽度约为整屏宽度的一半,通常在手机折叠屏展开时对应 sm 或 md 断点(320vp–840vp 区间)。页面层的布局需要保证在这个宽度下依然可用。

@Entry
@Component
struct ArticleListPage {
  @StorageProp('windowWidthVp') windowWidthVp: number = 360;

  build() {
    // 根据窗口宽度选择布局策略
    // 平行视界下宽度约为整屏一半,这里的逻辑与普通响应式布局一致
    Column() {
      List() {
        // 列表内容
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
  }
}

这里真正需要关注的是:页面本身不需要知道自己是否处于平行视界,它只需要响应窗口宽度变化。这与普通响应式布局的开发方式是一致的,平行视界适配的主要工作是保证布局在宽度减半后不出问题,而不是增加专门的"平行视界分支逻辑"。

五、路由结构区别

两种方案在路由层面的差别比表面看起来更根本。

Navigation Split 的路由是应用内路由

分栏里的"左侧"和"右侧"是 Navigation 组件内部的 NavBar 区域和 NavDestination 区域。它们由同一个 NavPathStack 管理,路由跳转(如 pushPathByName)决定右侧显示什么内容。

这意味着:

  • 路由栈的状态完全在应用层可控
  • 返回操作(pop())可以精确控制
  • 在分栏模式下,NavBar 始终可见,不受路由栈影响
  • 路由深度、页面参数传递、拦截逻辑都由开发者自己设计

平行视界的分屏是系统管理的

平行视界下,系统将应用窗口分成两半,每一半展示应用当前的某个页面状态。官方文档建议配合 Navigation + NavPathStack 使用,可以让平行视界与路由系统更自然地协同工作——但路由本身是 Navigation 的职责,不是平行视界直接提供的能力。

换句话说,平行视界不提供路由 API,它只管窗口怎么分;分出来的区域里显示什么,还是由应用的路由逻辑决定。

维度Navigation Split平行视界
路由控制者应用(NavPathStack)应用(路由逻辑不变)
分栏控制者应用(组件属性)系统
路由 APIpushPathByName、pop 等无专属路由 API
分栏时机开发者通过 navigationMode 控制系统在满足条件时触发

六、窗口适配思路区别

Navigation Split 不需要主动处理窗口尺寸

在 Auto 模式下,Navigation 组件本身会根据当前容器宽度自动在 Stack 和 Split 之间切换,默认阈值是 600vp。开发者不需要监听窗口尺寸或折叠状态,只需要保证 NavBar 内容和 NavDestination 内容在不同宽度下都能正常显示。

如果需要调整触发阈值,可以通过 minContentWidth(API Version 12+)属性修改。

平行视界必须主动响应窗口尺寸变化

平行视界激活后,窗口宽度会发生变化,这是应用感知平行视界状态的唯一途径。如果不监听窗口尺寸变化,页面布局可能在分屏后显示异常(例如仍然按全屏宽度渲染,导致内容溢出或排版错乱)。

官方文档明确说明:不应使用 display.getDefaultDisplaySync() 获取全屏宽度作为布局依据,应优先使用窗口宽度。这一点在平行视界场景下尤为重要,因为平行视界下窗口宽度和屏幕宽度不再相等。

七、各自需要考虑的业务条件

在动手选型之前,有几个业务条件值得先确认清楚。

使用 Navigation Split 时需要考虑:

  • NavBar 始终可见这件事是否符合业务:Split 模式下,NavBar 无论路由栈深度如何变化,始终保持可见。如果业务上需要某个状态下完全隐藏左栏,需要使用 .hideNavBar(true) 属性。
  • 右侧内容区的初始状态:路由栈为空时,右侧内容区默认为空白,需要设计一个占位界面,否则用户体验会比较差。
  • 断点设计:如果使用 Auto 模式,要确认 600vp 这个默认阈值是否适合自己的布局,必要时用 minContentWidth 调整。

使用平行视界时需要考虑:

  • 设备范围:如果应用需要支持普通直板手机、平板等非折叠屏设备,平行视界在这些设备上不会生效,布局适配不能依赖平行视界触发。
  • 不能假设平行视界一定会激活:即使声明了支持,用户也可能不开启平行视界,或设备不处于展开态。布局需要在没有平行视界时也能正常工作。
  • 响应式布局是前提:平行视界的本质是窗口尺寸变化,如果应用本身就有良好的响应式布局,适配成本会低很多;如果有大量固定宽度的代码,这些地方在分屏后容易出问题。

八、如何根据官方能力边界选择

官方文档对两者的定位有明确的说明:Navigation Split 是应用内布局方案,平行视界是系统级分屏机制。两者不是互相替代的关系,能力边界不同,适用的场景自然也不同。

按照官方能力边界,可以用以下条件做判断:

优先考虑 Navigation Split 的场景:

  • 应用需要在手机、平板、折叠屏等多种设备上都支持主从布局
  • 需要对分栏时机、宽度比例、路由逻辑有精确控制
  • 应用不依赖特定设备形态,要求较强的跨设备一致性

优先考虑平行视界的场景:

  • 应用的主要用户群体使用折叠屏,希望充分利用折叠屏展开态的大屏空间
  • 应用已有良好的响应式布局基础,希望以较低成本支持折叠屏分屏体验
  • 产品希望向用户提供"同一应用双页面并排"的交互形态,且愿意由系统来管理分屏行为

两者也可以同时使用。在平行视界激活后,每个分屏区域内部仍然可以使用 Navigation 管理路由,官方文档也建议将 Navigation + NavPathStack 作为平行视界场景下的路由管理方案。

开发经验总结

  1. 控制权归属是核心差异:Navigation Split 把分栏控制权交给开发者,平行视界把窗口管理权交给系统。这个差异决定了两者的开发思路完全不同。

  2. 平行视界的适配本质是响应式布局:不要把平行视界的适配理解为"写一套平行视界专用代码",它的适配工作主要是保证布局在窗口尺寸变化时能正常响应。

  3. NavigationMode.Auto 是常见的实用选择:对于需要同时支持多种设备形态的应用,Auto 模式可以用同一套代码处理单栏和分栏场景,不需要开发者手动判断设备类型。

  4. 不要用屏幕宽度替代窗口宽度:这个问题在平行视界场景下特别容易出现,display.getDefaultDisplaySync() 返回的是整屏尺寸,平行视界激活后它依然是整屏的值,用它做布局判断会得到错误的结果。

  5. 两者可以叠加使用:折叠屏展开态下,平行视界管窗口分屏,Navigation 管应用内路由,互不冲突,官方文档也推荐这种组合方式。

如果你正在同时做多设备适配,可以重点检查一下自己的布局代码里是否有依赖屏幕宽度(而非窗口宽度)做判断的地方——这类问题在普通设备上不会暴露,但在平行视界激活后很容易导致布局异常,往往比显式的分栏逻辑更难排查。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

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

更多推荐