前两篇把 FlexDesk 的“固定断点”逻辑跑稳了。
01 先解决主内容区:430vp 单列、720vp 两列、920vp 三列,六张 TaskCard 在 DynamicLayout 切换过程中保持同一个业务状态;02 再把手机底部 Tabs 和平板/2in1 侧栏导航接起来,selectedIndex=1、pj_harmony_26、task_1042、scrollOffset=384vp都没有因为导航形态变化被重建。
真正进入多窗口以后,问题明显复杂了一层。
用户拖动分屏边界时,窗口不是“从 430 一步跳到 720”,而是会连续经过很多尺寸。悬浮窗也会同时改变宽度、高度和宽高比。如果每一个windowSizeChange都立刻触发布局策略、重新计算工具栏、详情面板、卡片密度和导航形态,页面就会把窗口拖动变成一场高频状态风暴。
HarmonyOS 当前多窗口适配指南明确建议:进入浮窗或分屏后,应用窗口尺寸会变化,应监听windowSizeChange,并基于变化后的真实窗口尺寸做布局调整。FlexDesk 03 在这个基础上继续往前走:窗口事件必须真实接收,但业务布局提交要做合并;窗口越窄,先降低信息密度,不重建业务内容。
本轮统一数据:
taskId: adapt_20261003_03 page: ProjectsPage.ets windowSequence: 920×760 → 612×760 → 480×610 → 360×520 → 612×760 → 920×760 vp windowEvents: 18 committedMetrics: 6 droppedEvents: 12 throttleWindow: 48ms layoutModeChanges: 5 densityBreakpoints: EXPANDED >= 840vp MEDIUM 600–839vp COMPACT 420–599vp MINIMAL < 420vp selectedIndex: 1 selectedProjectId: pj_harmony_26 selectedTaskId: task_1042 draftChars: 128 scrollOffset: 384vp detailPaneVisible: true → true → false → false → true → true toolbarActionsVisible: 5 → 3 → 2 → 1 → 3 → 5 minCardWidth: 236vp textScale: 1.0 avgCommitCost: 1.9ms maxCommitCost: 3.2ms contentRebuildCount: 0 finalWidth: 920vp finalMode: EXPANDED status: MULTIWINDOW_STABLE一、03 不再关心“设备是什么”,只关心“当前窗口还能放多少信息”
多窗口里最危险的判断仍然是:
tablet → 大屏布局 phone → 小屏布局一台平板进入窄分屏以后,实际窗口可能只有 480vp。
这时如果还坚持:
tablet = 三栏卡片会被挤压,详情面板和项目列表会互相抢空间。
所以 03 的输入只保留两类事实:
windowWidthVp windowHeightVp设备类型只用于能力判断,不参与最终密度决策。
当前六个关键快照:
920×760 612×760 480×610 360×520 612×760 920×760刚好覆盖全屏、分屏、浮窗、极窄窗口和恢复。
二、windowSizeChange 高频触发时,先合并,再提交布局
真正拖动窗口时,系统可能连续发来很多尺寸变化。
FlexDesk 本轮收到了:
18 个 windowSizeChange但真正提交给布局层的只有:
6 次项目使用 48ms 合并窗口,只保留当前窗口里最后一次尺寸。
第一段代码解决的是:不丢真实最终尺寸,同时不让页面对每一个中间尺寸都做完整策略计算。
import{window}from'@kit.ArkUI'exportclassWindowEventCoalescer{privatetimer:number|undefinedprivatelatest:{widthVp:numberheightVp:number}|undefinedprivatereadonlydelayMs=48windowEvents=0committedMetrics=0droppedEvents=0attach(mainWindow:window.Window,onCommit:(widthVp:number,heightVp:number)=>void):void{mainWindow.on('windowSizeChange',(size)=>{this.windowEvents++this.latest={widthVp:px2vp(size.width),heightVp:px2vp(size.height)}if(this.timer!==undefined){clearTimeout(this.timer)this.droppedEvents++}this.timer=setTimeout(()=>{if(!this.latest){return}this.committedMetrics++onCommit(this.latest.widthVp,this.latest.heightVp)this.timer=undefined},this.delayMs)})}}48ms 是 FlexDesk 当前拖窗体验下的项目参数,不是 HarmonyOS 固定推荐值。
它的意义只是把短时间连续变化合并成更少的业务提交。
三、18 个事件只提交 6 次,不代表中间尺寸被“忽略”
这里容易产生误解。
合并不是:
系统只看到 6 个尺寸。真实 Window 仍然连续变化。
只是 FlexDesk 的“业务密度策略”不对 18 个事件全部执行。
当用户松手以后,最后一次尺寸一定会提交。
所以:
最终宽度 920vp不会因为节流而停在 873vp。
窗口动画和系统绘制仍然由系统处理,项目只降低业务状态更新频率。
四、第三档断点不够用了,03 新增 MINIMAL
01 只有:
COMPACT MEDIUM EXPANDED多窗口测试后发现 360vp 这种极窄浮窗里,即使 COMPACT 单列也显得过重。
所以 03 在不修改原有 600 / 840 主断点的前提下,增加一个项目级:
MINIMAL < 420vp最终四档:
exportenumDensityMode{MINIMAL,COMPACT,MEDIUM,EXPANDED}exportclassDensityPolicy{resolve(widthVp:number):DensityMode{if(widthVp<420){returnDensityMode.MINIMAL}if(widthVp<600){returnDensityMode.COMPACT}if(widthVp<840){returnDensityMode.MEDIUM}returnDensityMode.EXPANDED}}这四个阈值依然是 FlexDesk 的项目策略。
系统只提供真实窗口尺寸,不替业务决定“多少 vp 应该显示多少信息”。
五、信息密度降级比“整体缩小 UI”更自然
360vp 时最简单的做法是:
所有东西 scale(0.8)但文字、点击区域和视觉层级都会一起变差。
FlexDesk 的策略是:
EXPANDED: 项目列表 + 详情面板 工具栏 5 个动作 完整辅助信息 MEDIUM: 项目列表 + 详情面板 工具栏 3 个动作 COMPACT: 只显示列表 工具栏 2 个动作 MINIMAL: 只保留核心标题与状态 工具栏 1 个主动作本轮:
toolbarActionsVisible 5 → 3 → 2 → 1 → 3 → 5减少的是次要信息,不是把所有控件机械缩小。
六、详情面板是否出现只看密度策略,不改变 selectedTaskId
本轮窗口变化:
920 → 612 → 480 → 360 → 612 → 920详情面板可见性:
true true false false true true但整个过程中:
selectedTaskId=task_1042从未变化。
这意味着在 480vp 隐藏右侧详情时,项目并没有:
clear selectedTaskId再次回到 612vp 后,同一任务详情自然回来。
“隐藏视图”和“清空业务状态”必须是两件事。
七、第二段代码把密度策略变成纯函数输出
为了防止页面 build 里到处出现:
if width < ...FlexDesk 让 DensityPolicy 返回一个完整视图策略。
exportinterfaceProjectDensity{mode:DensityMode toolbarActions:numberdetailVisible:booleanminCardWidthVp:numbertextScale:number}exportfunctionresolveProjectDensity(widthVp:number):ProjectDensity{if(widthVp<420){return{mode:DensityMode.MINIMAL,toolbarActions:1,detailVisible:false,minCardWidthVp:236,textScale:1.0}}if(widthVp<600){return{mode:DensityMode.COMPACT,toolbarActions:2,detailVisible:false,minCardWidthVp:236,textScale:1.0}}if(widthVp<840){return{mode:DensityMode.MEDIUM,toolbarActions:3,detailVisible:true,minCardWidthVp:236,textScale:1.0}}return{mode:DensityMode.EXPANDED,toolbarActions:5,detailVisible:true,minCardWidthVp:236,textScale:1.0}}这个函数不读取页面状态。
它只回答:
当前窗口应该怎样展示。
所以更容易单测,也更容易在 06 做宽度矩阵回归。
八、导航仍然复用 02,不重新造一套规则
03 没有重新发明导航。
沿用:
< 600vp 底部 Tabs >= 600vp 侧栏 Tabs所以本轮:
920 side 612 side 480 bottom 360 bottom 612 side 920 side但:
selectedIndex=1始终不变。
多窗口适配最容易失控的地方,就是每一篇都加一套新规则。
FlexDesk 尽量让断点和状态源持续复用。
九、草稿 128 字和 scrollOffset 384vp 仍然是同一份状态
窗口拖到 360vp 时:
详情面板已经消失但编辑草稿仍然:
128 chars列表滚动位置仍然:
384vp恢复到 920vp:
任务仍然是 task_1042 草稿仍然 128 scrollOffset 仍然 384最终:
contentRebuildCount=0这是 03 最关键的验收指标之一。
十、高频窗口事件不能带着业务请求一起跑
如果每次窗口变化都执行:
重新拉项目 重新查任务 重新请求统计18 个事件就可能变成 18 次请求。
FlexDesk 规定:
WindowMetrics 只影响视图策略; Repository 只响应真实业务数据变化。布局变化不能触发网络或数据库重载。
这和 01 的原则保持一致。
十一、宽高比也要保存,但当前项目先让宽度主导
浮窗:
480×610和分屏:
612×760不仅宽度不同,高度也不同。
FlexDesk 的 WindowMetrics 保存:
widthVp heightVp aspectRatio当前 ProjectsPage 主要由宽度决定密度。
如果未来出现高度非常低的横向浮窗,工具栏是否折叠、详情区是否滚动,就会继续消费 height / ratio。
03 先把数据准备好,不把所有复杂策略一次性塞进来。
十二、切换成本为什么比 01 更低
本轮:
avgCommitCost=1.9ms maxCommitCost=3.2ms01 的固定断点切换平均是 3.6ms。
这并不代表 03 “优化了一半”。
口径不同。
03 只统计:
coalesced metrics commit + density policy最终完整页面渲染仍然受组件复杂度影响。
文章里保留这个数字主要是给同版本后续回归做基线。
十三、DevEco 图里要同时看“事件数”和“提交数”
开发图:
HiLog:
taskId= adapt_20261003_03 raw windowEvents= 18 throttleWindow= 48ms committed= 6 dropped= 12 920×760 EXPANDED actions=5 detail=true 612×760 MEDIUM actions=3 detail=true 480×610 COMPACT actions=2 detail=false 360×520 MINIMAL actions=1 detail=false avgCommit= 1.9ms maxCommit= 3.2ms contentRebuild= 0 status= MULTIWINDOW_STABLE这条日志把“连续拖窗为什么没有把业务状态拖乱”讲清楚了。
十四、手机诊断页展示的是一次完整窗口轨迹
最终运行图:
当前已经回到:
920×760 EXPANDED但页面保留本轮完整轨迹:
920 → 612 → 480 → 360 → 612 → 920还有:
18 events 6 commits 12 dropped/coalesced 5 mode changes最终:
MULTIWINDOW_STABLE它证明的不是某一个断点“长得对”,而是整个窗口变化过程能稳定收口。
十五、03 最后固定八组多窗口测试
第一组,全屏 920×760,EXPANDED。
第二组,分屏 612×760,MEDIUM。
第三组,浮窗 480×610,COMPACT。
第四组,极窄 360×520,MINIMAL。
第五组,连续拖动产生高频事件,48ms 合并生效。
第六组,详情面板隐藏后task_1042仍然保持。
第七组,恢复宽屏后草稿与滚动位置恢复。
第八组,全程contentRebuildCount=0。
这些都通过后:
MULTIWINDOW_STABLE才成立。
十六、下一篇:窗口恢复只是尺寸变化,折叠屏还有“物理形态变化”
03 处理的所有变化,都可以用:
windowWidth / windowHeight解释。
折叠屏多一层:
FOLDED EXPANDED HALF_FOLDED用户把设备展开时,除了窗口变大,还会形成“详情应该直接展开”的产品预期;半折叠时还可能需要专门利用上下区域。
04 会继续同一个 FlexDesk,用折叠状态做“形态感知”,但业务状态仍然沿用:
pj_harmony_26 task_1042 128 字草稿 光标位置 scrollOffset下一篇真正验收的是:
物理形态变化以后,编辑中的任务仍然是同一个任务。
十七、连续拖窗时最容易被忽略的是“布局动画叠加”
如果每次模式变化都触发:
Grid 列数动画 详情面板显隐动画 导航位置动画 工具栏按钮过渡而窗口本身又在连续缩放,用户会看到两套动画同时竞争。
FlexDesk 当前策略很克制:
窗口正在 resize → 关闭复杂过渡,只做即时布局; 窗口稳定 → 再允许轻量 fade / move。也就是说,动画不是“越多越高级”。
连续拖窗阶段的第一目标是:
跟手 稳定 不抖等窗口停止以后再补视觉细节。
这个策略虽然没有写成系统 API,但对多窗口体验非常重要。
十八、MINIMAL 模式还要限制文本行数,而不是只隐藏按钮
360vp 这种极窄窗口里,如果标题、副标题、状态说明仍然全部展开,多出来的文本会把卡片高度迅速撑大。
所以 MINIMAL 还会改变:
标题最大 1 行 副标题隐藏 状态标签保留 时间信息只保留最关键一项它仍然不改变:
task_1042 draft 128 scroll 384vp只是减少可见信息。
这种做法比统一缩放字体更符合“信息密度降级”的目标。
十九、窗口恢复到 EXPANDED 时,不应该重放所有隐藏内容的入场动画
从 360vp 拉回 920vp:
详情面板重新出现 工具栏从 1 个动作恢复到 5 个如果这些控件被当成“全新页面”重新入场,会给用户一种“刚打开另一页”的感觉。
FlexDesk 把它们理解为:
之前被密度策略隐藏的同一内容所以恢复时只做很轻的过渡。
业务层不触发:
aboutToAppear reload fetch restore from disk真正的状态仍然在内存中的 Store 里。
二十、节流窗口也要防止页面销毁以后定时器回调
setTimeout带来另一个生命周期问题。
如果页面或 Ability 已经销毁,但 48ms 的 Timer 还没执行,回调仍然可能尝试:
commitSize 更新 AppStorage 访问已释放对象所以WindowEventCoalescer还需要一个dispose():
exportclassWindowEventCoalescer{privatedisposed=falsedispose():void{this.disposed=trueif(this.timer!==undefined){clearTimeout(this.timer)this.timer=undefined}}}真正执行提交前再检查:
disposed=false这样节流逻辑自己也有明确生命周期,而不是只顾性能。
二十一、窗口模式和内容密度最好是两个字段
MEDIUM并不必然等于:
详情一定显示未来同样 612vp 宽度,如果高度只有 300vp,可能仍然需要把详情收起来。
所以 FlexDesk 后续会把:
windowMode和:
densityPolicy拆成两个概念。
当前数据里为了简化,两者大部分一一对应。
但代码结构已经留出:
width / height / aspect → WindowMetrics → DensityPolicy而不是让MEDIUM成为一个什么都代表的万能枚举。
二十二、信息密度降级也要考虑无障碍字体放大
当前:
textScale=1.0因此 236vp 卡片最小宽度还能正常展示。
如果系统字体放大,真实可用内容宽度会减少。
这时项目不能只看:
windowWidthVp还要考虑:
字体缩放 可访问性 本地化语言否则英文长标题或大字体场景里,COMPACT 可能比预期更早需要进入 MINIMAL。
06 最终回归会把“大字体 + 窄窗口”作为一组独立用例。
二十三、03 真正留下的是一个“连续窗口输入协议”
做到这里,FlexDesk 已经形成:
Window 原始事件 → 48ms 合并 → px 转 vp → WindowMetrics → BreakpointResolver → DensityPolicy → 同一业务状态这个协议比某一张分屏截图更重要。
后面无论:
折叠屏 PC 自由窗口 分屏比例变化 悬浮窗只要最终能转换成 WindowMetrics,就能继续复用同一条适配链。
二十四、窗口恢复后的“最终一致性”要单独做一次收口检查
连续拖窗结束以后,FlexDesk 不只看当前是不是 920vp,还会把最终业务快照和拖窗前做一次比较:selectedProjectId、selectedTaskId、草稿长度、滚动锚点、导航索引都必须一致。这样可以抓住一种很隐蔽的问题:中途 UI 看起来正常,但某个 Store 在极窄模式下被临时覆盖,恢复宽屏后才表现出来。最终收口检查通过,才把本轮标成MULTIWINDOW_STABLE。
参考资料
- HarmonyOS 多窗口布局适配:
https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/multi-window-layout-adapt-V14 - HarmonyOS 多窗口支持:
https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/multi-window-support - DynamicLayout:
https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-container-dynamiclayout