当一个任务需要缩成很小的进度提示时,我通常不会先画那个小窗口,而是先问一个更具体的问题:同一份业务状态被连续触发两次,界面会不会出现两个相互覆盖的预览?用户从订单页返回列表,再次进入页面,旧预览是否还会残留?这类重复挂载问题往往比圆角、尺寸或动效更难排查。

这次使用 FloatPreview 做一份页面内前置预览。业务对象是订单 OD-3482,会话编号 FP-031,当前状态为 配送中。先把幂等状态、重复调用与页面回收理顺,再决定是否接入真正的系统闪控窗。本文用的是 ArkUI 的 overlay 组件属性,只绘制在当前组件及其页面布局范围内,不是 @ohos.window.floatView,不等于跨页面或跨应用的系统闪控窗。这一点必须写在实现边界里,否则配图很容易被误读成系统能力已集成。

一、把窗口形态与业务状态分开

产品稿经常只标两个态:“展开”和“收起”。对开发者来说,这还不足以定义动作。屏幕里出现一个提示,可以来自首次挂载,也可以来自相同消息的重放;用户看见的是同一个界面,程序却可能持有不同的计时器和回调。先规定业务会话身份,才有办法判断重复调用是否值得执行。

FloatPreview 的演示状态刻意保持简单:DETACHED 表示当前页面不展示预览,ATTACHED 表示页面内预览已挂载,SKIP_DUPLICATE 是日志事件而非第三种持久状态。FP-031 是业务自己生成的会话标记,不是系统分配的 window ID。订单 ID OD-3482 代表同一个正在处理的业务对象,不能因为界面多次打开就生成新的订单会话。

下面先写一个和 UI 完全分开的状态小对象。解决的是“收到第二次打开请求时,是否改变现有状态”,不负责创建窗口。

class PreviewSession {
  readonly sessionId: string = 'FP-031';
  readonly orderId: string = 'OD-3482';
  attached: boolean = false;
  duplicateSkips: number = 0;

  attach(): boolean {
    if (this.attached) {
      this.duplicateSkips += 1;
      return false;
    }
    this.attached = true;
    return true;
  }

  detach(): void {
    this.attached = false;
  }
}

attach() 返回 false 的意义非常直接:本次动作被拒绝,不应重新绑定事件、重新启动倒计时,也不应清空原来的内容。如果实际产品允许订单更换,则应先比较 orderId;同订单重复挂载只需要忽略,不同订单则需要显式结束旧会话。这种判断放到组件外部,更方便后续对接消息推送或其他形态的窗口能力。

二、用 overlay 验证页面级交互,不越界模拟系统窗口

官方 overlay 文档规定,它可以接受文本、自定义 Builder 或 ComponentContent,允许配置相对定位。这里选文本形式,只验证一个关键问题:在页面右上方展示轻量状态,点击两次打开按钮只出现同一份内容。因为这不是系统闪控窗,所以不讨论悬浮权限,也不虚构调用系统窗口的参数。

这个页面把展示真值放在 @State active,业务 ID 固定为 FP-031;每次按“显示预览”,先检查 active,如已经显示,仅累计 duplicateSkips。第二次触发不会叠加另一层浮层。下面的代码是页面核心逻辑,样式部分有意从简。

@Entry
@Component
struct PreviewPage {
  @State active: boolean = false;
  @State duplicateSkips: number = 0;
  private sessionId: string = 'FP-031';

  private openPreview(): void {
    if (this.active) {
      this.duplicateSkips += 1;
      console.info(`FloatPreview ${this.sessionId} skip_duplicate`);
      return;
    }
    this.active = true;
    console.info(`FloatPreview ${this.sessionId} attach`);
  }

  build() {
    Column({ space: 18 }) {
      Text('订单 OD-3482')
        .fontSize(22)
        .overlay(this.active ? '配送中 · FP-031' : '', {
          align: Alignment.TopEnd
        });
      Text(`重复打开被忽略:${this.duplicateSkips}`);
      Button('显示预览').onClick(() => this.openPreview());
      Button('关闭预览').onClick(() => { this.active = false; });
    }
    .width('100%')
    .padding(20)
  }
}

.overlay() 属于当前组件层级的装饰能力,这段代码不能把内容送到应用之外。它适合用来排查内容映射、动作次数、层级关系;但遇到模态弹框、键盘避让或页面切换时,表现与系统级闪控窗并不等价。这里把预览文字锚定在订单标题处,避免使用全局屏幕坐标,让它先遵循所在组件的布局约束。视图过小或系统字体放大时,应检查文字截断,而不能假定右上角永远有充足空间。

