【HarmonyOS 7 平行视界深度实战】01 平行视界、普通分栏与响应式布局怎么选
文章目录
前言
手机上的列表详情页通常采用整页跳转,用户从列表进入详情后,列表会暂时离开屏幕。页面进入平板宽窗口以后,可用空间明显增加,所以整页覆盖开始浪费已经拥有的浏览上下文。开发者由此会想到双栏,不过双栏背后有三条改造路径:调整当前页面内部布局、使用 Navigation Split 组织 NavBar 与内容区,或者接入平行视界,让系统根据路由关系同时展示两个页面。
列表与详情在大屏设计中常被称为 Master-Detail 或 List-Detail。一侧负责查找、筛选和选择,另一侧承载当前条目的详细内容,邮箱、设置和文档目录都能看到这种关系。这个背景能解释宽屏为什么常保留两部分内容,也把选型问题缩小到页面关系:左右内容属于同一个路由页面时,应用需要调整页面内部布局;它们原本就是两个能够独立跳转的页面时,还要继续判断由应用还是系统组织两侧区域。
屏幕出现左右两块,只能说明画面已经分开,无法说明采用了哪条技术路径,工程评审都需要继续追问:两侧是否拥有独立路由,应用是否准备长期维护双栏宽度、选中状态和窄窗口切换。而答案会直接影响改动范围,也决定问题应从 ArkUI 布局、Navigation 组件还是 EasyGo 配置中排查。
平行视界从 API 23 开始支持开发者配置。HarmonyOS 7 / API 26 又在 EasyGo 中增加了购物模式、比例、过渡页、分隔线颜色和分屏场景等配置。我们的第一个 Demo 只保留 Navigation 的最小链路,页面从首页进入一个 NavDestination,不提前加入模式和比例实验。

一、三种能力分别改动哪里
要判断双栏由谁组织,需要先看改动发生在哪一层。响应式布局处理组件尺寸和排列,窗口变宽后,卡片可以增加列数,正文可以设置最大宽度,工具栏也可以调整间距。整个过程仍发生在同一个路由页面里,开发者负责全部布局变化。
Navigation Split 属于 Navigation 组件自身的显示模式。NavBar 位于一侧,NavDestination 显示在内容区,路由操作主要替换内容区。应用需要明确设计导航栏、分栏宽度、占位页和窄窗口行为。
平行视界由系统兼容方案组织页面。应用保留已有 Navigation 或 Router 路由,再通过 easy_go.json 声明宽窗口模式、主页和关联页。进入分栏时,一个窗口可以同时显示两个页面。导航模式默认让主页保持在左侧,路由跳转发生在右侧;购物模式按照路由栈的栈顶和次栈顶组织两栏。
把三种方案放进现有项目时,可以用左右区域由谁拥有做快速识别。页面代码中出现 Row、GridRow 或断点分支,通常属于应用自己的响应式或自适应布局;NavigationMode.Split 由 Navigation 组件组织 NavBar 与内容区;页面仍按原有路由跳转,同时由 EasyGo 声明主页和关联页,则进入系统平行视界的配置范围。排查时先确认这一层,能够避免反复修改无关参数。
| 方案 | 主要调整对象 | 页面数量 | 左右区域由谁组织 | 适合的现状 |
|---|---|---|---|---|
| 响应式布局 | 同一页面里的组件、断点和尺寸 | 1 | 应用布局代码 | 页面结构已经适合宽屏,只需重新排版 |
| Navigation Split | NavBar、NavDestination、NavigationMode | 1 个导航首页 + 子页面 | Navigation 组件 | 应用准备长期维护明确的双栏信息架构 |
| 平行视界 | EasyGo、主页、关联页和路由栈 | 2 个路由页面 | 系统兼容方案 | 现有单页路由稳定,希望较小改动获得大屏分栏 |
这张表把选择条件落到了调整对象上:同一页面内部重排时检查 ArkUI 布局,稳定 NavBar 由应用长期维护时检查 Navigation Split,两个现有路由页面需要系统协同显示时再检查 EasyGo。这里有一个名称容易造成误判。将 EasyGo 的 wideWindowMode 设为 navigationSplit,表示平行视界使用 Navigation 路由;NavigationMode.Split 控制 Navigation 组件自己的 NavBar 分栏。两个名称都包含 Split,所属配置和页面组织方式各自独立。
系统可以把两个路由页面放进左右区域,每个页面获得的容器宽度也会随之变小。详情页若固定了图片宽度、表格列数或底部操作区,分栏出现以后仍可能溢出。所以项目需要分别验收页面由谁并排,以及每个页面怎样适应自己的容器。责任边界确定以后,技术选型才能回到现有工程的改造范围。

