最近我把平板/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 时,用户看到的是一次完整重排,而不是七次细碎移动。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