HarmonyOS 7 平行视界实战 01:Navigation + ArkUI 搭建“双栏研读台”第一版分栏阅读【鸿蒙心迹】
第一篇我不急着把左右推挤、固定覆盖、比例切换这些能力一口气全做完,而是先把最重要的底座搭起来:Navigation 路由、主从页面关系、ArkUI 双栏骨架,以及列表点击后右侧详情更新。 先把第一版分栏阅读做稳,后面几篇再继续往平行视界真正的路由规则和大屏交互上演进。

一、为什么我想用“研读台”来写平行视界
我一开始考虑平行视界的时候,脑子里其实有好几个场景:购物比价、邮件阅读、消息列表、知识库、文档浏览。最后我还是选了“资料阅读”这个方向,原因很简单——它特别适合做成连续演进的 Demo。
传统手机页面里,列表和详情通常是串行关系:
文章列表
↓
点击某一篇
↓
进入详情
↓
返回
↓
继续找下一篇
这个流程在手机上没什么问题,屏幕本来就不大。但放到平板、折叠屏展开态或者其他大屏设备上,继续让用户“进详情—返回—再进详情”,就会显得很浪费空间。
平行视界真正吸引我的地方,就是它把“大屏上的页面关系”重新组织了一次。官方当前把典型体验概括为两类:一种偏左右推挤、连续浏览;另一种偏左侧固定导航、右侧内容覆盖切换。对于“资料研读”这种场景,我觉得最自然的切入点就是:
左侧一直看得到资料列表,右侧专心看当前文章。
这也是“双栏研读台”这个名字的由来。
第一版我先不追求复杂阅读器功能,只做 4 件事:
- 左侧显示资料目录和文章列表;
- 点击左侧某篇文章;
- 右侧显示对应详情;
- 在大屏上保持稳定的双栏结构。
只要这条链路成立,后面第二篇就可以继续写“左右推挤”,第三篇再写“导航固定 + 覆盖切换”,第四篇做分栏比例和状态同步,第五篇做生命周期和路由恢复。
二、第一篇先把“页面关系”想清楚,而不是先画两个 Column
刚开始做双栏页面时,很容易直接写成:
Row
├─ LeftColumn
└─ RightColumn
这样当然能画出两个区域,但它只是“视觉上两栏”,不等于真正建立了页面关系。
我更想让这个 Demo 从第一篇开始就具备继续往后演进的结构,所以我没有把左栏和右栏当成两个普通 Column,而是围绕 Navigation 来组织。
HarmonyOS 当前的 Navigation 分栏能力里,Navigation 本身就是一个路由容器,支持单栏、分栏和自适应等模式。在分栏模式下,导航区域保持显示,子页面通过 NavDestination 和 NavPathStack 切换,这种结构和“双栏研读台”的需求很贴。
所以第一版里,我把页面关系定义成:
Navigation
├─ 左侧:ArticleListPage
└─ 右侧:ArticleDetailPage
左侧承担“主列表”职责,右侧承担“详情内容”职责。
这一步特别重要,因为后面平行视界的很多交互——推挤、固定、覆盖、返回栈——本质上都和路由关系有关。如果第一篇只是做一个假的左右布局,后面每篇都会越来越难接。
三、项目目录先定下来,后面五篇都围绕这套结构演进
这套 Demo 第一篇我就把目录做得稍微清楚一点,避免后面每增加一个能力就重新拆文件。
DualReadDemo
├─ entry
│ ├─ src/main/ets
│ │ ├─ pages
│ │ │ ├─ Index.ets
│ │ │ ├─ ArticleListPage.ets
│ │ │ ├─ ArticleDetailPage.ets
│ │ │ └─ AboutPage.ets
│ │ ├─ model
│ │ │ └─ ArticleItem.ets
│ │ ├─ mock
│ │ │ └─ MockData.ets
│ │ ├─ common
│ │ │ ├─ Constants.ets
│ │ │ └─ Logger.ets
│ │ └─ components
│ │ ├─ ArticleListItem.ets
│ │ └─ TagChip.ets
│ └─ resources
└─ module.json5
这几个文件的职责比较明确:
Index.ets:Navigation 根容器;ArticleListPage.ets:左侧资料列表;ArticleDetailPage.ets:右侧正文详情;ArticleItem.ets:文章数据模型;MockData.ets:第一篇先用模拟数据;Logger.ets:记录列表点击、路由切换和详情加载过程。
第一篇我刻意不接后端,也不做数据库。因为这一篇要验证的是平行视界式分栏体验,而不是数据层能力。先用稳定的模拟数据把布局和路由关系跑清楚,反而更容易看出真正的问题。
四、Navigation 是第一篇最重要的骨架
第一版最核心的代码就在 Index.ets。
文件位置: entry/src/main/ets/pages/Index.ets
用途: 建立 Navigation 根容器,切到分栏模式,并让左侧列表点击后驱动右侧详情。
import { ArticleListPage } from './ArticleListPage'
import { ArticleDetailPage } from './ArticleDetailPage'
import { ArticleItem } from '../model/ArticleItem'
import { Logger } from '../common/Logger'
@Entry
@Component
struct Index {
private pathStack: NavPathStack = new NavPathStack()
@State selectedArticle?: ArticleItem
build() {
Navigation(this.pathStack) {
ArticleListPage({
onSelect: (item: ArticleItem) => {
this.openArticle(item)
}
})
}
.title('双栏研读台')
.mode(NavigationMode.Split)
.navBarWidth('34%')
.navBarWidthRange(['28%', '42%'])
.enableDragBar(true)
.navDestination(this.pageMap)
}
@Builder
pageMap(name: string, param: object) {
if (name === 'ArticleDetailPage') {
ArticleDetailPage({
article: param as ArticleItem
})
}
}
private openArticle(item: ArticleItem) {
this.selectedArticle = item
Logger.info(
`[Navigation] open detail, id=${item.id}, title=${item.title}`
)
this.pathStack.pushPath({
name: 'ArticleDetailPage',
param: item
})
}
}
这段代码里,我最关注 4 个点。
1)NavigationMode.Split
它让 Navigation 以分栏方式工作。第一篇的目标不是做最终平行视界配置,而是先把主从页面在大屏上的分栏关系搭出来。
2)navBarWidth
我先给左侧列表一个比较克制的宽度,让右侧正文拿到更多空间。对于资料阅读场景,左栏主要是定位文章,不应该和正文抢面积。
3)enableDragBar
我把拖拽分割线先打开,让第一版就能感受到“大屏分栏不是死布局”。这也给第四篇的分栏比例适配埋了一个很自然的伏笔。
4)NavPathStack
列表点击以后,不是手工把右侧某个 Column 的内容改掉,而是通过 NavPathStack 推进详情页。这样第二篇、第三篇继续做路由演进时,会顺很多。