二、项目可以按改造范围选择
前面的边界说明了三种能力各自修改什么,项目选择还要继续看现有页面愿意承担多少改造。
邮件、设置、文档目录这类页面已经有稳定的左侧分类结构时,Navigation Split 更容易承载长期交互。开发者可以控制导航栏宽度、默认占位页和选中状态,也能直接处理窄窗口下的单栏切换。这个选择会带来较多布局代码,同时也提供更强的页面控制能力。
资讯流、商品详情和普通内容应用经常积累了多年的单页路由。业务页面可以独立运行,宽屏上只是缺少同时浏览两层内容的方式。平行视界适合从这里接入,改动集中在 EasyGo 和页面关系,已有页面继续通过 Navigation 跳转。
选择系统平行视界前,还要检查页面是否真的能独立存在。某些旧项目把列表选中项、详情数据和返回按钮都写在父页面里,详情页离开父容器便无法初始化。此时接入 EasyGo 的配置量很小,状态拆分工作却可能很大。更稳妥的判断方式是先在普通单栏中直接打开详情页,确认它能够依靠明确参数加载内容,并能在参数缺失时显示可理解的空态或错误态。
响应式布局仍然要参与宽屏适配。平行视界把窗口分成两个页面区域以后,每一栏的可用宽度会缩小。页面内部如果依赖完整窗口宽度,内容可能被截断。EasyGo 提供 enableReducedContainerSize,ArkUI 也有自适应布局能力,项目仍需检查卡片、图片和横向控件。
可以把选择顺序压缩成几个工程问题:
| 项目现状 | 建议起点 | 原因 |
|---|---|---|
| 单页内容在宽窗口里过于稀疏 | 响应式布局 | 问题发生在一个页面内部 |
| 产品已经确定左侧目录与右侧内容长期并存 | Navigation Split | 应用需要控制稳定双栏结构 |
| 已有单页路由成熟,准备低成本增加宽屏双页 | 平行视界 | 系统可以根据页面关系组织两页 |
| 已经手写双栏并完成多设备回归 | 保留现有分栏 | 迁移收益需要覆盖重构和回归成本 |
| 页面要连续比较商品或内容 | 平行视界购物模式 | 栈顶与次栈顶适合连续浏览 |
表里的建议起点来自页面关系和维护责任,应用类型只能帮助寻找候选场景。平行视界常见于办公、邮箱、即时通讯和电商等频繁切换页面的应用,真实项目仍要检查小窗口回退、横竖屏、全屏页和设备支持范围。场景筛选完成以后,还需要用一条低风险链路验证当前项目是否具备接入条件。
现有项目可以先挑一个低风险列表详情链路做验证。这个链路最好具备三个条件:页面层级短、参数简单、没有支付或编辑中的临时数据。它能把变量缩小到页面关系、路由和 EasyGo 配置。已经拥有成熟手写双栏的项目还应计算迁移收益,配置简化需要覆盖重新回归的成本。选出验证链路以后,下一步就是建立一份能反复复制的最小工程基线。
三、用最小 Navigation 保留可复制基线
低风险链路确定以后,基线页面只创建一个 NavPathStack,并把它交给 Navigation。这个基线刻意保留单文件页面和两项 EasyGo 关系,让普通窗口只验证 Navigation 跳转,让满足系统条件的宽窗口再验证两个路由页面怎样排列。加入业务网络请求、多个 Navigation 或动态路由表会扩大排查范围,所以当前先用下面的 Builder 固定唯一目标页。
@Provide('pageStack')
pageStack: NavPathStack = new NavPathStack()
@Builder
pageMap(name: string) {
if (name === 'PreviewPage') {
PreviewPage()
}
}
这段映射只证明 PreviewPage 能由同一条 Navigation 路由创建,还没有创建任何左右容器。模拟器后续若出现系统分栏,就能排除 Builder 手写布局带来的干扰。跳转参数继续使用显式类,避免 ArkTS 把未声明类型的对象字面量判为不合规:
let param: RouteParam = new RouteParam()
param.title = 'PreviewPage'
this.pageStack.pushPathByName('PreviewPage', param)
NavigationMode.Stack 固定了应用自身的单栏导航。满足 EasyGo 条件时,左右区域由系统平行视界组织,这样不会把 Navigation 自有 Split 的结果混进实验:
Navigation(this.pageStack) {
// 首页内容
}
.mode(NavigationMode.Stack)
.navDestination(this.pageMap)
到这里,页面代码仍然只有普通单栏 Navigation。左右区域的变量已经被收敛到 EasyGo,因此最小配置只声明主页和关联页,不加入 API 26 的模式、比例和颜色参数:
"navigationSplitOptions": {
"homePage": "navBar",
"relatedPage": "PreviewPage"
}
| 配置 | 当前值 | 作用 |
|---|---|---|
wideWindowMode | navigationSplit | 长方形宽窗口使用 Navigation 路由的平行视界 |
squareWindowMode | navigationSplit | 方形宽窗口使用相同路由方案 |
homePage | navBar | 把 Navigation 首页识别为主页 |
relatedPage | PreviewPage | 提供静态关联页 |
四个字段共同回答宽窗口采用哪种组织方式,以及哪两个页面建立初始关系。当前构建使用 API 26 Beta2 SDK 26.0.0.32。CompileArkTS 完成且没有 ArkTS 类型错误,构建随后完成 PackageHap 与 assembleHap,产物为 entry-default-unsigned.hap。未配置签名,因此交付的是调试用 unsigned HAP。
把这个 Demo 搬进其他工程时,建议先保持 NavigationMode.Stack,确认系统分栏生效后再接入原项目的标题栏、页面参数和状态管理。若一开始就把 Navigation 自有 Split、响应式 Row 和 EasyGo 同时打开,屏幕虽然也可能出现两栏,验收人员却无法确认是哪一条链路产生了结果。

