首页 / 资讯中心 / 文章详情

HarmonyOS 7 Navigation:平行视界双栏路由仲裁与详情续接【鸿蒙心迹】

HarmonyOS 7 Navigation:平行视界双栏路由仲裁与详情续接【鸿蒙心迹】 ★ FEATURED ARTICLE
折叠屏上阅读一篇技术文档最怕的不是界面挤一点而是从展开态折回去以后明明刚才还停在第四节页面却退回列表或者右侧详情又打开了一份相同内容。这个问题很容易被误判成组件刷新。最初我也盯着build()和列表复用看了很久后来才发现屏幕形态变化本身并不应该改变“用户正在阅读什么”真正把现场弄乱的是业务路由与布局模式互相抢着做决定。这次用一个文档阅读 DemoParallelShelf做复盘。它不是把两个页面硬拼到一块而是把同一个阅读意图分别投射到窄屏单页、展开态双栏以及恢复过程中。我们固定一条可追踪的记录文档DOC-026-017标题《HarmonyOS 7 多形态交互设计指南》阅读锚点section-04偏移量1420vp展开态参考窗口宽824vp折叠态参考宽396vp路由代次23过期回调丢弃计数1。这些都是演示验收数据不代表某台真实设备的性能指标。一、先把异常还原成能重复的操作这个 Demo 首页有 38 篇文档左侧是分类与文档列表右侧是文档正文。用户在大屏上选中DOC-026-017滚动到第四节然后把折叠屏合上。预期结果很简单窄屏接着显示相同文档阅读位置不跳按返回键才回文档列表。实际容易出现三种偏差重新生成详情页导致锚点清零旧请求晚一步返回覆盖当前文档或者“切换单栏”被当作一次新的业务push结果返回栈里出现两份详情。我把这三种偏差写成同一条复现用例而不是分别修补 UI。因为它们的共同根源是系统负责窗口与可见布局业务负责选中文档、页面意图和阅读进度。只要把“布局变化”当作“路由变化”迟早会在转屏、分屏、悬停甚至窗口大小连续抖动时再碰到类似情况。平行视界和普通响应式布局也需要分开看。前者偏重系统级应用内分栏体验后者可以由 ArkUINavigation的Auto模式以及业务断点适配实现。两种机制在适用设备、配置和交互上有差异本文主要处理它们共同依赖的业务状态协议不把自定义双栏写法冒充某个系统专有接口也不把历史版本easygo.json样例直接套进 HarmonyOS 7 项目。二、先定义“阅读现场”再定义页面原来状态散在三个地方列表页保存选中 ID详情页持有滚动偏移导航栈记录页面实例。页面单独看都没错可窗口一变化三个状态谁先更新就变成运气。调整后我让业务层维护一份可序列化的ReadingSnapshot。页面可以重建阅读现场不能因为重建而丢失。// model/ReadingSnapshot.ets export interface ReadingSnapshot { docId: string; anchorId: string; scrollVp: number; routeEpoch: number; layout: COMPACT | EXPANDED; updatedAt: number; } export const INITIAL_READING: ReadingSnapshot { docId: DOC-026-017, anchorId: section-04, scrollVp: 1420, routeEpoch: 23, layout: EXPANDED, updatedAt: 0 };这里最重要的字段不是宽度而是routeEpoch。业务的文档选择动作会推动路由代次单纯折叠、展开、调整窗口宽度不需要创造新业务意图。anchorId是语义位置scrollVp是局部视觉位置。两者保留是因为文档字体、宽度或图片加载完成之后同一个像素偏移未必还是原段落。恢复顺序应优先定位锚点再在相同布局条件下尝试还原局部偏移而不是永远盲目scrollTo(1420)。持久化时不建议整个导航对象都序列化更不要保存组件实例、Scroller或异步回调。把必要的基础字段写到应用自有存储启动时校验文档是否还存在、锚点是否有效失效时回退到第一节并留下恢复日志。示例中的updatedAt用于调试时间先后不用于证明请求在网络上天然有序。三、路由仲裁只承认两种输入真正重构的是RouteArbiter。它不关心左右栏具体怎么画只处理“用户选择文档”和“窗口投影变化”。前者可能需要发起详情加载、推进代次后者只更新布局标签绝不偷偷追加历史记录。如此一来即使窗口宽度在折叠边界附近快速抖动也不会触发多轮重复导航。// services/RouteArbiter.ets import { ReadingSnapshot } from ../model/ReadingSnapshot; export class RouteArbiter { private state: ReadingSnapshot; private staleDiscard: number 0; constructor(initial: ReadingSnapshot) { this.state { ...initial }; } selectDoc(docId: string): number { if (docId this.state.docId) { return this.state.routeEpoch; } this.state.docId docId; this.state.anchorId section-01; this.state.scrollVp 0; this.state.routeEpoch 1; return this.state.routeEpoch; } projectWindow(widthVp: number): void { this.state.layout widthVp 600 ? EXPANDED : COMPACT; } acceptDetail(epoch: number): boolean { if (epoch ! this.state.routeEpoch) { this.staleDiscard 1; return false; } return true; } getSnapshot(): ReadingSnapshot { return { ...this.state }; } getDiscardCount(): number { return this.staleDiscard; } }600vp只是这个阅读场景的工程断点不是 HarmonyOS 平行视界的通用强制阈值实际产品要结合字体等级、窗口可用空间、分栏策略和设计规范调整。这里故意不在projectWindow()内执行pushPathByName()目的就是强制隔离“可见形式”和“用户历史”。对于相同文档的重复点击仲裁器直接返回原代次防止双击造成重复详情。还有一个容易忽略的竞态用户在 A 文档的请求尚未返回时选了 B稍后 A 的图片或目录接口完成。如果页面只看“这是一次合法响应”就可能把 A 覆盖到 B 上。现在请求发出时携带epoch落地前用acceptDetail()比对。我们这次演示中人为制造了一次旧代次响应计数应显示staleDiscard1它不应再触发滚动恢复或者覆盖右侧内容。四、ArkUI 页面只消费状态不替用户作路由决定UI 侧使用Navigation和NavPathStack管理页面展示。文档列表和详情的数据来源统一指向仲裁后的当前阅读快照。下面保留最有关系的骨架业务仓库内还需要为路由名称注册NavDestinationbuilder、实现数据加载和异常页不能只复制这段就期待工程直接运行。// pages/ShelfHomePage.ets关键结构示意 Entry Component struct ShelfHomePage { private navStack: NavPathStack new NavPathStack(); State selectedDocId: string DOC-026-017; State layoutMode: string EXPANDED; Builder pageBuilder(name: string) { if (name ReadingDetailPage) { NavDestination() { ReadingDetailPage() } .title(文档详情) } } build() { Navigation(this.navStack) { Column() { Text(ParallelShelf).fontSize(22).fontWeight(FontWeight.Bold) Text(已选文档 ${this.selectedDocId}) Button(打开详情).onClick(() { this.navStack.pushPathByName(ReadingDetailPage, { docId: this.selectedDocId, routeEpoch: 23 }); }) }.width(100%) } .navDestination(this.pageBuilder) .mode(NavigationMode.Auto) } }这里的按钮表示真实的用户打开行为不是窗口变化回调。NavigationMode.Auto使容器能够根据布局空间适配展示但它并不替业务保存文章 ID、阅读锚点或迟到请求代次。进入详情时应从参数中读取docId和routeEpoch并在装载数据前与仲裁器核对。实际工程要把页栈、选中项以及详情组件的状态绑定做好避免示例里固定演示值长期硬编码。当详情已经处于可见状态时折叠回窄屏需要做的是恢复“当前目的地”不是再push一层。如果 UI 层为了响应布局变化重新构造页面页面自身应把可恢复状态从仓库取回来。这种约束比让某个组件永不销毁更稳因为后台回收、语言切换等情况同样可能导致 UI 对象重新创建。五、窗口事件与阅读事件要分日志通道窗口宽度从824vp变化为396vp时最先看到的通常是windowSizeChange。它可能与生命周期、动画、系统栏变化同时发生不能把每次变化都解释为一次折叠动作。业务上只需要一个“当前宽度投影”结果。边界连续触发时可以节流但节流属于 UI 性能优化不等于把中间业务选择请求删掉。// EntryAbility.ets窗口尺寸采样示意 import { UIAbility } from kit.AbilityKit; import { window } from kit.ArkUI; export default class EntryAbility extends UIAbility { private mainWindow?: window.Window; onWindowStageCreate(stage: window.WindowStage): void { stage.getMainWindow().then((win: window.Window) { this.mainWindow win; const current win.getWindowProperties().windowRect; AppStorage.setOrCreate(shelfWidthPx, current.width); win.on(windowSizeChange, (size: window.Size) { AppStorage.setOrCreate(shelfWidthPx, size.width); console.info([ParallelShelf] layout widthPx${size.width}); }); }).catch((err: Error) { console.error([ParallelShelf] obtain window failed ${err.message}); }); stage.loadContent(pages/ShelfHomePage); } onWindowStageDestroy(): void { this.mainWindow?.off(windowSizeChange); this.mainWindow undefined; } }必须强调windowSizeChange提供的尺寸与 UI 布局里谈论的vp不一定是相同单位。进入仲裁器前要按当前窗口密度做明确转换并且打印转换前后的单位如果把原始像素直接当vp断点会在高密度设备上提前触发。实际项目可以结合px2vp()和 UI 上下文完成转换并在首帧就初始化窗口宽度不要等第一次变化事件来了才补状态。日志我分成三条线layout记录大小及投影route记录用户意图及代次restore记录锚点与偏移的恢复结果。这样看到页面跳转时能追问这是用户选择引发的还是布局变化误触发的如果发现重复push马上检查它是否错误地写在窗口监听器中通常比在组件树里盲找要省时间。六、把“恢复成功”改成可以核验的状态对于一篇长文档我把恢复过程划成SNAPSHOT_READY → TARGET_RESOLVED → ANCHOR_APPLIED → RESTORED四步。TARGET_RESOLVED表示详情内容已经装载完成ANCHOR_APPLIED表示第四节锚点已经找到RESTORED才表示用户看到的阅读现场稳定。单纯页面aboutToAppear()被调用并不能当作恢复成功否则异步图片加载完成后再次改变排版的问题就会漏掉。本轮约定的验收快照是DOC-026-017 / section-04 / 1420vp / routeEpoch23。展开态右侧详情使用它折叠态单页仍使用它。发生一次迟到响应时只有staleDiscard从 0 变为 1文档 ID 和代次都不回退。下图 03 展示窄屏中的阅读内容不是把大屏机械裁剪成一半导航、页内标题、段落宽度需要适配所读章节和历史上下文仍然一致。也不能把1420vp当作所有布局下的精确绝对位置。若展开态两栏每行容纳更多文字折叠态一栏每行文字减少页面总高度变了优先保持section-04的内容定位局部偏移只能作为同排版条件下的候选值。用户感知的“我还在第四节”比机械保持同一滚动数值更重要。字体大小调节、图片异步解码和深色模式切换都要按这个原则处理。七、详情诊断页记录的是因果链不是漂亮数字我专门加了一页RouteDebugPage用于把当前快照打印成对人有意义的字段。页面显示824vp → 396vp的最近变化routeEpoch23锚点section-04历史偏移1420vpstaleDiscard1恢复结果SUCCESS目标滚动位置618vp。这不是展示一个“成功”绿色标签就结束关键是让开发者和测试能判断成功对应哪个文档、哪个路由代次、哪个锚点。异常用例要更严格连续点击相同文档三次栈里只能留一次有效详情先选文档 A 再迅速选 BA 的迟到结果不允许覆盖 B折叠与展开交替两轮routeEpoch不应凭空增长后台恢复时没有快照则安全回列表本地缓存发现文档版本变化则重新定位锚点并提示部分阅读位置可能变化。最后这条对知识库类产品很重要因为在线文档内容更新比屏幕变化更容易让原像素偏移失效。我还会看“返回行为”。展开态的返回可能只是关闭右侧详情或回到上一级内容折叠态则要退出详情到列表。两种可见形式允许不同但不能让用户按一次返回产生两次历史回退。判定应基于业务历史深度与当前展示策略而不是简单认为宽屏返回键永远隐藏右栏。若产品采用系统平行视界推挤或覆盖模式还需要对照系统实际的全局返回规则验收。八、容易藏在实现细节里的三个边界第一routeEpoch不是线程安全锁更不是自动取消网络请求。它只保证“结果是否有资格写入当前视图”真正高成本的加载任务仍应适时取消或复用。旧结果可以进只读缓存但不能修改当前文档的选中态。第二UI 的State刷新有自己的时机仓库对象原地修改未必触发你期望的更新发布快照时要用新对象或明确的状态容器通知。第三缓存恢复涉及数据过期与隐私登录账户切换时不能把前一个账户的阅读位置误带到新账户。此外还要留意半屏浮窗、分屏拖拽与键盘弹出。它们都可能改变可用宽度却不代表物理折叠事件。用“宽度断点”控制显示模式时测试矩阵应包含这些非折叠路径。页面内如果存在编辑操作草稿的同步机制要独立于阅读位置不应因一次 layout 投影覆盖输入中的文字。真正的系统级平行视界配置也要遵循对应版本文档不能拿 ArkUI 的单/双栏截图反向证明系统模式已经生效。八点一、异步阅读恢复最好按阶段提交前面讲了业务快照但仍有一个值得单独处理的细节详情页通常不是一次渲染完成。目录接口先返回正文随后返回图片按需加载长文还可能有代码高亮和折叠块。在这种环境里恢复过程不能简单写成“页面出现后立刻滚到 1420vp”。如果页面正文刚到一半就执行滚动后续内容扩容又把位置推走测试看到的是偶现的跳屏。我的做法是把恢复任务绑定到docId routeEpoch contentRevision三个字段缺一不可。docId标识文章routeEpoch标识用户选中意图contentRevision标识当前内容版本。只有这三项都没有变化才允许已经排队的锚点定位继续执行。图片没有完全解码时可以先显示定位占位区域等关键节点尺寸稳定后再发起一次受控的偏移校正但不能因为每张图片完成都重复滚动。这里需要设置明确的“最多校正次数”否则一个动态内容页可能永远处在恢复中。在诊断日志里我会为每个阶段记录snapshotAccepted、contentMounted、anchorResolved和viewportSettled。最后一个阶段通过后才把状态改为RESTORED。如果三秒后内容仍未稳定不要让用户对着永远转动的进度圈而应保留正文显示给出“已定位到章节精确偏移待内容加载完成”的可解释退化状态。这个设计的价值是把“数据到了”“锚点找到了”“视觉恢复完成”三个不同事实拆开出问题时能定位具体环节。八点二、并发的正确性必须由压力路径验证单次折叠顺利不足以证明路由仲裁成立。我补了一组压力路线列表中快速选择 A、B、C 三篇文档网络层人为延迟 A 的响应再连续改变窗口大小。当 C 成为最新业务意图时A 和 B 的迟到响应即使返回成功也只能写入与当前视图隔离的缓存。此时页面顶部标题、目录高亮和滚动位置都应属于 C而不是出现“标题 C、正文 B”的混合状态。对于正文中有媒体内容的场景还要检查旧播放器是否停止、旧监听是否解除。第二类压力是从599vp拖到601vp再立刻回到598vp。如果每跨过一次阈值都重新建栈即使正常折叠没问题浮窗拖拽也会暴露重复详情。我会对断点引入适当的迟滞区间宽度明显超过阈值后才稳定进入大屏投影反方向同理。但迟滞只影响 UI 投影不得改动业务的routeEpoch。这点要在代码评审时写成约束否则后续同事很容易把优化节流逻辑复用到用户点击事件上。最后是持久化的原子性保存docId与anchorId必须来自同一份快照不建议分两次无版本约束地写入。异常退出如果只写入新文档 ID 却保留旧章节锚点重启后会产生“存在但不属于本文”的定位失败。解决方法是把完整快照作为一个记录提交附带版本号读取时先校验格式和文档修订再决定恢复或回退。这类测试看起来费劲却比线上用户反馈“偶尔跳章节”后再补日志可靠得多。九、这轮落地后的检查清单我最终把验收收成五个可读结果演示数据中的当前文档始终为DOC-026-017布局变化从824vp投影到396vp业务路由代次保持23旧响应只有一次被丢弃计数为1锚点恢复到section-04详情诊断状态为RESTORED。这些数字是为复现与截图统一而设置的样例真实项目需要跑设备矩阵、导出日志和录屏后才能写成实测结论。这次调整看似没有增加多少新功能价值却在于把页面的生命周期和用户的阅读意图拆开了。系统可以重排窗口ArkUI 可以更新导航展示业务数据可以重新加载但它们不应一起争夺“此刻用户在看什么”的最终解释权。将来换成商品详情、邮件阅读或课程目录思路仍然成立先定业务快照和因果代次再让不同设备形态使用它。参考资料以实际所用 SDK 版本接口为准HarmonyOS 7 平行视界能力介绍与开发实践https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-easygo-parallelArkUI Navigation 官方开发指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-navigation-navigation-V5多窗口布局适配https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/multi-window-layout-adapt-V14说明本文代码用于解释方案与关键边界配图为按同一 Demo 数据制作的仿真开发界面并非 DevEco Studio 真机取证截图集成时需按 SDK 实际接口与设备能力进行编译、调试与实测。
阅读完成 · 觉得有帮助?
咨询建站