☰
HarmonyOS 7 FlexDesk:Multi-Window连续缩放与信息密度降级
2026/10/7 20:18:23 网站建设 项目流程

前两篇把 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.2ms

01 的固定断点切换平均是 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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询