前两篇把 TwinShelf 的大屏阅读基本结构跑通了。

01 用 Navigation 把左侧文章目录和右侧详情接在同一条路由主线上,1280×800vp 下进入 SPLIT 后,article_042 仍然是同一个选中对象;02 又把右栏继续往前推进,article_042 → related_107 → related_205 → back → back 全部沿用一条 NavPathStack,分栏比例在 1:2 / 2:1 之间切换,左侧选择和 612vp 阅读位置都没丢。

做到这里,大屏“能分栏”已经不是主要问题。

真正进入内容阅读以后,有一类页面不适合一直困在右侧半屏里:图片画廊、视频预览、长图、地图、沉浸式阅读。

HarmonyOS 7 当前官方平行视界示例已经把“配置过渡页面”和“路由跳转过程中请求全屏”列为两个典型场景;Window 侧也提供 setWindowLayoutFullScreen() 这类沉浸式能力。TwinShelf 03 就把这两个能力放到同一条阅读链里:

article_042
→ MediaTransition
→ ImageGallery Fullscreen
→ Exit Fullscreen
→ article_042

这篇真正要验收的不是“全屏能打开”,而是:

全屏只是一段展示状态,退出以后原文章、原选中态、原阅读位置和原路由层级都要恢复。

本轮统一数据:

taskId:
parallel_20261003_03

window:
1440 × 900vp

startRoute:
article_042

transitionRoute:
MediaTransition

fullscreenRoute:
ImageGallery

assetId:
img_nav_03

galleryIndex:
3 / 8

routeDepth:
1 → 2 → 3 → 2 → 1

transitionPushes:
1

fullscreenRequests:
2

fullscreenSuccess:
2

systemBarsHidden:
2

systemBarsRestored:
2

fullscreenDuration:
14.2s

selectedArticleId:
article_042

detailScrollOffset:
612vp

selectionRetained:
true

contentRebuildCount:
0

status:
FULLSCREEN_ROUTE_STABLE

一、过渡页不是为了“多一层路由”,而是为了隔开两种显示语义

如果在文章详情里直接:

点击图片
→ setWindowLayoutFullScreen(true)
→ ImageGallery

功能可以跑。

但平行视界下右栏本身还承担“详情页”语义。全屏资源页和普通详情页混在同一层,很容易出现两个问题:

全屏刚切换时,右栏内容闪一下;
退出全屏以后,不知道应该回文章还是回关联页。

TwinShelf 增加 MediaTransition。

它不做复杂业务,只保存:

articleId
assetId
galleryIndex
detailScrollOffset

然后再进入真正的 ImageGallery。

也就是说:

ArticleDetail
是阅读状态;

MediaTransition
是切换状态;

ImageGallery
是沉浸展示状态。

三者职责分开。

二、Route Snapshot 必须在进入全屏之前固定下来

第一段代码解决的是:全屏之前先保存阅读现场,而不是退出以后再猜。

export interface FullscreenSnapshot {
  articleId: string
  assetId: string
  galleryIndex: number
  detailScrollOffsetVp: number
}

export class FullscreenCoordinator {
  private snapshot:
    FullscreenSnapshot | undefined

  prepare(
    articleId: string,
    assetId: string,
    galleryIndex: number,
    scrollOffsetVp: number
  ): void {
    this.snapshot = {
      articleId,
      assetId,
      galleryIndex,
      detailScrollOffsetVp:
        scrollOffsetVp
    }
  }
}

本轮固定:

article_042
img_nav_03
index=3
scroll=612vp

只要进入全屏,这份 Snapshot 就不再跟着页面临时状态变化。

这样退出时不需要从 Gallery 反向推导“刚才从哪篇文章进来的”。

三、先 Push 过渡页,再请求沉浸式全屏

第二段代码把路由和 Window 状态拆开。

async openMedia(
  articleId: string,
  assetId: string,
  index: number,
  scrollOffsetVp: number
): Promise<void> {
  this.prepare(
    articleId,
    assetId,
    index,
    scrollOffsetVp
  )

  await this.pathStack
    .pushPathByName(
      'MediaTransition',
      {
        articleId,
        assetId,
        index
      }
    )

  await this.enterFullscreen()

  await this.pathStack
    .pushPathByName(
      'ImageGallery',
      {
        articleId,
        assetId,
        index
      }
    )
}

