HarmonyOS 7 DualCart 平行视界适配实录 03:Navigation × Window:商品视频全屏横屏与双栏恢复【鸿蒙心迹】
前两期把 DualCart 的基础购物链路做稳定了。
01 解决的是 ProductList | ProductDetail 怎么成为一组稳定的双栏关系;02 又把 ProductBridge 这种过渡页从可见分栏里剥离掉,保证右栏切换商品时只替换详情,不污染左侧列表状态。
做到这里,普通购物浏览已经比较顺。
真正继续点“商品视频”以后,新的工程矛盾很明显:视频不适合被困在右侧 520vp 的详情栏里。
当前官方“平行视界”示例把“路由跳转过程请求全屏”和“路由跳转过程请求横屏显示”都列为典型场景。也就是说,大屏浏览虽然默认是双栏,但某些沉浸页面本来就应该暂时离开双栏。与此同时,HarmonyOS 当前窗口沉浸式和旋转能力也要求应用根据场景处理全屏、系统栏和方向变化。
这一篇就只处理这件事:
ProductList | ProductDetail
→ ProductVideo 全屏横屏
→ 退出视频
→ ProductList | ProductDetail
本轮统一数据:
taskId:
parallel_20261002_03
viewport:
824vp
before:
ProductList | ProductDetail
immersiveRoute:
ProductVideo
product:
product_1047 / MateBook Air
orientation:
LANDSCAPE
fullscreen:
true
leftScrollOffset:
960vp
enterCost:
53ms
exitCost:
47ms
after:
ProductList | ProductDetail
status:
RESTORED