五、左侧列表不要做得太重,它只是“找内容”的工具
第一篇左侧列表的定位很明确:快速定位资料。
所以我没有一上来加复杂筛选、收藏、分组、多级目录,只保留:
- 搜索框;
- 文章缩略图;
- 标题;
- 简短摘要;
- 日期;
- 当前选中态。
文件位置: entry/src/main/ets/pages/ArticleListPage.ets
用途: 展示文章列表,并把用户选中的 ArticleItem 交给 Navigation。
@Component
export struct ArticleListPage {
@Link selectedId: string
@State keyword: string = ''
private articles: ArticleItem[] = MockData.getArticles()
onSelect: (item: ArticleItem) => void = () => {}
build() {
Column() {
TextInput({
placeholder: '搜索文章、关键词或标签',
text: this.keyword
})
.height(40)
.margin({ left: 16, right: 16, top: 12, bottom: 10 })
.onChange((value: string) => {
this.keyword = value
})
List({ space: 8 }) {
ForEach(this.filteredArticles(), (item: ArticleItem) => {
ListItem() {
ArticleListItem({
item: item,
selected: this.selectedId === item.id
})
}
.onClick(() => {
this.selectedId = item.id
Logger.info(`[List] select article=${item.title}`)
this.onSelect(item)
})
}, (item: ArticleItem) => item.id)
}
.layoutWeight(1)
.padding({ left: 12, right: 12, bottom: 12 })
}
.width('100%')
.height('100%')
}
private filteredArticles(): ArticleItem[] {
const key = this.keyword.trim()
if (!key) {
return this.articles
}
return this.articles.filter(item =>
item.title.includes(key) ||
item.summary.includes(key) ||
item.tags.some(tag => tag.includes(key))
)
}
}
这一页我故意写得很普通,因为“研读台”的主角不是列表,而是左右协同的阅读体验。
六、右侧详情页才是大屏价值真正开始体现的地方
右侧详情页和普通手机详情页最大的区别,不是组件变了,而是它终于可以“在不离开列表上下文的情况下”展开完整内容。
我在详情页里先放这些东西:
- 文章标题;
- 日期和阅读量;
- 标签;
- 封面图;
- 正文;
- 三张相关阅读图片。
这样右侧已经足够像一篇完整技术文章。
文件位置: entry/src/main/ets/pages/ArticleDetailPage.ets
用途: 在右侧内容区展示完整文章内容。
@Component
export struct ArticleDetailPage {
@Prop article?: ArticleItem
aboutToAppear() {
if (this.article) {
Logger.info(
`[Detail] load article, id=${this.article.id}, title=${this.article.title}`
)
}
}
build() {
if (!this.article) {
this.buildEmpty()
return
}
Scroll() {
Column({ space: 14 }) {
Text(this.article.title)
.fontSize(26)
.fontWeight(FontWeight.Bold)
.width('100%')
Row({ space: 12 }) {
Text(this.article.date)
.fontSize(13)
.fontColor('#888888')
Text(`阅读 ${this.article.readCount}`)
.fontSize(13)
.fontColor('#888888')
}
.width('100%')
Row({ space: 8 }) {
ForEach(this.article.tags, (tag: string) => {
TagChip({ text: tag })
})
}
.width('100%')
Image(this.article.cover)
.width('100%')
.height(260)
.objectFit(ImageFit.Cover)
.borderRadius(12)
Text(this.article.content)
.fontSize(17)
.fontColor('#333333')
.lineHeight(30)
.width('100%')
Text('相关阅读')
.fontSize(20)
.fontWeight(FontWeight.Medium)
.width('100%')
.margin({ top: 8 })
Row({ space: 12 }) {
ForEach(this.article.relatedCovers, (cover: ResourceStr) => {
Image(cover)
.layoutWeight(1)
.height(120)
.objectFit(ImageFit.Cover)
.borderRadius(10)
})
}
.width('100%')
}
.padding(22)
.width('100%')
}
.width('100%')
.height('100%')
}
@Builder
buildEmpty() {
Column() {
Text('从左侧选择一篇文章开始阅读')
.fontSize(18)
.fontColor('#888888')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
这段代码本身没什么炫技,但它很适合第一篇。因为你把列表和详情放到同一大屏以后,会非常直观地感受到:原来同样是文章列表和文章详情,在大屏上的组织方式一变,阅读节奏真的会变。

七、第一版最重要的体验,不是“双栏好看”,而是“列表上下文一直在”
我做完第一版以后,最明显的感受不是屏幕利用率提高了,而是列表上下文没有丢。
以前手机上阅读:
列表 → 详情 → 返回 → 找下一篇
现在大屏上:
左侧列表一直在
右侧详情不断切换
这个变化看起来很简单,但它会直接影响用户的阅读节奏。
比如我在左侧连续点:
- HarmonyOS 7 全新特性解析;
- ArkUI 3.0 开发实践指南;
- 分布式能力全场景体验;
- 应用上架与版本管理。
右侧内容会跟着变化,但我始终知道自己“还在这个资料目录里”。
这就是我觉得平行视界特别适合资料阅读的原因。它不是简单把一页硬切成两半,而是把“导航”和“内容”同时保留下来。
八、为什么第一篇还不直接写左右推挤和固定覆盖
做到这里,有人可能会问:既然是平行视界,为什么第一篇不直接把左右推挤、固定覆盖、返回栈都写进去?
原因很简单:我不想让第一篇承担太多概念。
HarmonyOS 7 当前的平行视界典型场景里,确实包含左右推挤、固定导航 / 覆盖切换,以及可调分栏比例。官方示例也已经给出基于 Navigation 路由的典型实现方向。
但对于这套五连载来说,我更希望顺序是:
- 第一篇:先把 Navigation 主从页面搭起来;
- 第二篇:开始研究连续浏览和左右推挤;
- 第三篇:把左侧导航固定住,让右侧做覆盖切换;
- 第四篇:处理不同大屏尺寸、分栏比例和状态同步;
- 第五篇:收生命周期、返回栈、窗口恢复和工程结构。
这样每篇都有一个明确问题,而且 Demo 真的在往前长。
九、类型二综合图里,我希望一张图把“结构、代码、界面、实机”都讲清楚
这套连载我继续保留之前已经确定下来的“类型二综合讲解图”。
第一篇的类型二图,我最希望它能把 4 类信息同时放进去:
- 整套“双栏研读台”的结构关系;
- DevEco 里 Navigation 和页面代码;
- 无设备边框的大屏截图;
- 真机平板运行效果。
因为平行视界这个主题特别适合做这种图。单看代码,读者很难脑补最终大屏体验;单看 UI,又看不出路由关系。把它们放在同一张综合图里,反而最容易理解。
十、无边框大屏截图更适合展示第一篇的核心结果
这篇我没有把“纯截图”做成手机竖屏,而是换成了更符合平行视界场景的大屏横向截图。
因为这一篇真正要展示的是:
- 左侧资料目录;
- 右侧文章详情;
- 两栏同时存在;
- 当前文章选中态;
- 详情正文、封面和标签。
如果套一个设备外框,反而会缩小真正有用的 UI 内容。所以正文里我更倾向于直接放一张无设备边框的横向运行截图。

这张图基本就是第一篇最核心的“结果证明”。
十一、文章数据模型也要提前定好,不然后面路由参数会越来越乱
第一篇虽然是 UI 和 Navigation 为主,但我还是专门把 ArticleItem 单独抽了出来。原因很简单:右侧详情页的数据最终都来自左侧列表选择,如果每次跳转都临时拼几个字段,第二篇继续做左右推挤、第三篇处理覆盖切换时,参数很快就会乱。
文件位置: entry/src/main/ets/model/ArticleItem.ets
用途: 定义列表和详情共用的文章数据结构。
export interface ArticleItem {
id: string
title: string
summary: string
content: string
date: string
readCount: string
category: string
tags: string[]
cover: ResourceStr
relatedCovers: ResourceStr[]
}
第一篇我让列表和详情都直接围绕这一个模型工作。这样做以后,路由参数就不会出现“列表传 title,详情又单独去找 cover”的情况。
Mock 数据也尽量写得像真实资料,而不是 文章1 / 文章2 / 文章3 这种占位内容。因为分栏阅读 Demo 最重要的是观察真实信息密度:标题长一点会不会挤压,摘要两行会不会影响左栏高度,详情图加正文以后右侧滚动是不是舒服。
文件位置: entry/src/main/ets/mock/MockData.ets
export class MockData {
static getArticles(): ArticleItem[] {
return [
{
id: 'harmonyos7',
title: 'HarmonyOS 7 全新特性解析',
summary: '探索更智能、更流畅、更开放的全新体验',
content: 'HarmonyOS 7 在系统架构、全场景协同、大屏体验等方向继续演进……',
date: '2026-10-27',
readCount: '1.2万',
category: 'HarmonyOS',
tags: ['HarmonyOS 7', '系统特性', '大屏体验'],
cover: $r('app.media.cover_harmonyos7'),
relatedCovers: [
$r('app.media.related_1'),
$r('app.media.related_2'),
$r('app.media.related_3')
]
},
{
id: 'arkui-guide',
title: 'ArkUI 3.0 开发实践指南',
summary: '从组件布局到大屏交互的工程实践',
content: '这篇资料重点整理 ArkUI 在复杂页面和大屏设备中的布局实践……',
date: '2026-10-25',
readCount: '856',
category: 'ArkUI',
tags: ['ArkUI', 'Navigation'],
cover: $r('app.media.cover_arkui'),
relatedCovers: []
}
]
}
}
这种数据模型看起来和“平行视界”没有直接关系,但它其实决定了后面路由链路能不能干净地演进。第一篇先把它定下来,后面几篇只需要继续扩展 selectedId、阅读进度、页面栈状态,不需要重新改基础模型。
十二、第一版我专门踩了几个坑,这些坑比“分栏成功”更值得记
页面第一次跑起来以后,真正让我觉得有必要记录的,并不是“终于看到两个栏了”,而是几个很典型的问题。
1)右侧第一次进入时一大片空白
Navigation 分栏以后,如果没有默认详情内容,右侧很容易变成纯空白。技术上没错,但体验特别像“页面没加载出来”。
我后来给详情区补了明确空状态:
从左侧选择一篇文章开始阅读
这句文字看起来不起眼,但它马上把“错误感”变成了“等待用户操作”。
2)左侧选中态和右侧详情不同步
最初我只调用 pushPath(),没有同步 selectedId。结果详情已经切换,但左侧列表仍然高亮上一篇文章。大屏双栏里这种错误特别明显,因为两个状态同时在眼前。
所以后来我把选择状态放到更上层:
@State selectedId: string = ''
private openArticle(item: ArticleItem) {
this.selectedId = item.id
this.pathStack.pushPath({
name: 'ArticleDetailPage',
param: item
})
}
这件事让我意识到:平行视界最大的挑战之一,就是两个区域同时可见以后,状态不一致会被立刻放大。
3)左栏宽度不能只看“好不好看”
一开始我把左栏设得比较宽,列表看起来舒服,但右侧正文变窄以后,文章阅读明显不自然。后来我才慢慢收成 30%~35% 左右,把主要空间还给详情页。
这个比例并不是最终答案,第四篇还会专门处理不同屏幕和比例。但第一篇至少让我确认:导航区是辅助,内容区才是主体。
4)详情页不能每次重新造一套数据
如果右侧详情每次进入都重新查一遍数据,列表快速切换时会出现明显延迟。第一篇先用轻量 ArticleItem 直接传参,一方面是为了 Demo 稳定,另一方面也是为了让连续点击更顺。
后面如果接真实数据源,再考虑 Repository、缓存和异步加载,而不是第一篇就把链路做复杂。
十三、日志必须跟着路由一起走,不然第二篇开始很难排查
我这次继续保留了前面系列文章里的一个习惯:从第一篇就打关键日志。
因为第二篇要做连续浏览和左右推挤,第三篇还会碰返回栈和覆盖切换。如果第一篇完全没有日志,后面出问题时很难判断到底是列表点击没触发、路由没推进,还是详情页没拿到参数。
我现在主要记录 4 类日志:
[List] select article=HarmonyOS 7 全新特性解析
[Navigation] push ArticleDetailPage, id=harmonyos7
[Detail] load article, id=harmonyos7
[PageLife] ArticleDetailPage aboutToAppear
这些日志不复杂,但足够帮助我判断当前链路。
我也刻意没有把每一个组件重绘、每一个属性变化都打出来。技术文章里的日志和代码一样,应该服务于问题,而不是越多越专业。
十四、第一篇为什么先用 Navigation 分栏,而不是直接把所有平行视界配置一次讲完
这一点我觉得有必要单独说明。
HarmonyOS 的平行视界本质上是面向大屏设备的系统级应用内分屏方案,官方当前的适配思路里会涉及路由关系、不同分栏模式以及相关配置;官方示例也以 Navigation 路由应用为载体展示典型场景。
但我这套连载第一篇先从 Navigation 分栏和业务页面关系入手,是刻意控制学习节奏。
因为如果第一篇同时讲:
- Navigation;
- 页面路由;
- easy_go 配置;
- 购物模式;
- 导航模式;
- 全屏页;
- 过渡页;
- 虚拟容器;
文章一定会变成“能力清单”,而不是一个 Demo 的学习过程。
所以第一篇只解决业务页面基础:
先把左右两栏、主从页面和路由关系搭好。
后面第二篇再进入连续浏览和左右推挤,第三篇再进入固定导航和覆盖切换。这样每篇文章都能围绕一个真实问题展开,而不是一篇里把所有名词都扫一遍。
这也是我写五连载时最想保持的节奏:每一篇都有一个可以跑出来的结果,也留下一个自然暴露出来的下一步问题。
十五、第一篇我会怎么验收
第一篇的验收标准我不会写得特别复杂,主要看下面几件事。
1)大屏上能稳定显示左右两栏
左侧列表和右侧详情必须同时可见,而不是屏幕宽一点就偶尔分栏,缩一点就布局崩掉。
2)点击列表,右侧详情能准确切换
这一条是主线。列表点击和右侧文章必须一一对应,不能出现选中态已经变了、详情还是上一篇的情况。
3)Navigation 路由栈能记录详情页
我会从日志里确认每次点击都产生对应的路由动作,并且详情页能拿到正确的 ArticleItem。
4)空状态要成立
刚进入页面、还没有选文章时,右侧不应该是一大片不知道在干什么的空白,而应该明确告诉用户“从左侧选择一篇文章开始阅读”。
5)大屏阅读真的比单栏顺
这一条不是代码验收,而是体验验收。我会连续点 5~6 篇文章,看看自己是否还需要频繁返回。如果双栏只是“看起来宽”,但阅读节奏没变,那这个 Demo 就没有意义。
十六、实机上跑起来以后,我才真正确定这个题材适合继续写
当“双栏研读台”真正跑到平板上以后,我才比较确定这套五连载值得继续写。
原因不是 UI 多漂亮,而是它确实出现了一个很明确的大屏价值:
左侧不丢上下文,右侧专心读内容。
这种感觉在模拟器里能看到,但放到真机上会更明显。因为实际平板横屏以后,左右两侧的空间比例、触控距离、阅读密度都会更接近真实使用。