这段代码解决的不是“动画好看”。

而是保证顺序明确:

路由上下文先稳定
→ Window 再进入全屏
→ 最后加载 Gallery

如果全屏请求失败,当前还停在过渡页,仍然有机会回到 article_042,而不是让 Navigation 和 Window 状态各走各的。

四、沉浸式全屏只改变 Window,不修改选中文章

TwinShelf 进入全屏时调用 Window 能力:

private async enterFullscreen():
  Promise<void> {
  const win =
    await window.getLastWindow(
      this.uiContext.getHostContext()
    )

  await win
    .setWindowLayoutFullScreen(
      true
    )

  await win
    .setWindowSystemBarEnable(
      []
    )

  this.fullscreenRequests++
  this.fullscreenSuccess++
}

当前官方文档里,setWindowLayoutFullScreen(true) 用于让应用主窗口/子窗口进入沉浸式布局;系统栏显隐则是另一个窗口状态。

TwinShelf 只改变:

Window Presentation

不改变:

selectedArticleId

所以全屏过程中左栏虽然不可见,业务选择仍然是:

article_042

五、全屏页只消费 assetId,不接管文章路由

ImageGallery 当前拿到:

assetId=img_nav_03
index=3/8

它不知道:

左栏有哪些分类;
article_042 在第几行;
文章列表滚到哪里。

这些都属于外层阅读状态。

Gallery 自己只维护:

当前图片索引
手势缩放
左右切换
全屏时长

本轮全屏持续:

14.2s

整个过程中没有重新创建 ArticleDetail。

六、退出顺序必须和进入顺序反过来

最容易出错的是退出。

如果直接:

pop ImageGallery
pop MediaTransition

但 Window 还在 Fullscreen,文章页会突然以全屏状态出现。

TwinShelf 的顺序是:

1. 退出 Fullscreen
2. 恢复 System Bar
3. pop ImageGallery
4. pop MediaTransition
5. restore article snapshot

第三段代码:

async exitMedia(): Promise<void> {
  const win =
    await window.getLastWindow(
      this.uiContext.getHostContext()
    )

  await win
    .setWindowLayoutFullScreen(
      false
    )

  await win
    .setWindowSystemBarEnable([
      window.SystemBarType.STATUS_BAR,
      window.SystemBarType.NAVIGATION_BAR
    ])

  this.pathStack.pop()
  this.pathStack.pop()

  if (this.snapshot) {
    this.articleStore
      .select(
        this.snapshot.articleId
      )

    this.articleStore
      .restoreScroll(
        this.snapshot
          .detailScrollOffsetVp
      )
  }
}

本轮:

systemBarsHidden=2
systemBarsRestored=2

进入和退出全部成对。

七、为什么路由深度最后一定要回到 1

这一轮的路由深度:

1
article_042

2
MediaTransition

3
ImageGallery

2
退出 Gallery

1
退出 Transition

最终:

article_042

如果最终深度仍然是 2,说明过渡页成了“幽灵路由”。

用户下一次按返回键就会先回到一个已经没有视觉意义的 MediaTransition。

所以 03 把:

finalDepth=1

作为硬验收。

八、重复点击媒体入口也要做幂等

用户快速连点两次图片时,如果:

MediaTransition
MediaTransition
ImageGallery
ImageGallery

连续入栈,退出逻辑会直接失控。

TwinShelf 在 Coordinator 里增加:

opening
isFullscreen

两层判断。

只要已经处于:

Transition / Fullscreen

新的 openMedia 直接丢弃。

这类去重不应该只靠按钮 disabled。

路由协调器本身必须保证幂等。

九、全屏失败要回退到文章,而不是留在中间态

例如:

setWindowLayoutFullScreen
失败

项目不会继续 push ImageGallery。

当前处理:

记录 error
pop MediaTransition
恢复 article_042
恢复 scroll=612vp

也就是说,过渡页既负责“进入前缓冲”,也负责“失败时回滚”。

真正产品里,这种回退路径比成功路径更值得测试。

十、平行视界和沉浸式全屏不是互斥的两套应用状态

平行视界负责:

大屏里的左右协同

全屏负责:

某一段内容临时占满窗口

TwinShelf 不会因为进入 ImageGallery 就重新启动另一个 Ability,也不会把大屏阅读上下文清空。

退出后继续回:

article_042
612vp

这就是“全屏是一段展示状态”的核心。

