ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 7 ArkUI + FoldSplitContainer:折叠屏断点布局与形态切换状态保留【鸿蒙心迹】

HarmonyOS 7 ArkUI + FoldSplitContainer:折叠屏断点布局与形态切换状态保留【鸿蒙心迹】 折叠屏适配最容易做成“把页面拉宽”。真正难的是设备从折叠到展开、再从展开回折叠时页面结构可以变化但用户正在编辑的内容、选中的文档和操作位置不能跟着丢。一、这次我先做的不是双栏而是“让草稿无论怎么折都还在”我给这个 Demo 起名FoldNote是一个很普通的笔记编辑器左边笔记列表右边编辑区域。在普通直板手机上它就是典型的单页编辑器展开到大尺寸折叠屏以后最自然的形态是左侧列表、右侧详情。UI 本身并不难真正麻烦的是第一次做形态切换时我把“折叠态页面”和“展开态页面”写成了两个分支。效果看着没问题用户在展开态编辑一半把设备合起来页面切成单栏草稿却回到了上一次保存的版本。问题不是折叠屏也不是 ArkUI 没有自适应能力而是我的状态归属错了。我当时让两个布局分支分别维护自己的State draftContent。窗口宽度改变后组件树发生变化新分支重新创建编辑态自然跟着重新初始化。这类问题很典型开发者一开始关注的是“折叠后显示几栏”而用户真正关心的是“我刚才写的字还在不在”。所以第二版我把目标改成了三个明确指标折叠态宽度412 vp时显示单栏编辑区展开态宽度872 vp时显示左360 vp、右512 vp的双栏工作区当前文档始终是layout-note-17草稿长度286 字无论形态如何切换都保持不变。到这里布局和状态才算被拆开。二、断点只决定“怎么排”不应该决定“数据是什么”HarmonyOS 的多设备适配指南一直强调按窗口尺寸和设备形态做响应式布局。对我来说最重要的理解不是记住某一个断点数字而是把窗口环境看成输入。窗口从窄变宽应该改变的是页面是一栏还是两栏左右区域占多少工具栏放哪里哪些辅助信息需要展开。不应该跟着改变的是当前文档 ID用户正在编辑的正文光标位置未保存标记业务请求结果。所以我把设备布局归一成两个业务层可理解的模式export enum WorkspaceMode { COMPACT COMPACT, EXPANDED EXPANDED } export class BreakpointUtil { static resolve(widthVp: number): WorkspaceMode { return widthVp 600 ? WorkspaceMode.COMPACT : WorkspaceMode.EXPANDED } }这段代码解决什么问题把“窗口宽度”转换成稳定的布局语义避免页面到处判断具体数字。这里的600是 FoldNote Demo 自己的断点不是系统规定的唯一值。实际项目应该根据内容密度、设计稿和目标设备验证。比如邮件、笔记和图片编辑器即使面对同一个 700 vp 窗口也可能选择完全不同的布局。我不太建议在十几个组件里分别写if (width 600)。今天只有两个断点还好后面一旦加入平板、自由窗口、横屏再想修改就会非常痛苦。断点工具输出的是COMPACT / EXPANDED页面只消费模式。这样设备策略和页面结构之间就有了一层隔离。三、FoldSplitContainer 解决分区状态保留仍然要自己负责FoldSplitContainer是 ArkUI 为折叠屏分区场景提供的组件可以定义 primary、secondary、extra 区域并配置展开态、悬停态和折叠态的布局信息。它擅长的是“区域怎么摆”不是替你保存业务草稿。这正好符合我想要的职责分工。在 FoldNote 里主要区域是编辑器次要区域是文档列表扩展区域暂时不使用。不同设备状态下容器可以调整区域形态但编辑内容来自同一个状态仓库。这段代码解决什么问题让布局容器负责分区而当前文档和草稿来自统一状态源。import { FoldSplitContainer, ExpandedRegionLayoutOptions, HoverModeRegionLayoutOptions, FoldedRegionLayoutOptions, PresetSplitRatio } from kit.ArkUI Entry Component struct AdaptiveWorkspace { StorageLink(currentDocId) currentDocId: string layout-note-17 StorageLink(draftContent) draftContent: string private expandedOptions: ExpandedRegionLayoutOptions { horizontalSplitRatio: PresetSplitRatio.LAYOUT_2V3 } private hoverOptions: HoverModeRegionLayoutOptions { showExtraRegion: false, horizontalSplitRatio: PresetSplitRatio.LAYOUT_1V1 } private foldedOptions: FoldedRegionLayoutOptions { verticalSplitRatio: PresetSplitRatio.LAYOUT_1V1 } Builder primaryArea() { EditorPage({ docId: this.currentDocId, content: this.draftContent }) } Builder secondaryArea() { NoteListPage({ selectedId: this.currentDocId }) } build() { FoldSplitContainer({ primary: () this.primaryArea(), secondary: () this.secondaryArea(), expandedLayoutOptions: this.expandedOptions, hoverModeLayoutOptions: this.hoverOptions, foldedLayoutOptions: this.foldedOptions, animationOptions: { duration: 220 } }) } }这里我故意没有把“草稿内容”写进EditorPage自己的私有State。如果编辑器组件因为布局切换重新创建它仍然能从上层状态恢复。真实项目里StorageLink只是其中一种思路。更复杂的应用可以用状态管理 V2、应用级 Store、ViewModel 或自己的数据层。关键不是装饰器名称而是业务状态必须高于可被替换的布局子树。图二是 FoldNote 展开态的调试画面DevEco Studio 左侧能看到Breakpoint.ets、WorkspaceState.ets、AdaptiveWorkspace.ets右侧折叠设备模拟器当前宽度872 vp界面已经是双栏HiLog 里同时记录layout-note-17和draftChars286。我做这张图时特意把“展开态双栏”和“状态保留”同时标出来因为只截一个漂亮双栏页面根本证明不了形态切换是否真的稳定。四、监听窗口变化时最怕把“布局更新”和“业务重置”写在同一个回调里第一次实现的时候我把所有事情都塞进窗口尺寸变化回调重新算宽度改双栏重新读取文档重建编辑器把滚动位置归零。这其实是在用一个环境事件触发业务初始化。窗口变化可能来自折叠展开、横竖屏、自由窗口变化、分屏等多种场景。它的语义只是“可用空间改变了”并不代表用户切换了文档更不代表草稿应该重新从数据库加载。后来我把处理逻辑分成两类。windowSizeChange只做布局层信息更新业务状态恢复只在页面真正首次创建、文档 ID 变化或者进程恢复时做。示意代码如下。这段代码解决什么问题窗口尺寸变化只刷新布局不重新初始化当前文档。import { window } from kit.ArkUI import { hilog } from kit.PerformanceAnalysisKit const DOMAIN 0x0000 const TAG FoldNote export class WindowLayoutController { private mainWindow?: window.Window async bind(stage: window.WindowStage, onModeChange: (width: number, mode: WorkspaceMode) void): Promisevoid { this.mainWindow stage.getMainWindowSync() this.mainWindow.on(windowSizeChange, (size: window.Size) { const widthVp size.width const mode BreakpointUtil.resolve(widthVp) hilog.info( DOMAIN, TAG, windowSizeChange width${widthVp}, mode${mode} ) onModeChange(widthVp, mode) }) } unbind(): void { this.mainWindow?.off(windowSizeChange) } }这段代码只把环境变化转成布局模式至于layout-note-17的草稿是什么它完全不碰。这种分离看起来只是代码洁癖但它直接决定折叠屏形态切换是不是稳定。因为尺寸变化属于高频环境事件如果每次都顺便重建业务状态迟早会碰到内容闪烁、重复请求、光标跳动和数据覆盖。五、折叠态不是“展开态缩小版”信息层级应该真的变化折叠到412 vp后FoldNote 不再保留左侧笔记列表。编辑器占满主要区域用户可以继续写当前文档。这里有一个常见误区为了追求“一套布局”把双栏硬压到窄屏上。结果左边列表只剩一条窄缝右边编辑区也不够用。响应式布局真正要做的是在不同空间里改变信息优先级。窄屏时我把“当前编辑任务”放在第一优先级文档列表通过返回按钮或导航进入展开以后才把“列表 详情”同时展示。这种变化不会让用户觉得功能少了因为核心任务连续。图三就是折叠后的单栏状态。顶部状态栏时间是 10:41当前模式明确显示COMPACT窗口宽度412 vp当前文档仍然是layout-note-17草稿字符仍然是286状态为PRESERVED。我很喜欢把这些诊断信息直接做进内部测试页。它比“肉眼看起来没丢”更可靠因为测试人员可以在折叠、展开、旋转、切后台以后快速确认关键状态有没有变化。六、展开以后恢复双栏不能重新创建一份“看起来一样”的草稿从 412 vp 再展开到 872 vp页面会恢复双栏。这里最容易出现一种隐蔽 Bug右侧编辑器显示的内容和之前一样看起来没有丢但它其实是从持久化数据重新读出来的用户刚输入但还没保存的几个字已经消失。所以我判断“状态保留成功”时不只对比文档 ID还对比未保存草稿长度和 dirty 状态。FoldNote 的测试数据里切换前后草稿一直是286 字。为了验证不是碰巧一致我实际测试时还会在折叠前追加一段未保存文本再展开检查。另外列表选择状态也要保持。展开以后左侧列表必须继续高亮layout-note-17不能默认跳回第一项。这类“视觉上差不多”的问题如果没有状态字段很难在回归测试里抓到。七、滚动位置、光标和输入法才是第二阶段最容易漏掉的状态草稿不丢只是第一层。编辑器真正投入使用以后还会出现更细的连续性问题用户正在第 20 段编辑展开以后滚动回顶部光标原来在一句话中间切换后跑到文末选区丢失输入法弹出时窗口变化又触发了一次布局更新右侧编辑器宽度改变后文本重新换行导致可视位置漂移。这些状态不能全部简单塞到持久化数据库里。更合适的做法是分层。文档内容属于业务数据应该稳定保存当前选中文档属于工作区状态滚动位置和光标属于页面会话状态窗口宽度则只是环境状态。我会把会话快照单独建一个模型。export interface EditorSessionSnapshot { docId: string cursorOffset: number scrollOffset: number dirty: boolean } export class WorkspaceState { currentDocId: string layout-note-17 draftContent: string snapshot: EditorSessionSnapshot { docId: layout-note-17, cursorOffset: 0, scrollOffset: 0, dirty: false } saveSession(cursorOffset: number, scrollOffset: number): void { this.snapshot { docId: this.currentDocId, cursorOffset, scrollOffset, dirty: true } } }这段代码解决什么问题把“文档内容”和“编辑会话”分开保存让布局重建后可以恢复工作位置。这种模型还有一个好处以后支持平板自由窗口、电脑窗口缩放时不需要重新设计状态层。环境怎么变化工作区只负责读取同一份会话快照。八、不要把 foldStatusChange 和 windowSizeChange 当成同一件事折叠设备上常见的两个信号一个来自设备形态一个来自窗口尺寸。它们经常同时变化但语义不同。设备形态告诉我们“物理折叠状态发生了变化”窗口尺寸告诉我们“应用当前真正能使用的布局空间是多少”。如果页面只是做一栏、双栏这种响应式布局我更愿意让窗口尺寸成为主要判断依据因为最终决定排版的是可用空间。如果业务确实和物理形态相关比如悬停拍摄、上下屏控制区、折痕避让那再使用折叠状态或 FoldSplitContainer 的悬停态能力。官方多设备适配指南也强调形态切换后要根据窗口变化及时重排并恢复状态沉浸式场景下折叠展开还可能触发避让区域变化因此安全区也不应该写死。换句话说“我现在是折叠屏”不等于“我现在必须双栏”。真正的布局输入是窗口尺寸、方向、窗口模式、避让区和当前业务任务的组合。九、我用展开态截图做的最后一次验收最后一次验收我从 412 vp 的折叠态开始当前文档layout-note-17草稿 286 字输入几段内容不点击保存然后展开设备。展开后窗口是872 vp左侧面板360 vp右侧编辑区域512 vp。左侧仍然高亮同一篇笔记右侧草稿仍然显示 286 字会话状态没有被重置。图四不是简单放一张“大屏版 UI”而是把当前模式、窗口宽度、左右区域宽度、文档 ID 和草稿字数全部放在底部诊断区。红色标注分别指向双栏恢复、872 vp 和“草稿 286 字未丢失”。如果这几个数字不在我很难证明适配完成以后真的保持了连续性。十、折叠屏适配真正应该验收的是“连续”不是“能显示”这次做 FoldNote 以后我对多形态适配的看法更明确了。如果验收标准只是“折叠态不崩、展开态能铺满”很多状态问题都不会被发现。真正的验收应该围绕一个用户任务连续走完打开文档 → 输入内容 → 折叠 → 继续输入 → 展开 → 切换窗口大小 → 再回来。在整条链路里确认当前文档没有变化未保存草稿没有丢列表选中项没有重置滚动和光标尽量恢复布局没有覆盖系统避让区窗口改变没有触发多余的业务初始化。这才是我理解的折叠屏适配。FoldSplitContainer帮我们处理分区响应式断点帮我们选择布局窗口事件告诉我们环境变化。但用户状态要不要丢最终仍然取决于应用自己的架构。一个页面从单栏变成双栏其实很快真正需要时间的是把“布局变化”和“业务状态”彻底拆开。做到这一步以后后续适配平板、自由窗口甚至鸿蒙电脑时很多代码都能继续用因为我们已经不是在写“某款折叠屏专用页面”而是在写一个能够接受不同窗口环境的工作区。十一、自由窗口加入以后按设备型号判断很快会失效FoldNote 做到后面我特意把它放进多窗口环境里测试。这个动作让我更加确定不要把布局逻辑写成“折叠屏就双栏普通手机就单栏”。同一台展开态折叠设备上的应用窗口也可能被用户拖成很窄的自由窗口反过来大尺寸设备即使不是折叠形态也可能有足够空间显示双栏。如果布局绑设备类型窗口尺寸一变化就会出现设备明明展开但可用空间已经不够页面仍然强行双栏的情况。所以 FoldNote 的最终规则一直围绕“当前可用窗口”建立。窗口宽时列表和编辑区同时存在窗口窄时列表退回导航层。设备物理形态只在需要处理悬停、折痕或特殊交互时参与判断。测试范围也因此从“折叠 / 展开”扩大成折叠态全屏、展开态全屏、展开态自由窗口、横屏、分屏、系统栏显隐和输入法弹出。这些场景最终都可能改变窗口尺寸或避让区域。HarmonyOS 的沉浸式布局文档提到窗口形态变化、旋转以及多折叠设备的折叠展开都可能引起避让区域更新。所以业务状态保持不动布局状态允许持续变化避让信息作为环境数据实时更新才更适合多形态应用。十二、形态切换动画要克制连续性比“炫”更重要第二版 FoldNote 我给单双栏切换加过明显位移动画。展开时左侧列表从边缘滑进来右侧编辑器缩放到目标宽度看起来很有设计感。真机多试几次以后我把动画收敛了。编辑场景里用户注意力本来就在文字上设备展开只是环境变化不是一次业务跳转。如果整个页面大幅移动用户会重新寻找刚才的编辑位置长文档里尤其明显。现在我的原则是布局变化可以有短过渡但不要把内容当成新页面重新入场当前光标附近文本尽量保持视觉位置列表出现时不要抢焦点输入法存在时优先保证当前编辑区域可见。这也是为什么截图里我更强调PRESERVED、当前文档和草稿字数而不是做夸张的折叠动画。对于生产力工具来说状态连续比动画更能说明适配质量。十三、我最后用一张状态验收表代替“肉眼看看”第一组验收是布局连续性412 vp 单栏是否正确872 vp 双栏是否正确412 → 872 → 412 往返以后能否恢复连续快速调整窗口宽度时有没有重叠和闪烁。第二组是数据连续性当前文档 ID 是否一直为layout-note-17未保存草稿 286 字是否保持dirty 标记是否一致左侧选中项是否仍然对应当前文档。第三组是会话连续性光标位置是否可恢复滚动位置是否合理输入法打开时切换形态后编辑区是否仍然可见焦点有没有被列表抢走。第四组是环境连续性系统栏变化后内容是否重新避让自由窗口缩放时断点是否按实际宽度工作从后台回来以后是否只恢复一次业务状态而不是重复加载。把这些项目列出来以后折叠屏适配就从“看起来差不多”变成了可以测试、可以回归的工程问题。真正昂贵的 Bug 通常不是某个按钮偏了几个 vp而是用户正在做的事情被打断。十四、这套状态分层并不只适用于笔记应用换成邮件应用状态可能是当前邮件、草稿正文和附件上传进度换成电商应用状态可能是当前商品、规格选择和临时购物车换成代码编辑器则可能是当前文件、光标、未保存缓冲区和搜索结果。这些状态都不应该因为设备展开一下就重新初始化。所以我现在做多形态页面时会在写 UI 之前先画两张图一张布局树一张状态树。布局树可以因为 COMPACT / EXPANDED 重组状态树尽量保持稳定。只要这两棵树没有混成一棵折叠屏、平板和自由窗口的很多问题都会简单很多。FoldSplitContainer是布局工具不是状态管理器断点是环境判断不是业务判断形态切换是视图变化不应该被当成用户重新进入应用。这三句话基本就是 FoldNote 这次适配最后留下的工程结论。参考资料鸿蒙应用多设备通用适配指南https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/HarmonyOS 多设备开发最佳实践https://developer.huawei.com/consumer/cn/best-practices/multidevice/ArkUI API 26 FoldSplitContainer 变更https://developer.huawei.com/consumer/cn/doc/doccenter-release-notes/js-apidiff-arkui-b031沉浸式布局与避让区域https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/immersive-window-feature
返回列表