总结
项目进入大屏后,可以先确认问题发生在哪一层。单个页面内部过于稀疏时调整响应式布局;产品已经确定长期 NavBar 双栏时使用 Navigation Split;现有路由页面需要系统在宽窗口协同显示时,再验证平行视界。Master-Detail 只能帮助识别页面关系,最终选择还要落到路由独立性、区域维护者和回退成本。
当前最小 Demo 保留 Navigation、NavPathStack、NavDestination 和最小 EasyGo,并已通过 API 26 SDK 检查和 HAP 构建。项目可以先用这条低风险链路确认系统分栏,再逐项验证页面关联、模式、比例和窗口回退。这样处理后,开头的选型问题就有了可执行顺序:判断页面关系,明确维护责任,用最小工程验证,随后再决定是否扩大接入范围。
我目前手里还没有可以测试 HarmonyOS 7 的真机,所以相关内容现阶段主要通过 HarmonyOS 7 模拟器进行验证,真机上的系统表现、设备差异和实际体验,后面有条件再继续补测,最终还是以实际设备运行结果为准。
完整代码
Index.ets
class RouteParam {
title: string = ''
}
@Entry
@Component
struct Index {
@Provide('pageStack')
pageStack: NavPathStack = new NavPathStack()
@Builder
pageMap(name: string) {
if (name === 'PreviewPage') {
PreviewPage()
}
}
build() {
Navigation(this.pageStack) {
Column({ space: 18 }) {
Text('平行视界能力选择')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.width('100%')
Text('当前页面只保留一条 Navigation 跳转链路,用来确认页面结构和路由控制器工作正常。')
.fontSize(16)
.fontColor('#596074')
.lineHeight(24)
.width('100%')
Column({ space: 10 }) {
Text('当前基线')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.width('100%')
Text('Navigation + NavPathStack + NavDestination')
.fontSize(15)
.fontColor('#46506A')
.width('100%')
Text('页面没有使用 Row、SideBarContainer 或 NavigationMode.Split 手写左右两栏。')
.fontSize(14)
.fontColor('#667085')
.lineHeight(22)
.width('100%')
}
.width('100%')
.padding(18)
.backgroundColor('#F1F4FA')
.borderRadius(18)
.alignItems(HorizontalAlign.Start)
Button('打开预览页')
.width('100%')
.height(50)
.onClick(() => {
let param: RouteParam = new RouteParam()
param.title = 'PreviewPage'
this.pageStack.pushPathByName('PreviewPage', param)
})
Text('窄窗口继续按普通单页显示;满足 EasyGo 条件的宽窗口由系统决定是否进入平行视界。')
.fontSize(14)
.fontColor('#667085')
.lineHeight(22)
.width('100%')
}
.width('100%')
.height('100%')
.padding(24)
.alignItems(HorizontalAlign.Start)
}
.mode(NavigationMode.Stack)
.title('能力选择')
.navDestination(this.pageMap)
.width('100%')
.height('100%')
}
}
@Component
struct PreviewPage {
@Consume('pageStack')
pageStack: NavPathStack
build() {
NavDestination() {
Column({ space: 18 }) {
Text('预览页')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.width('100%')
Text('NavDestination 已经进入当前 Navigation 的路由栈。')
.fontSize(16)
.fontColor('#596074')
.lineHeight(24)
.width('100%')
Button('返回首页')
.width('100%')
.height(50)
.onClick(() => {
this.pageStack.clear()
})
}
.width('100%')
.height('100%')
.padding(24)
.alignItems(HorizontalAlign.Start)
}
.title('预览页')
}
}
module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"easyGo": "$profile:easy_go",
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
]
}
]
}
}
easy_go.json
{
"common": {
"displayModeOptions": {
"wideWindowMode": "navigationSplit",
"squareWindowMode": "navigationSplit",
"navigationSplitOptions": {
"homePage": "navBar",
"relatedPage": "PreviewPage"
}
}
}
}
更多推荐



所有评论(0)