十一、全屏期间不改变文章阅读位置

用户在文章里滚到:

612vp

点击第三张图片进入 Gallery。

在 Gallery 里看了 14.2 秒。

回到文章:

detailScrollOffset=612vp

没有跳到顶部。

如果把滚动位置保存在 ArticleDetail 的局部临时变量里,页面重建时很容易丢。

所以 01 就建立的 Article Store 到 03 继续复用。

十二、DevEco 图里重点看进入和退出的顺序

开发图:

HiLog:

taskId=
parallel_20261003_03

start=
article_042

scroll=
612vp

push=
MediaTransition

push=
ImageGallery

depth=
3

fullscreen=
true

systemBarsHidden=
true

exit fullscreen

duration=
14.2s

systemBarsRestored=
true

pop Gallery

pop Transition

finalDepth=
1

selected=
article_042

scroll=
612vp

rebuild=
0

status=
FULLSCREEN_ROUTE_STABLE

这条日志比“全屏图片很好看”更能说明工程是否稳定。

十三、手机运行图展示的是完整 Fullscreen Route Timeline

最终运行图:

中间:

article_042
→ MediaTransition
→ ImageGallery
→ Exit Fullscreen
→ article_042

当前停在 Gallery 的:

3 / 8
img_nav_03

下方同时保留:

14.2s
2/2 全屏请求成功
2/2 系统栏恢复
scroll=612vp
contentRebuild=0

最终:

FULLSCREEN_ROUTE_STABLE

十四、03 最后固定八组反向测试

第一组,从 article_042 第三张图进入 Transition。

第二组,Transition 只 Push 一次。

第三组,全屏请求成功以后再进入 Gallery。

第四组,快速连点不会重复入栈。

第五组,Gallery 返回时先恢复 Window 再 pop。

第六组,System Bar 隐藏和恢复成对。

第七组,最终 NavPathStack 回到 depth=1。

第八组,article_042 与 612vp 阅读位置保持。

全部通过后:

FULLSCREEN_ROUTE_STABLE

才成立。

十五、下一篇:全屏解决“临时占满”,横屏与虚拟容器解决“同一右栏里继续工作”

03 的 Gallery 是一个很明确的临时全屏页。

但阅读应用里还有另一类需求:

图表分析
笔记
讨论

它们不是“一眼看完就返回”。

用户可能在同一篇文章上下文里来回切三四次。

如果每切一次都往外层 NavPathStack 继续 push:

ArticleDetail
→ Chart
→ Notes
→ Discussion
→ Notes
→ Chart

外层路由会越来越深。

04 会继续 TwinShelf,处理两个官方平行视界示例已经覆盖的场景:

路由过程请求横屏;
开启虚拟容器。

TwinShelf 会把:

ChartView
NotesView
DiscussionView

组织到一个 reader_workspace 内部工作区里。

外层文章仍然保持 article_042。

十六、过渡页还要解决“资源准备”和“路由切换”谁先发生

图片画廊里真正耗时的并不一定是路由本身。

如果全屏页一进入才开始:

读取原图
计算尺寸
生成首屏缩略图
准备手势状态

用户会看到一个很明显的黑屏或空白。

TwinShelf 把 MediaTransition 当成一个很轻的预热窗口。

它会在过渡阶段提前完成:

asset 元数据读取
目标图片索引确认
首张资源预取
Gallery 初始尺寸计算

但不会创建真正的 Gallery 页面业务状态。

预热成功以后才继续:

enterFullscreen
→ push ImageGallery

如果预热失败,直接从 Transition 回 article_042。

这种做法让“路由成功”和“媒体可展示”两件事不再互相赌时序。

十七、全屏页的返回手势不能绕过 Coordinator

如果 ImageGallery 自己直接:

pathStack.pop()

外层就失去了恢复 Window 的机会。

所以 TwinShelf 把:

返回按钮
系统返回手势
Esc / Back

最终都汇总到:

FullscreenCoordinator.exitMedia()

只有 Coordinator 可以执行:

退出全屏
恢复系统栏
回退 Gallery
回退 Transition
恢复 article snapshot

这能避免“顶部返回正常,系统返回却留下全屏窗口”的双路径问题。

十八、全屏状态还要防止应用进入后台以后恢复错误

用户在 Gallery 全屏时切到后台,再回前台,系统栏、窗口布局和路由可能经历一次恢复。

