ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HarmonyOS 7 FlexDesk:FoldStatus折叠态与编辑草稿保持

HarmonyOS 7 FlexDesk:FoldStatus折叠态与编辑草稿保持 03 把 FlexDesk 从几个固定宽度推进到了真实多窗口。一轮测试里窗口经历920×760 → 612×760 → 480×610 → 360×520 → 612×760 → 920×760收到 18 个windowSizeChange通过 48ms 合并成 6 次业务提交详情面板和工具栏按密度降级但pj_harmony_26 / task_1042 / draft 128 / scroll 384vp始终保持。04 开始碰另一个更“物理”的变化折叠屏。HarmonyOS 当前折叠屏设计原则把设备形态分成折叠态、展开态和悬停态并强调两件事屏幕尺寸变化后需要优化布局同时应用在折叠与展开切换中应该保持运行与状态连续。FlexDesk 因此没有把折叠状态当成“切换到另一个 App”。它只把 FoldStatus 当成一个额外的形态信号真实窗口宽度 决定主体布局 FoldStatus 用于识别折叠 / 展开 / 悬停语义。本轮故意在task_1042编辑到一半时连续折叠、展开、半折再展开并折回。最终要求页面怎么变都可以草稿、光标、项目、任务和滚动位置不能丢。本轮统一数据taskId: adapt_20261003_04 page: ProjectDetailPage.ets isFoldable: true foldSequence: FOLDED → EXPANDED → HALF_FOLDED → EXPANDED → FOLDED widthSequence: 430 → 920 → 720 → 920 → 430 vp layoutSequence: SINGLE → DUAL → HOVER → DUAL → SINGLE foldEvents: 5 selectedProjectId: pj_harmony_26 selectedTaskId: task_1042 draftChars: 128 cursorOffset: 96 scrollOffset: 384vp unsaved: true stateRetained: 5 / 5 detailStateRetained: true contentRebuildCount: 0 avgTransitionCost: 4.1ms finalFoldStatus: FOLDED finalWidth: 430vp finalLayout: SINGLE status: FOLD_CONTINUOUS一、04 的第一原则布局继续看窗口FoldStatus 只补充形态语义如果只监听 FoldStatusEXPANDED → 大屏 FOLDED → 小屏看起来很合理。但真实多窗口里展开态设备一样可能处在窄分屏 悬浮窗所以 FlexDesk 不会写if EXPANDED 一定 DUAL真正布局仍然先看windowWidthVpFoldStatus 只帮助项目理解用户正在折叠 还是展开 还是处于悬停形态。这和前面三篇“窗口事实与业务策略分离”的原则完全一致。二、折叠状态监听放在统一 Coordinator不塞进编辑页当前项目在可折叠设备上监听display.on(foldStatusChange)同时可以通过display.getFoldStatus() display.isFoldable()读取当前能力与状态。第一段代码解决的是折叠事件只进入统一协调器再转换成轻量状态给 UI。import{display}fromkit.ArkUIexportclassFoldContinuityCoordinator{privatecallback:((status:display.FoldStatus)void)|undefinedprivatefoldEvents0init():void{constfoldabledisplay.isFoldable()AppStorage.setOrCreate(isFoldable,foldable)if(!foldable){return}this.callback(status:display.FoldStatus){this.foldEventsconstcurrentdisplay.getDefaultDisplaySync()AppStorage.setOrCreate(foldStatus,status)AppStorage.setOrCreate(foldWidthVp,px2vp(current.width))}display.on(foldStatusChange,this.callback)AppStorage.setOrCreate(foldStatus,display.getFoldStatus())}destroy():void{if(!this.callback){return}display.off(foldStatusChange,this.callback)this.callbackundefined}}监听器有明确注册和释放边界。不会因为页面重复进入而叠加回调。三、状态连续性真正依赖的是业务 Store不是 FoldStatus 本身本轮编辑中的对象project: pj_harmony_26 task: task_1042 draft: 128 chars cursor: 96 scroll: 384vp unsaved: true这些信息都存在TaskEditorStore而不是存在FoldedPage ExpandedPage HoverPage如果状态跟布局页面绑定形态变化一定会变得非常难维护。第二段代码只保存业务事实exportinterfaceTaskEditSnapshot{projectId:stringtaskId:stringdraftText:stringcursorOffset:numberscrollOffsetVp:numberunsaved:boolean}exportclassTaskEditorStore{snapshot:TaskEditSnapshot{projectId:pj_harmony_26,taskId:task_1042,draftText:优化多形态下的任务详情布局与状态保持,cursorOffset:96,scrollOffsetVp:384,unsaved:true}}FoldStatus 变化只会改变怎么展示这份 Snapshot不会改变 Snapshot 本身。四、FOLDED 用 SINGLE不代表详情状态消失430vp 折叠态layoutSINGLEFlexDesk 当前只展示一个主区域。用户从项目列表进入task_1042后编辑页单独占满屏幕。这时detailPaneVisible可能是 false但selectedTaskId仍然是task_1042。折叠态减少的是“同时可见区域”不是“业务上下文”。五、EXPANDED 切 DUAL直接把当前详情展开到右侧展开到 920vplayoutDUAL左侧Project / Task List右侧task_1042 Detail Editor这里没有重新 push 一个详情路由而是同一份选中态在宽屏下自然具象成右侧面板。所以从折叠到展开草稿仍然 128 cursor96 unsavedtrue不会闪一下空白再重新加载。六、HALF_FOLDED 的 HOVER 不是“缩小版 DUAL”本轮半折叠时窗口宽度720vp如果只按 01 的断点它属于 MEDIUM。但 FoldStatus 告诉我们HALF_FOLDED产品可以额外采用悬停布局。SceneForge 当前 HOVER 策略上半区: 项目 / 任务上下文 下半区: task_1042 编辑区域重点是编辑 Store 仍然同一个 只重新分配展示区域。悬停态不是第三套业务页面。七、第三段代码把 FoldStatus 和 WindowMode 合成布局结果FlexDesk 最终不让页面直接判断多个枚举。exportenumFoldLayout{SINGLE,DUAL,HOVER}exportclassFoldLayoutPolicy{resolve(status:display.FoldStatus,widthVp:number):FoldLayout{if(statusdisplay.FoldStatus.FOLD_STATUS_HALF_FOLDED){returnFoldLayout.HOVER}if(widthVp600){returnFoldLayout.DUAL}returnFoldLayout.SINGLE}}这里再次强调600vp仍然是 FlexDesk 项目策略。HALF_FOLDED → HOVER也是当前 Demo 产品策略。真实项目应结合具体设备、折痕位置和业务场景验证。八、悬停态也要避免把编辑器卸载再挂一次如果 HOVER 进入时destroy TaskEditor → create HoverTaskEditor128 字草稿可能还能从 Store 恢复但编辑器内部IME 状态 selection undo stack local composition仍可能被破坏。所以 FlexDesk 尽量让TaskEditor保持同一业务组件身份。变化的是父层布局区域。这也是contentRebuildCount0真正想保证的事情。九、5 次形态变化必须保持 5/5 状态一致本轮FOLDED EXPANDED HALF_FOLDED EXPANDED FOLDED五个关键快照逐次核对projectId taskId draftChars cursorOffset scrollOffset unsaved全部一致。最终stateRetained5/5这比“展开后页面看起来没问题”更有可验证性。十、光标位置 96 是很容易在布局切换里丢掉的细节草稿 128 字都还在不代表编辑状态完全连续。如果光标从96跳到0用户下一次输入就会出现在开头。所以 04 特意把cursorOffset96作为验收项。真正编辑器接入时还要结合 TextArea / TextInput 的选择区能力恢复 selection。文章先把这条状态链保留出来。十一、未保存状态不能因为展开而自动变成“已保存”本轮unsavedtrue全程保持。形态变化本质只是UI 结构变化不应该触发save draft clear dirty flag否则用户只是打开了折叠屏却无意中改变了业务事务状态。同理折回手机态也不能自动丢弃草稿。十二、FoldStatus 监听和 windowSizeChange 可能同时到达设备真正折叠时FoldStatus 改变 窗口尺寸也改变两个事件可能很接近。如果每个事件都单独提交一次完整 UI 策略就会重复切布局。FlexDesk 的 Coordinator 只把foldStatus window metrics写入统一状态源。最终FoldLayoutPolicy消费最新快照。项目不假设两个系统事件一定按固定顺序到达。十三、平均切换 4.1ms 只表示项目状态与布局策略提交本轮avgTransitionCost4.1ms它不是整台折叠屏的系统折叠动画耗时。只统计 FlexDesk 自己状态快照 FoldLayoutPolicy 父布局切换这一小段成本。物理折叠、系统动画、屏幕模式切换都有自己的成本。工程指标必须明确口径。十四、DevEco 图里最重要的是“形态变了业务状态没变”开发图HiLogtaskId adapt_20261003_04 FOLDED 430vp SINGLE project pj_harmony_26 task task_1042 EXPANDED 920vp DUAL draftChars 128 cursor 96 scroll 384vp HALF_FOLDED 720vp HOVER stateRetained 3/3 unsaved true EXPANDED 920vp DUAL detailStateRetained true FOLDED 430vp SINGLE foldEvents 5 retained 5/5 avgCost 4.1ms contentRebuildCount 0 status FOLD_CONTINUOUS整篇真正要证明的都在这条日志里。十五、手机运行图故意展示未保存草稿最终运行图当前已经回到FOLDED 430vp SINGLE但任务编辑区仍然显示task_1042 128 / 500 未保存 cursor96 scroll384vp下面状态保持5/5最终FOLD_CONTINUOUS这比展示一张“展开屏更宽”更接近真实产品问题。十六、FoldStatus 不应该替代窗口断点如果一个普通平板窗口 920vp没有 FoldStatus一样应该使用 DUAL。如果一个展开态折叠屏被放进 480vp 悬浮窗FoldStatusEXPANDED也不应该强行 DUAL。所以 FlexDesk 最终优先级业务状态 独立保持 真实 Window 决定基础布局 FoldStatus 补充形态特性。三者职责不能互换。十七、折叠监听要在正确生命周期内释放和 01 的windowSizeChange一样foldStatusChange也必须on → 保存 callback → off如果只在页面aboutToAppear()里注册却没有稳定释放反复进入页面以后可能出现多个回调。FlexDesk 把监听放在 Coordinator由 Ability / App 级生命周期统一管理。十八、04 最后固定七组形态测试第一组FOLDED 430vpSINGLE。第二组EXPANDED 920vpDUAL。第三组HALF_FOLDED 720vpHOVER。第四组再展开详情仍然是 task_1042。第五组再折叠草稿仍然 128 字。第六组cursor96 / scroll384vp / unsavedtrue 全程保持。第七组五次形态变化中contentRebuildCount0。全部通过以后FOLD_CONTINUOUS才成立。十九、下一篇开始真正补 PC / 2in1 输入体验FlexDesk 到 04 已经解决窗口宽度 多窗口 底栏 / 侧栏 折叠 / 展开 / 悬停 状态连续05 会进入另一个完全不同的多端问题鼠标悬停 右键菜单 滚轮 键盘焦点 快捷键 Escape / Enter这时页面宽度已经不是核心矛盾。同一个 FlexDesk 要从“触屏 App”进一步变成真正适合 PC / 2in1 的生产力界面。二十、折叠状态和窗口状态需要一个“同帧快照”FoldStatus 和 windowSizeChange 可能在非常接近的时间内分别到达。如果 UI 先消费EXPANDED 430vp下一次才变成EXPANDED 920vp中间会出现一次不合理组合。FlexDesk 因此给 Coordinator 增加FoldWindowSnapshot里面同时保存foldStatus widthVp heightVp timestamp只在状态稳定后提交给页面。这样布局层拿到的是“一次形态变化后的完整事实”而不是两个系统事件的中间态。二十一、HALF_FOLDED 不能假设所有设备折痕位置都一样悬停态最诱人的写法是上半屏列表 下半屏编辑器但不同设备折痕位置 可用区域 屏幕比例都可能不同。所以 SceneForge 的 HOVER不是简单按height / 2切。真实产品应该结合当前设备可用区域、折痕区域等信息做进一步验证。04 文章里只把 HOVER 当成一个“产品形态策略”演示不宣称任何半折设备都能照搬同一像素位置。二十二、展开以后详情面板出现也要保留编辑器的输入法上下文草稿和 cursor 都保存下来以后还有一层很容易忽略IME / composition state尤其中文输入法正在组词时设备突然展开如果编辑器被重建未确认的拼音可能直接消失。FlexDesk 当前 Demo 先保证draft cursor unsaved连续。真实生产版会进一步把当前焦点 selection range 输入法激活状态纳入测试。这也是为什么 04 尽量保持同一个 TaskEditor 组件身份而不是切三套编辑器。二十三、折叠回手机态时不应该自动关闭“当前详情”展开态左列表 右详情折回手机态以后系统只能显示一个区域。FlexDesk 的规则是如果用户正在编辑 task_1042 → 手机态继续显示 task_1042 详情 如果只是浏览项目列表 → 手机态回项目列表。也就是说SINGLE只描述布局不描述导航目的地。真正应该显示什么仍然由用户当前业务上下文决定。二十四、FoldStatus 为 UNKNOWN 时必须有安全退化路径任何系统状态 API 都可能在初始化或特殊设备上出现UNKNOWNFlexDesk 不会UNKNOWN → 强行 HOVER而是退化到只看真实 windowWidthVp因此普通平板、PC、非折叠手机也可以使用同一套页面。FoldStatus 是增强信号不是页面能否工作的前置条件。二十五、折叠事件重复到达也要做幂等如果连续收到两个EXPANDED但窗口宽度和业务状态都没有变化项目不应该再次执行DUAL layout apply detail restore editor restoreFlexDesk 会比较previous FoldWindowSnapshot current FoldWindowSnapshot语义相同就直接忽略。这和 02 的重复导航阻断、03 的 resize 合并是同一种工程思想事件可以重复业务提交应该幂等。二十六、折叠状态不应该直接触发数据保存本轮unsavedtrue经过五次形态变化以后仍然是 true。这其实是一条非常重要的边界。设备折叠不是页面离开 用户提交 App 退出因此不能因为 FoldStatus 变化就偷偷autoSave()如果产品本身有自动保存那应该由编辑防丢策略 定时策略 业务事务策略决定而不是由“用户折了一下设备”决定。二十七、详情状态保持还要覆盖子 Tabtask_1042的详情以后可能有任务 笔记 附件 讨论多个子 Tab。如果展开前正在附件折回去又展开后不能默认跳回任务所以detailStateRetainedtrue最终会继续扩展成selectedDetailTab attachmentScroll editorSelection04 先把大框架跑稳让后续复杂状态都有一个可靠 Owner。二十八、04 真正建立的是“形态变化不参与业务事务”的规则从 FOLDED 到 EXPANDED再到 HALF_FOLDED本质上都是展示环境变化FlexDesk 的业务事务仍然是同一个pj_harmony_26 task_1042 draft 128 unsavedtrue所以最终架构可以总结成FoldStatus 决定形态特性 WindowMetrics 决定可用空间 LayoutPolicy 决定排布 TaskEditorStore 决定业务事实。只要这四层不混折叠屏状态连续就不会变成“每种形态维护一套页面”的灾难。二十九、最终折回 430vp 以后还要验证“返回路径”没有改变展开态的右侧详情收回单栏以后用户按返回键应该回到pj_harmony_26的项目列表而不是因为 DUAL 布局曾经存在就多出一层虚假的返回栈。FlexDesk 把“布局层级”和“导航层级”分开保存DUAL 只是同屏多显示一块内容不等于多 push 一层路由。最终折回后返回关系仍和最初 430vp 完全一致这也是状态连续的一部分。参考资料HarmonyOS 折叠屏设计原则https://developer.huawei.com/consumer/cn/doc/doccenter-ux-design/design-principles-0000001957023989HarmonyOS 多窗口布局适配https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/multi-window-layout-adapt-V14HarmonyOS 文档中心https://developer.huawei.com/consumer/cn/doc/
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进