HarmonyOS 7 Window Kit + ArkUI:自由窗口尺寸变化中的断点收敛、独立子窗锚点重算与事件防抖【鸿蒙心迹】
最近我把平板/PC 共用的调试面板改成独立子窗:inspector_panel 固定在主窗口右上角,保持右侧 24 vp、顶部 96 vp。
真正跑起来后,windowSizeChange 会连续触发;每次都立刻 moveWindowTo(),子窗就会抖。从 MEDIUM 跨到 WIDE 时,主页面还要重排,一段拖拽很容易执行七八次移动。
这次我只处理三个工程问题:尺寸事件怎样收敛成一次布局决定、页面断点与子窗锚点怎样使用同一份尺寸、页面销毁时 listener / timer / Window 怎样成对回收。
Demo 叫 SubWindowAnchorLab。最终主窗口 1180 × 760 vp,断点 WIDE,子窗 inspector_panel,右边距 24 vp、顶部 96 vp。一次拖拽收到 7 次尺寸事件,真正执行 2 次 reposition,5 次被防抖合并。

一、先确定这个面板为什么要做成独立子窗
这里用 createSubWindowWithOptions() 创建子窗,并开启 zLevelAboveParentLoosened。
private async createInspector(
windowStage: window.WindowStage
): Promise<void> {
const options: window.SubWindowOptions = {
title: 'Inspector Panel',
decorEnabled: true,
zLevelAboveParentLoosened: true
}
this.subWindow =
await windowStage.createSubWindowWithOptions(
'inspector_panel',
options
)
await this.subWindow.resize(
this.uiContext.vp2px(320),
this.uiContext.vp2px(420)
)
await this.subWindow.setUIContent(
'pages/InspectorPanel'
)
await this.subWindow.showWindow()
}
没有打开完全跟随父窗口布局,因为 inspector 的 size 和 position 都由业务控制。
二、windowSizeChange 只做输入,不直接驱动子窗
主窗口尺寸变化以后,我不会让页面主动通知子窗,也不会让子窗反过来改页面。
两者只共享同一份 window.Size。
事件进来后,宽高写入 AppStorage;ArkUI 页面根据同一份数据决定断点;Coordinator 则把 size 放进防抖队列。这样页面和子窗不会出现“一边已经 WIDE,另一边还拿着 MEDIUM 尺寸”的错位。
三、真正解决抖动的是 120 ms 防抖
用户拖动自由窗口时,resize 回调会连续到来。每个事件都变成一次 reposition,视觉上一定会抖。
private resizeTimer: number = -1
private scheduleReposition(
size: window.Size
): void {
if (this.resizeTimer !== -1) {
clearTimeout(this.resizeTimer)
this.debounced++
}
this.resizeTimer = setTimeout(() => {
this.resizeTimer = -1
this.applyWindowState(size)
}, 120)
}
120 ms 是 Demo 的业务参数,不是系统固定值。
最终 Resize Events=7、Debounced=5、Applied Repositions=2。也就是说,大部分原始尺寸事件被合并成稳定布局,而不是直接变成窗口移动。
这里更关心最终执行了几次动作。
四、断点和锚点必须使用同一套 vp 尺寸
窗口事件提供的是 px,而 ArkUI 响应式布局更适合按 vp 判断断点。
如果页面按 vp 判断 WIDE,子窗却直接拿 px 做 24、96 这样的业务边距,不同密度设备上迟早会错位。
private async applyWindowState(
size: window.Size
): Promise<void> {
const widthVp =
this.uiContext.px2vp(size.width)
const heightVp =
this.uiContext.px2vp(size.height)
const breakpoint =
widthVp >= 960
? 'WIDE'
: widthVp >= 720
? 'MEDIUM'
: 'COMPACT'
AppStorage.setOrCreate(
'windowBreakpoint',
breakpoint
)
const panelWidthVp = 320
const xVp = widthVp - panelWidthVp - 24
await this.subWindow?.moveWindowTo(
this.uiContext.vp2px(xVp),
this.uiContext.vp2px(96)
)
this.appliedRepositions++
this.state = 'ANCHORED'
}
最终宽度 1180 vp,所以断点稳定在 WIDE。锚点也直接从当前窗口矩形重新计算,而不是根据“上一次移动了多少”做增量补偿。
这样窗口一次跨过几百 vp 时,不会因为连续 delta 累加误差把 inspector 越推越偏。
五、COMPACT 不是把同一个 320 vp 面板硬塞进去
本次验收为 WIDE,inspector 保持 320 vp 宽度。
真实产品一定要给 COMPACT 单独策略。主窗口很窄时,可以隐藏独立子窗、缩小面板,或者改成页面内 Sheet。
断点变化影响的不只是 x 坐标,而是整个子窗呈现策略,所以断点判断和锚点计算放在同一个 Coordinator。
六、独立子窗仍然要有明确的释放路径
层级能力不会替业务释放 listener、timer 和 Window 引用。
页面退出时我会依次做三件事:对 windowSizeChange 执行 off();如果 120 ms timer 还没结束就先 clearTimeout();最后 destroyWindow() 并清空引用。
顺序的目的不是“写得完整”,而是防止三类真实问题:下一轮页面重建出现双回调、页面退出后还有迟到的 move、同名子窗创建时残留旧窗口。
正式工程里这套清理不能只放在“用户点返回”的正常路径,Ability 或页面异常销毁同样要覆盖。
七、调试页只看原始事件和最终动作的差值
工程只保留 EntryAbility.ets、MainWorkspacePage.ets、InspectorPanel.ets 和 SubWindowAnchorCoordinator.ets。
HiLog 固定输出:
windowSizeChange=860x760
windowSizeChange=1180x760
breakpoint=WIDE
move inspector_panel x=812vp y=96vp
repositionCount=2
State: RESIZING -> ANCHORED
把 Resize Events、Debounced 和 Applied Repositions 同时展示,比只看“子窗最后在右上角”更有用。它能直接说明防抖有没有真正收敛事件。

八、最终运行结果验证的是“7 次事件只移动 2 次”
最终页面显示:
- Session:
subwin_anchor_20261001_16 - State:
ANCHORED - Main Window:
1180 × 760 vp - Breakpoint:
WIDE - SubWindow:
inspector_panel - Anchor Right:
24 vp - Anchor Top:
96 vp - Resize Events:
7 - Applied Repositions:
2 - Debounced:
5 - Independent:
true - Last Reposition:
16:36:18
预览里 inspector_panel 始终贴在右上角,真正说明实现稳定的是 7 次原始事件只落成 2 次定位。

九、正式产品还要处理几个边界
正式产品还要处理几个边界:COMPACT 下需要降级策略;跨显示器后密度可能变化,必须基于当前 UIContext 转换 px/vp;允许用户手动拖动 inspector 时要区分自动锚定和手动位置;子窗里的异步任务也应在 destroyWindow 前结束。
十、这次真正优化的是“尺寸事件怎样变成一次布局决定”
状态链路:
RESIZING → DEBOUNCING → RECALCULATING → ANCHORED
自由窗口真正麻烦的是变化连续发生。更稳妥的方式是先把 resize 收敛成稳定尺寸,再让页面断点和子窗锚点一起计算。这样从 MEDIUM 拉到 WIDE 时,用户看到的是一次完整重排,而不是七次细碎移动。
更多推荐




所有评论(0)