HarmonyOS 7 TwinShelf 平行视界工程实录 03:Transition Page × Fullscreen:过渡页、沉浸式全屏与路由状态回收【鸿蒙心迹】
前两篇把 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
更多推荐




所有评论(0)