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

请添加图片描述

一、为什么我想用“研读台”来写平行视界

我一开始考虑平行视界的时候,脑子里其实有好几个场景:购物比价、邮件阅读、消息列表、知识库、文档浏览。最后我还是选了“资料阅读”这个方向,原因很简单——它特别适合做成连续演进的 Demo。

传统手机页面里,列表和详情通常是串行关系:

文章列表
   ↓
点击某一篇
   ↓
进入详情
   ↓
返回
   ↓
继续找下一篇

这个流程在手机上没什么问题,屏幕本来就不大。但放到平板、折叠屏展开态或者其他大屏设备上,继续让用户“进详情—返回—再进详情”,就会显得很浪费空间。

平行视界真正吸引我的地方,就是它把“大屏上的页面关系”重新组织了一次。官方当前把典型体验概括为两类:一种偏左右推挤、连续浏览;另一种偏左侧固定导航、右侧内容覆盖切换。对于“资料研读”这种场景,我觉得最自然的切入点就是:

左侧一直看得到资料列表,右侧专心看当前文章。

这也是“双栏研读台”这个名字的由来。

第一版我先不追求复杂阅读器功能,只做 4 件事:

  1. 左侧显示资料目录和文章列表;
  2. 点击左侧某篇文章;
  3. 右侧显示对应详情;
  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 路由的典型实现方向。

但对于这套五连载来说,我更希望顺序是:

  1. 第一篇:先把 Navigation 主从页面搭起来;
  2. 第二篇:开始研究连续浏览和左右推挤;
  3. 第三篇:把左侧导航固定住,让右侧做覆盖切换;
  4. 第四篇:处理不同大屏尺寸、分栏比例和状态同步;
  5. 第五篇:收生命周期、返回栈、窗口恢复和工程结构。

这样每篇都有一个明确问题,而且 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,而是第一次把“大屏为什么需要两栏”这件事做成了可以实际点击、切换和验证的页面关系。这个基础一旦稳定,后面的交互演进才真正有意义。

Logo

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

更多推荐