第一篇到这里,我觉得已经够了。
它没有一次性把平行视界所有模式都讲完,但把最重要的地基搭起来了:
- Navigation 根容器;
- 主列表与详情页关系;
- NavPathStack 路由;
- Split 分栏;
- 左右双栏第一版 UI;
- 实机大屏验证。
这些东西一旦稳定,第二篇就可以顺着它自然进入下一层问题:
连续浏览时,右侧详情继续打开新内容,页面之间怎样左右推挤,才不会让阅读链路断掉?
十七、本文小记
如果让我用一句话总结第一篇,我会说:
第一篇不是为了证明“我能做一个左右布局”,而是为了把“双栏研读台”的页面关系和阅读主线真正搭起来。
这两者差别很大。
一个普通左右布局,只是视觉排版;而现在这版 Demo 已经开始具备路由、主从页面、详情切换和大屏上下文保留。
这也是我为什么愿意把平行视界继续拆成五篇来写。它很适合用“一个 Demo 不断发现新问题”的方式往下推。
下一篇,我准备继续处理第一版最明显的下一层问题:连续浏览和左右推挤。
对我来说,这一篇最值得保留的,不是某个单独 API,而是第一次把“大屏为什么需要两栏”这件事做成了可以实际点击、切换和验证的页面关系。这个基础一旦稳定,后面的交互演进才真正有意义。
更多推荐



所有评论(0)