平行视界和 Navigation Split 有什么不同?用同一个 Demo 看两种分栏思路【鸿蒙心迹】

👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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.Split | NavBar(主栏)和 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) | 应用(路由逻辑不变) |
| 分栏控制者 | 应用(组件属性) | 系统 |
| 路由 API | pushPathByName、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 作为平行视界场景下的路由管理方案。
开发经验总结
-
控制权归属是核心差异:Navigation Split 把分栏控制权交给开发者,平行视界把窗口管理权交给系统。这个差异决定了两者的开发思路完全不同。
-
平行视界的适配本质是响应式布局:不要把平行视界的适配理解为"写一套平行视界专用代码",它的适配工作主要是保证布局在窗口尺寸变化时能正常响应。
-
NavigationMode.Auto 是常见的实用选择:对于需要同时支持多种设备形态的应用,Auto 模式可以用同一套代码处理单栏和分栏场景,不需要开发者手动判断设备类型。
-
不要用屏幕宽度替代窗口宽度:这个问题在平行视界场景下特别容易出现,
display.getDefaultDisplaySync()返回的是整屏尺寸,平行视界激活后它依然是整屏的值,用它做布局判断会得到错误的结果。 -
两者可以叠加使用:折叠屏展开态下,平行视界管窗口分屏,Navigation 管应用内路由,互不冲突,官方文档也推荐这种组合方式。
如果你正在同时做多设备适配,可以重点检查一下自己的布局代码里是否有依赖屏幕宽度(而非窗口宽度)做判断的地方——这类问题在普通设备上不会暴露,但在平行视界激活后很容易导致布局异常,往往比显式的分栏逻辑更难排查。
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。
更多推荐




所有评论(0)