三、页面离开时要主动收口

重复挂载通常伴随第二个问题:页面离开了,业务还在。若预览附带更新倒计时或延迟关闭的任务,组件销毁后继续修改状态,可能让下次进入页面看到旧会话留下的结果。简单的页面预览不必引入后台运行能力,只要在当前组件结束时把临时资源释放干净。

第三段代码用于处理一个可选的“十五秒自动关闭”策略。它是应用业务计时器,不是系统窗口寿命;演示图没有把十五秒说成真实测得的超时时间。每次启动前先清掉旧计时器,离开页面时也明确回收。

private closeTimer: number = -1;

private scheduleClose(): void {
  this.cancelClose();
  this.closeTimer = setTimeout(() => {
    this.active = false;
    this.closeTimer = -1;
  }, 15000);
}

private cancelClose(): void {
  if (this.closeTimer >= 0) {
    clearTimeout(this.closeTimer);
    this.closeTimer = -1;
  }
}

aboutToDisappear(): void {
  this.cancelClose();
  this.active = false;
}

这段方法应与上一段同属 PreviewPage,不能直接追加在 struct 外面;它承担的是集成时需要补上的生命周期职责。真实产品若允许用户主动关闭预览,也应该调用 cancelClose(),防止关闭后旧计时器仍然触发。若页面正在跳转,业务状态不能仅靠组件 @State 保存;订单本身应由独立的数据层管理,页面浮层只持有展示态。订单持续运行和浮层是否可见是两件事,不能绑定成同一个布尔值。

需要注意,aboutToDisappear 是本页面级别的退出收口,不保证处理所有系统级中断。如果后续换成真正的 FloatView,还必须遵循对应能力的创建、绑定、隐藏和销毁规则,不能把页面内 .overlay() 的清理方式直接复制过去。此处保留这种差异,是为了不把 UI 原型误写成已经接入系统窗口的成品。

四、演示状态如何对应到调试信息

本轮三张图采用一组固定展示值:项目名 FloatPreview、页面 PreviewPage、会话 FP-031、订单 OD-3482、业务状态 配送中、页面状态 ATTACHED,重复打开被忽略次数 1。演示时刻 16:40:20。其中“重复打开被忽略:1”表示同一会话的第二次打开请求没有创建新的显示态,不表示发生了一次真实的系统闪控窗创建与回收。

如果后续准备验证这套逻辑,建议保留一次明确的人工操作序列:进入订单页,打开一次,再连续触发两次中的第二次,确认 duplicateSkips 增加而 active 不翻转;之后主动关闭并返回,检查页面回收是否清掉计时器;最后重新进入同订单,确认重建的是新的页面展示状态,而不是旧组件的残留。对于实际的系统级闪控窗,还需要重新编排窗口可见性、任务持续状态及系统提供的回调,不可用这份页面 UI 的验收结论代替系统能力验证。

日志中只需要记录会话、动作、是否跳过、当前展示态。不要把个人地址、电话等配送敏感信息输出到普通 HiLog;样例里的 OD-3482 是专为文章设计的虚构订单编号。日志时间也是排版一致性需要的示例值,不是性能计量结果,更没有理由推导出“毫秒级响应”之类的结论。

五、从前置预览走向真正窗口的决策点

这份轻量原型能回答三个问题:同一个业务事件是否会重复执行,页面层级中的预览会不会遮挡重要按钮,以及离开页面时临时资源是否被回收。它不能回答系统级窗口能否跨应用展示、真实窗口形态受什么约束、特定设备上的触摸分发如何变化。这些必须等到阅读适配目标版本的闪控窗指南并完成实际集成验证后再下结论。

从工程上说,状态设计应该先于形态设计。如果将来把页面内预览替换为系统闪控窗,可以继续保留 sessionId、orderId、重复请求过滤和敏感日志约束,再让一个明确的窗口适配层处理系统 API。这样能避免 UI 和业务生命周期绑死,窗口形态变了,订单状态却被意外重置。

在目前的演示边界内,最终结论只有一个:浮层出现一次不等于业务创建一次。把幂等判断、清理时机和可见性分开管理,才有资格继续探索真正的系统级闪控窗。本文所有画面均为模拟演示,并非实际 IDE 运行或系统浮窗的测试证据。

官方资料: ArkUI overlay 属性参考(2026-08-29)、HarmonyOS 闪控窗开发指南(具体接入能力需按 SDK 验证)、主窗口与辅助窗口生命周期(2026-09-09)。

Logo

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

更多推荐