HarmonyOS 7 ArkUI Navigation + WindowStage:折叠屏窗口宽度驱动的单双栏切换与草稿状态保持【鸿蒙心迹】
这篇不从“折叠屏有几种形态”开始而是从一个更具体的问题切入同一个笔记页面在 420vp 的窄窗口里应该是单栏在 840vp 的展开窗口里应该变成列表 详情双栏真正麻烦的是布局切换以后用户正在编辑的草稿不能丢当前选中的笔记也不能跳回首页。我给 Demo 起名叫FoldNoteDesk。这次固定一组运行状态方便正文、日志和图片保持一致Window420vp → 840vpModeSTACK → SPLITNote IDnote_20261001_07DraftDIRTYUnsaved126 charsBreakpointCOMPACT → EXPANDED官方当前的多设备适配指南强调折叠屏在展开、折叠以及窗口尺寸变化过程中应用需要保证布局适配和状态连续ArkUI 的 Navigation 也提供单栏、分栏和自适应模式。对我这个笔记场景来说真正值得处理的是“窗口变化驱动布局”而不是只判断设备是不是折叠屏。一、我没有直接监听折叠状态而是先监听窗口宽度最早我也想过直接拿折叠状态做判断展开就双栏折叠就单栏。但实际项目里很快就会遇到自由窗口、分屏、横竖屏这些情况。设备即使处在展开态应用窗口本身也可能很窄反过来某些宽屏设备根本不是折叠屏也同样适合双栏。所以最后我把布局判断依据改成了窗口宽度。这段代码解决的问题是在应用启动和后续窗口变化时持续得到当前可用窗口宽度并把结果写进全局断点状态。import{UIAbility}fromkit.AbilityKit;import{display,window}fromkit.ArkUI;exportdefaultclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.getMainWindow().then((windowObj){this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width);windowObj.on(windowSizeChange,(size){this.updateBreakpoint(size.width);});});}privateupdateBreakpoint(widthPx:number):void{constdensitydisplay.getDefaultDisplaySync().densityPixels;constwidthVpwidthPx/density;constbreakpointwidthVp600?EXPANDED:COMPACT;AppStorage.setOrCreate(windowBreakpoint,breakpoint);AppStorage.setOrCreate(windowWidthVp,Math.round(widthVp));}}这里的 600vp 是当前 Demo 的业务断点不是系统强制值。它的作用只是表达我的笔记应用在小于 600vp 时列表和详情挤在一起会影响编辑超过这个宽度以后双栏开始有实际收益。实际项目里断点应该根据内容密度、字号、导航结构重新验收而不是所有页面共用一个数字。官方开发资料也推荐在响应式布局里根据窗口宽度划分断点并通过windowSizeChange处理动态变化。这个思路比“检测设备型号”更适合长期维护。二、布局切换不能顺手把业务状态也重建做到这里以后我遇到第二个问题。最初页面里写的是if(this.breakpointEXPANDED){this.buildSplitPage()}else{this.buildStackPage()}看上去没问题但列表、详情、编辑器实际上被当成了两套 UI 树。一旦状态绑定做得不干净窗口一变化就可能出现详情页被重新创建当前 Note ID 回到默认值编辑中的 TextArea 重新取服务器旧内容草稿DIRTY状态被重置。所以我后面改成布局只决定 Navigation 的显示模式不决定当前业务数据是谁。这段代码解决的是“同一份选中状态在单双栏之间复用”。EntryComponentstruct WorkspacePage{privatenavPathStack:NavPathStacknewNavPathStack();StorageLink(windowBreakpoint)breakpoint:stringCOMPACT;StorageLink(selectedNoteId)selectedNoteId:stringnote_20261001_07;StorageLink(draftContent)draftContent:string;privategetNavigationMode():NavigationMode{returnthis.breakpointEXPANDED?NavigationMode.Split:NavigationMode.Stack;}build(){Navigation(this.navPathStack){NoteList({selectedNoteId:this.selectedNoteId})}.mode(this.getNavigationMode())}}这一层最重要的变化是selectedNoteId和draftContent不再属于某个“单栏组件”或“双栏组件”。它们属于 Workspace 业务本身。页面从 420vp 切到 840vpUI 可以重排数据不能因为重排就换一份。三、草稿状态我单独做了一层不让 TextArea 成为唯一真相接下来是最容易被忽略的一点。如果用户正在输入 126 个未保存字符此时设备展开窗口触发重排TextArea 组件自己的内部状态并不应该承担“草稿持久化”责任。所以我给FoldNoteDesk加了一个很薄的DraftStore。当前代码解决的是输入变化以后立刻更新业务草稿同时标记当前笔记为脏数据。exportclassDraftStore{privatestaticdrafts:Mapstring,stringnewMap();staticsave(noteId:string,content:string):void{this.drafts.set(noteId,content);}staticread(noteId:string):string|undefined{returnthis.drafts.get(noteId);}staticremove(noteId:string):void{this.drafts.delete(noteId);}}页面里再把编辑动作接进来privateonDraftChanged(value:string):void{this.draftContentvalue;this.isDirtytrue;DraftStore.save(this.selectedNoteId,value);AppStorage.setOrCreate(draftState,DIRTY);}这层 Store 在当前 Demo 里只是进程内缓存。它能解决窗口重排、组件重建导致的草稿丢失但不能解决应用被系统终止以后恢复的问题。正式笔记应用需要继续把草稿落到 Preferences、数据库或者文件层。这里我没有为了文章看起来“完整”硬把所有持久化方案全塞进一个 Demo。四、420vp 切到 840vp 时我只做三件事真正跑起来以后窗口从 420vp 变成 840vp我不再重新初始化页面而是只做更新windowWidthVp更新windowBreakpoint让 Navigation 根据状态切换模式HiLog 里最终固定成Window: 420vp - 840vp Mode: STACK - SPLIT Selected: note_20261001_07 Draft restored: 126 chars这一张 DevEco 图里可以看到几个关键点。左侧项目不是一个只有Index.ets的空 Demo而是单独拆了common/ BreakpointStore.ets model/ DraftStore.ets pages/ WorkspacePage.ets中间代码监听windowSizeChange右侧展开屏已经进入双栏底部日志则证明状态没有因为窗口变化而回到默认值。如果日志里Mode对了Selected却变了我会先查状态归属不会继续调布局参数。五、为什么我没有把所有逻辑交给 NavigationMode.AutoNavigation 本身支持自适应模式这个能力很实用。但当前 Demo 仍然手动维护COMPACT / EXPANDED原因不是 Auto 不够好而是我的业务还有额外状态需要跟随断点变化。例如小窗口隐藏详情辅助工具栏大窗口固定显示最近笔记列表600vp 以上展示项目目录840vp 以上未来可能增加第三块辅助信息。也就是说Navigation 的 mode 只是其中一个消费者。断点状态还会被其他组件使用。如果一个项目只是简单的导航单栏 / 分栏没有这些业务差异直接使用 Auto 会更省事如果页面同时有多块内容随窗口变化保留一个业务断点层会更清楚。六、窗口变化时不要顺便重新拉一遍网络数据这个问题我在旧项目里遇到过很多次。有些页面把网络加载写在aboutToAppear()里又因为布局切换造成页面局部重新创建结果窗口变化一次详情接口跟着又请求一次。这对笔记编辑尤其危险。服务器回来的旧正文有可能覆盖本地还没提交的 126 个字符。所以当前 Demo 里草稿优先级明确高于远端正文privaterestoreNote(noteId:string):void{constlocalDraftDraftStore.read(noteId);if(localDraft!undefined){this.draftContentlocalDraft;this.isDirtytrue;return;}this.loadRemoteNote(noteId);}这段代码解决的不是“离线同步”而是一个更小的问题如果本地已经有未提交草稿就不要因为窗口重排重新拿旧数据覆盖它。正式项目里还需要版本号、更新时间、冲突处理这些属于同步层逻辑。七、最终运行结果看的是“连续”不是“双栏成功”最后手机运行图固定在 840vp 的展开结果。当前状态是Window840vpModeSPLITBreakpointEXPANDEDDraftDIRTYUnsaved126 charsnote_20261001_07仍然是“项目会议纪要”没有因为展开切回列表首页。这才是我这次适配真正想验证的东西。如果只看截图双栏谁都能搭出来。真正进入实际使用以后用户会在折叠、展开、旋转、分屏之间来回切。每次变化都把输入内容清空一次这种适配即使视觉上再漂亮也很难算完成。官方对折叠屏体验的核心要求里也强调状态连续。我的理解是布局可以根据窗口变化任务不能因为布局变化被打断。八、这次留下的不是一个“折叠屏专用页面”写完以后我反而把FoldNoteDesk里的“折叠屏判断”删得越来越少。最后真正保留下来的是三层关系WindowStage 提供窗口变化 → Breakpoint 解释当前空间 → Navigation 和业务组件根据断点重排。而笔记 ID、草稿、编辑状态不属于布局层。它们应该独立存在。这种结构的好处是未来这个页面放到平板、自由窗口、桌面窗口里仍然可以继续工作。如果后续要扩展我会继续补两块第一是把 DraftStore 换成真正的持久化草稿层。第二是给断点切换加 UI 自动化验证专门检查“窗口尺寸变化后 selectedNoteId 和 draft 长度不变”。相比写一个“适配折叠屏”的 if/else这两件事更像实际工程里应该继续做的工作。参考资料HarmonyOS 多设备通用适配指南https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/Navigation 分栏开发https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode折叠屏设计原则https://developer.huawei.com/consumer/cn/doc/doccenter-ux-design/design-principles-0000001957023989
上一篇/下一篇内容由系统自动关联
返回资讯列表 →