一、不是所有关联页都应该继续占右栏
第二篇已经把路由分成:
PRIMARY
ASSOCIATED
TRANSITION
FULLSCREEN
现在 ProductVideo 正式进入 FULLSCREEN。
这一步很重要,因为商品详情本身是关联页:
ProductList
|
ProductDetail
但商品视频的内容目标完全不同。
详情页希望用户:
看规格
看价格
看评价
继续切商品
视频页希望用户:
把视觉注意力集中到播放内容
尽量减少周围 UI 干扰
必要时横屏
如果继续硬塞右栏,最终只能看到一块窄视频,既浪费大屏,也不符合沉浸内容的使用预期。
所以 DualCart 的规则不是:
进入平行视界后
所有后续页面都保持双栏
而是:
浏览型路由
→ 双栏
沉浸型路由
→ 临时全屏
退出沉浸路由
→ 恢复原双栏上下文
真正难的不是进入全屏,而是回来以后左右两栏必须和进入前一致。
二、进入全屏以前,先保存“成对状态快照”
这一篇新增 ImmersiveRouteCoordinator.ets。
进入视频页前不直接改 Window,而是先保存当前平行视界上下文:
export interface PairedStateSnapshot {
taskId: string
leftRoute: string
rightRoute: string
selectedProductId: string
categoryId: string
leftScrollOffsetVp: number
}
export class ParallelStateStore {
snapshot(): PairedStateSnapshot {
return {
taskId: 'parallel_20261002_03',
leftRoute: 'ProductList',
rightRoute: 'ProductDetail',
selectedProductId: 'product_1047',
categoryId: 'computer_office',
leftScrollOffsetVp: 960
}
}
}
这段代码解决的是“视频退出后只知道回 ProductDetail,却不知道左侧浏览到了哪里”的问题。
保存的不是组件实例,也不是 Window 对象。
真正需要恢复的是:
左栏路由
右栏路由
当前商品
分类
列表滚动位置
和 InsightBoard 那条多形态连载里一样,业务状态应该独立于 UI 容器生命周期。
三、全屏路由只通过 Coordinator 切换,不让视频页自己改双栏
第一版里,我把“隐藏双栏”“请求横屏”“打开视频页”都写进了 ProductVideoPage.aboutToAppear()。
看起来集中,实际很危险。
因为页面本身并不知道:
它从普通手机详情进来
还是从平行视界右栏进来
如果视频页无条件修改全局双栏状态,手机路径也会被一起影响。
所以现在由 ImmersiveRouteCoordinator 决定:
export interface ImmersiveWindowAdapter {
enterFullscreenLandscape(): Promise<void>
leaveFullscreenRestore(): Promise<void>
}
export class ImmersiveRouteCoordinator {
private saved:
PairedStateSnapshot | null = null
constructor(
private windowAdapter:
ImmersiveWindowAdapter
) {}
async enterProductVideo(
productId: string
): Promise<void> {
this.saved =
ParallelStateStore.shared()
.snapshot()
await this.windowAdapter
.enterFullscreenLandscape()
ProductRouteStack.shared()
.push('ProductVideo', {
productId,
source: 'parallel_detail'
})
}
async exitProductVideo():
Promise<void> {
await this.windowAdapter
.leaveFullscreenRestore()
if (this.saved !== null) {
await ParallelRouteManager.shared()
.restorePairedState(
this.saved
)
}
this.saved = null
}
}
这里的 ImmersiveWindowAdapter 是 DualCart 自己的适配层。
它内部再对接当前 HarmonyOS Window / 方向能力以及平行视界官方示例里的“请求全屏 / 请求横屏”配置。这样系统配置字段有变化时,业务路由不需要跟着改。
四、全屏和横屏不是同一个状态
实际测试时我一开始把两个状态合并成:
immersive = true
后来发现不够用。
可能存在:
全屏但仍保持竖屏
横屏但没有隐藏全部系统 UI
从横屏退出时窗口已经回来了,但方向还没恢复
所以现在显式拆开:
export interface ImmersiveState {
fullScreen: boolean
orientation:
'PORTRAIT' |
'LANDSCAPE' |
'AUTO'
route: string
}
export class ImmersiveStateStore {
private state:
ImmersiveState = {
fullScreen: false,
orientation: 'AUTO',
route: ''
}
enterVideo(): void {
this.state = {
fullScreen: true,
orientation: 'LANDSCAPE',
route: 'ProductVideo'
}
}
leaveVideo(): void {
this.state = {
fullScreen: false,
orientation: 'AUTO',
route: ''
}
}
}
这段代码解决的是“退出视频以后双栏恢复了,但页面还保持横屏语义”的问题。
窗口恢复和方向恢复都完成以后,状态才真正算退出沉浸模式。
五、ProductVideo 页只负责媒体,不再知道平行视界细节
现在视频页职责很单纯:
@Entry
@Component
struct ProductVideoPage {
@State
private productId: string =
'product_1047'
@State
private positionSec: number = 24
aboutToAppear(): void {
ProductVideoSession.shared()
.prepare(this.productId)
}
aboutToDisappear(): void {
ProductVideoSession.shared()
.pause()
}
build() {
Column() {
VideoSurface({
productId: this.productId,
positionSec: this.positionSec
})
}
.width('100%')
.height('100%')
}
}
页面里没有:
leftRoute
rightRoute
splitRatio
scrollOffset
这些全属于外部上下文。
这样 ProductVideoPage 以后从普通手机页进入,仍然可以复用,不会因为“它曾经属于平行视界”而带着一堆双栏判断。
六、视频退出时,恢复顺序比进入顺序更重要
我第一版退出顺序是:
恢复 ProductList | ProductDetail
→ 再退出全屏
→ 再恢复方向
视觉上会出现一瞬间:
横屏里的窄双栏
后来改成:
停止沉浸内容
→ 退出全屏
→ 方向恢复 AUTO
→ 等 Window 稳定
→ 恢复双栏 route
→ 恢复 selected / scroll
这样更干净。
当前测试数据:
enterCost=53ms
exitCost=47ms
这两个数字是 DualCart 当前真机 / 当前测试版本的工程观测,不是系统性能指标。
我把 exit 单独记,是因为真正容易卡顿的往往不是“进入视频”,而是“退出后两个 Pane 同时恢复”。
七、selectedProduct 和 leftScrollOffset 必须一起验证
进入视频前:
product_1047
MateBook Air
leftScrollOffset=960vp
退出以后如果右侧仍然是 MateBook Air,但左边列表回顶部,状态恢复仍然不完整。
因此恢复断言同时检查:
export class PairedRestoreAssert {
verify(
current: PairedStateSnapshot,
expected:
PairedStateSnapshot
): boolean {
return (
current.leftRoute ===
expected.leftRoute &&
current.rightRoute ===
expected.rightRoute &&
current.selectedProductId ===
expected.selectedProductId &&
current.leftScrollOffsetVp ===
expected.leftScrollOffsetVp
)
}
}
当前:
selectedPreserved=true
scrollPreserved=true
这比只判断“ProductDetail 回来了”更有意义。
八、DevEco 里要看到进入和退出两段日志
开发图:

当前 HiLog:
taskId=parallel_20261002_03
enter immersive ProductVideo
product=product_1047
request fullscreen=true
orientation=LANDSCAPE
save pairedState
ProductList | ProductDetail
scroll=960vp
exit immersive
restore:
ProductList | ProductDetail
selected=product_1047
scroll=960vp
enterCost=53ms
exitCost=47ms
status=RESTORED
如果日志只看到“exit video success”,还不能证明双栏恢复成功。
一定要同时看到:
right route
selected product
left scroll
九、运行图第一次把“进入前 / 全屏中 / 退出后”放在同一条链上
最终运行图:

这一轮三段状态非常清楚:
进入前:
ProductList | ProductDetail
沉浸中:
ProductVideo
LANDSCAPE
fullscreen=true
退出后:
ProductList | ProductDetail
product_1047
scroll=960vp
最终状态:
RESTORED
这才是 03 真正想解决的问题。
不是“视频能全屏”,而是“全屏以后还能准确回到原来的购物上下文”。
十、高清大图和商品视频为什么先共用同一个沉浸策略
商品详情还有“查看高清大图”。
从业务上看,它和视频不一样;从平行视界角度看,两者有同一个特征:
短时间离开双栏
要求最大展示面积
退出后回原商品
所以我没有再写一套 GalleryFullscreenManager。
当前统一走:
ImmersiveRouteCoordinator
区别只在:
route=ProductVideo
route=ProductGallery
媒体 Session 各自管理自己的资源。
这样“平行视界恢复”逻辑只有一套。
十一、当前官方资料给这条链路的边界很明确
HarmonyOS 官方 2026-08-29 的“平行视界”示例明确列出:
路由跳转过程请求全屏
路由跳转过程请求横屏显示
作为七个典型场景的一部分。
与此同时,当前 Window 沉浸式开发文档强调,需要根据设备屏幕特性、窗口和系统界面元素设计沉浸体验;旋转能力也有单独的窗口方向适配指导。
DualCart 这一篇没有伪造新的系统 API,而是把系统能力包在 ImmersiveWindowAdapter 下面,业务层只维护:
进入前状态
沉浸路由
退出后恢复
这样代码更稳定。
十二、下一篇:比价不是再开一个 Detail
03 解决了“暂时离开双栏”。
04 会做另一种相反需求:
不离开当前浏览
反而要同时看两个商品
也就是:
MateBook Air
VS
MatePad Pro
如果直接在主路由里再 push 一个 Detail,很容易污染原来的 ProductList | ProductDetail。
所以下一篇会接官方示例里的“虚拟容器”场景,用一个独立的比较上下文承载双详情,并在关闭比价以后恢复原来的列表 / 详情状态。
十三、真正容易错的是“返回键”和“视频页关闭”走了两条退出路径
做到这一轮时,我又碰到一个很典型的生命周期问题。
视频页里本来有自己的“关闭”按钮,它会调用 exitProductVideo();但系统返回键也能离开视频页。如果这两个入口最终没有走同一套收口逻辑,就会出现两种结果:
点页面关闭:
双栏恢复正常
按系统返回:
路由回来了
但横屏状态没恢复
所以我最后没有让页面按钮自己直接 pop 路由,而是统一发出一个“结束沉浸会话”的动作。
对业务来说,退出视频只有一种语义:
结束媒体展示
恢复 Window / Orientation
恢复成对路由
恢复商品与滚动位置
无论触发源是:
页面按钮
系统返回
播放结束
异常关闭
都进入同一个 Coordinator。
这样做以后,真正的退出条件就不会散落在页面事件里。
十四、进入全屏失败时,不能把已保存的双栏快照丢掉
进入沉浸模式并不是一定成功。
可能出现:
窗口状态正在变化
方向请求未完成
媒体初始化失败
页面路由打开失败
如果保存完 PairedStateSnapshot 以后,后续任意一步报错就把快照清掉,用户会回到一个“不知道该恢复什么”的状态。
现在 DualCart 的处理原则是:
保存快照
→ 进入沉浸流程
→ 全部成功后标记 ACTIVE
中途失败
→ 执行 rollback
→ 恢复原双栏
→ 最后再清快照
我把沉浸状态分成:
IDLE
ENTERING
ACTIVE
EXITING
ROLLING_BACK
这样日志里如果出现 ENTERING → ROLLING_BACK,说明问题发生在进入过程,不会和普通退出混为一谈。
本轮正常路径最终还是:
ENTERING
→ ACTIVE
→ EXITING
→ IDLE
十五、媒体资源释放和双栏恢复也不能抢顺序
视频播放时,播放器本身有一套生命周期。
如果先恢复双栏,再异步释放视频资源,短时间里会同时存在:
ProductList
ProductDetail
ProductVideo player
页面上看不出来,内存峰值却会明显抬高。
现在退出顺序固定为:
pause video
→ detach surface
→ release temporary player resources
→ restore orientation
→ leave fullscreen
→ restore ProductList | ProductDetail
真正需要长期保留的播放历史,只保存:
productId
positionSec
播放器实例不跨页面保存。
这和前面所有状态连续性原则是一致的:保存可重建的数据,不保存沉重的运行对象。
本轮视频停在:
00:24 / 02:18
如果以后需要“再次进入继续播放”,只恢复 24 秒的位置即可,不需要把播放器对象带回双栏。
十六、我给 03 做了六组验收,不只测一次正常进入退出
为了确认这条沉浸链路真的稳定,最后固定做了六组测试。
第一组,从 product_1047 / MateBook Air 进入视频,正常播放 24 秒后退出,要求恢复原商品和 960vp 滚动位置。
第二组,视频刚进入就按返回,验证 ENTERING 阶段也能正确回滚。
第三组,模拟视频资源加载失败,确认页面不会停在黑色全屏,也不会丢失原双栏。
第四组,连续进入 / 退出 10 次,检查方向状态最终都回 AUTO,没有一次残留 LANDSCAPE。
第五组,在视频期间切后台再回来,媒体会话可以暂停,但双栏快照不能被后台生命周期清掉。
第六组,从普通手机单栏进入同一个 ProductVideo,确认它不会误执行“恢复平行视界双栏”的逻辑。
这些测试完成以后,我才把本轮状态写成:
RESTORED
它代表的不是“页面回来”,而是:
Window 恢复
Orientation 恢复
Route 恢复
Product 恢复
Scroll 恢复
五项全部成立。
十七、这一期最重要的取舍,是不让沉浸页面知道自己来自哪种设备
从工程维护角度看,这其实比全屏本身更重要。
如果 ProductVideoPage 里出现:
if 折叠屏
if 平行视界
if 平板
if 手机
那这个页面很快就会变成设备判断中心。
现在设备和窗口差异全部停在外部 Coordinator。
ProductVideo 只知道:
我要播放 product_1047
当前 position=24s
页面可见 / 不可见
这意味着以后 DualCart 加 PC 或普通平板时,视频页仍然是同一份代码。
平行视界只是“如何进入和如何退出”的环境能力,不应该侵入视频业务本身。
这也是我认为 03 真正完成的标志。
十八、恢复完成以后再开放下一次沉浸请求
最后还有一个很小但很实用的保护。
用户退出视频的 47ms 内如果马上再次点击“播放视频”,旧版本可能同时出现:
上一轮正在恢复双栏
下一轮又开始请求全屏
于是 saved snapshot 会被第二次进入覆盖。
现在 Coordinator 增加一个简单状态门:
IDLE
才允许 enter
ENTERING / ACTIVE / EXITING / ROLLING_BACK
都拒绝重复 enter
页面按钮在非 IDLE 状态下也会暂时禁用。
这不是为了限制用户,而是保证每次沉浸会话都有唯一的进入快照和退出收口。对于这种跨 Window、Orientation、Navigation 的组合动作,宁可把状态机做明确,也不要依赖“用户不会点那么快”。
参考资料
- HarmonyOS 官方示例:平行视界:https://developer.huawei.com/consumer/cn/samples/
- Navigation 分栏开发:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode
- 窗口沉浸式:https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/immersive-window-feature
- Window Rotation:https://developer.huawei.com/consumer/en/doc/harmonyos-guides/rotation
更多推荐




所有评论(0)