TwinShelf 额外保存:

isFullscreen
fullscreenRoute
snapshotVersion

前台恢复时只检查状态,不重复:

push ImageGallery

如果当前路由已经在 Gallery,就只重新校验 Window 呈现状态。

如果 Gallery 路由已经被系统恢复到 article_042,则清掉残留的 fullscreen 标记。

这种“以当前真实路由为准”的恢复逻辑比简单在 onForeground 里再次 enterFullscreen 更安全。

十九、Gallery 自己也有内部索引,但它不能反向改文章选中态

当前:

galleryIndex=3/8

用户在全屏里切到:

5/8

只改变:

GalleryState.currentIndex

不会把左栏的:

selectedArticleId

改成别的文章。

这是两个不同维度:

文章选择
和
文章内媒体选择。

退出以后,文章仍然是 article_042。

如果产品需要记录“上次看到第几张图”,可以把 Gallery Index 写回 article_042 的阅读快照,但不能混成主路由选择。

二十、系统栏恢复要用进入前快照,而不是假设默认一定显示

本轮 Demo 进入 Gallery 前:

状态栏显示
导航栏显示

所以退出时恢复:

STATUS_BAR
NAVIGATION_BAR

真实应用里有可能文章本身已经处于沉浸式阅读模式。

这时退出 Gallery 如果硬恢复所有系统栏,反而会改变用户原来的阅读状态。

正式版本应该保存:

systemBarSnapshot
layoutFullScreenSnapshot

进入时记录,退出时按原值恢复。

03 的 2/2 恢复数据表达的是“成对恢复”,不是“所有页面都必须显示两条系统栏”。

二十一、全屏时长只是诊断数据,不参与路由判断

本轮:

fullscreenDuration=14.2s

它可以用于体验分析:

用户是否真的在看 Gallery;
媒体页是否一闪而过;
不同资源平均停留多久。

但路由正确性不能依赖:

停留超过 1 秒才算进入成功。

真正的 Gate 仍然是:

route depth
window state
system bars
snapshot restore

时间只作为辅助证据。

二十二、内容重建次数 0 是这一篇的隐藏主指标

如果退出全屏以后:

article_042
scroll=612vp

只是靠重新请求文章,再手工滚回 612vp 恢复出来,表面看也能通过。

但这条路径成本更高,而且容易丢编辑态、图片加载态和局部组件状态。

所以 TwinShelf 同时记录:

contentRebuildCount=0

证明全屏往返只是路由与 Window 呈现变化,没有把文章主体重建一遍。

这和 01、02 的连续性主线完全一致。

二十三、Transition Page 最终应该保持“轻”和“无业务副作用”

过渡页如果自己开始:

修改收藏状态
写阅读记录
加载另一篇文章
提交统计

就会从“切换缓冲层”演变成第四个业务页面。

TwinShelf 对 MediaTransition 设定明确边界:

读取快照
预取媒体
显示过渡视觉
处理失败回滚

不拥有文章业务事务。

这样未来即使换成另一种转场动画,也不会影响正文与 Gallery 的状态协议。

二十四、最终验收还要覆盖“全屏中途换图再返回”

只验证进入第 3 张图片后原样退出还不够。TwinShelf 还会在 Gallery 内切到第 5 张,再退出全屏,确认文章仍然回到 article_042 / 612vp,同时 Gallery 自己可以把最后浏览索引保存为文章内媒体状态。这样能够证明“媒体内部状态变化”和“文章外层路由状态”互不覆盖。后续如果加入视频、长图或地图,全屏协调器仍然只负责 Window 与路由收口,媒体组件自己的播放位置、缩放比例和索引由各自 Store 管理。

这种分层还有一个直接好处:以后全屏页发生异常,只需要回滚当前媒体状态,不必重置整篇文章。对于平行视界这类同时存在左栏、右栏、全屏和过渡页的场景,越早把状态 Owner 拆清楚,后面越不容易出现“退出一个页面,整个阅读现场都回到默认值”的问题。

参考资料

  • HarmonyOS 平行视界示例:
    https://developer.huawei.com/consumer/cn/samples/
  • 窗口沉浸式:
    https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/immersive-window-feature
  • setWindowLayoutFullScreen FAQ:
    https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-199
  • Navigation 分栏开发:
    https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode
Logo

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

更多推荐