👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
  
  🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
  🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
  💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
  
   如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!

前言

用户把应用退到后台,任务还在跑——这时候怎么让他感知到进度,又不强迫他回到应用?

这正是闪控球和闪控窗要解决的问题。一个是常驻屏幕边缘的小球,承担最简进度提示;另一个是点击小球后弹出的展开面板,展示更多信息并提供操作按钮。两者分工明确,配合使用,形成一套完整的后台任务可见性方案。

本文围绕官方文档提供的接口,用一个下载任务场景,把从创建控制器到状态切换、再到退出清理的完整开发流程串联起来。

一、两种形态各自承担什么

闪控球是一个系统级全局应用,已由系统集成,开发者不需要自己实现它的 UI,只需要通过接口向它传递进度值、颜色、图标和标签,它就会把这些信息渲染在小球上。

闪控窗是应用自己展示的悬浮窗,内容完全由开发者控制。通过 ComponentContent 绑定两个 @Builder 组件——一个进度条区域、一个操作按钮区域——然后在应用退到后台后通过 FlashControlWindowController.showWindow() 弹出。

两者的联动关系是:用户点击闪控球,闪控窗展开或收起;收起时闪控球继续显示进度。状态变化通过 flashWindowStatusListener 回调通知给应用。

这里有一点容易理解偏差:闪控球不是应用自己画的,FlashBallConfig 只是配置数据,实际渲染由系统接管。

二、官方规定的前置条件

在动手写代码之前,几个限制条件需要先确认清楚。

项目要求
API Level从 API version 18 开始支持
支持设备仅 Phone
权限ohos.permission.FLASH_CONTROL
showWindow 调用时机只能在应用处于后台时调用
系统能力SystemCapability.ArkUI.ArkUI.Full
import 来源@kit.ArkUI

showWindow 这个限制是关键——如果在前台调用,接口会失败。这意味着显示闪控窗的逻辑必须放在 UIAbility 的 onBackground 之后触发,不能提前。

三、最小场景设定

以一个文件下载任务为例。

目标是:

  • 用户在应用前台发起下载;
  • 切换到后台后,屏幕边缘出现闪控球,显示下载进度(0~100);
  • 点击闪控球,弹出闪控窗,展示进度条和"暂停/取消"按钮;
  • 再次点击小球,收起闪控窗;
  • 应用回到前台,闪控窗隐藏;
  • 应用销毁,清理控制器。

四、核心代码实现

4.1 权限配置

在 module.json5 的 requestPermissions 字段中添加:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.FLASH_CONTROL"
      }
    ]
  }
}

4.2 定义进度条和操作按钮的 Builder 组件

这两个组件将通过 ComponentContent 注入到闪控窗中,是闪控窗展开后用户看到的内容。

import { FlashControlWindowController, FlashBallConfig, FlashWindowConfig, FlashWindowStatus }
  from '@kit.ArkUI';
import { UIAbilityContext } from '@kit.AbilityKit';

// 进度条区域参数类型
interface ProgressParams {
  progress: number;
  color: ResourceColor;
  label: string;
}

// 操作按钮区域参数类型
interface ButtonParams {
  onPause: () => void;
  onCancel: () => void;
}

// 进度条组件
@Builder
function ProgressBarBuilder(params: ProgressParams) {
  Column() {
    Progress({ value: params.progress, total: 100, type: ProgressType.Linear })
      .width('100%')
      .height(8)
      .color(params.color)
    Text(params.label)
      .fontSize(14)
      .fontColor('#333333')
      .margin({ top: 8 })
  }
  .padding(16)
  .width('100%')
}

// 操作按钮组件
@Builder
function StartButtonBuilder(params: ButtonParams) {
  Row() {
    Button('暂停')
      .onClick(() => params.onPause())
      .margin({ right: 8 })
    Button('取消')
      .onClick(() => params.onCancel())
  }
  .padding(16)
  .justifyContent(FlexAlign.Center)
  .width('100%')
}

4.3 在 UIAbility 中管理控制器生命周期

这一段代码是整个流程的核心,放在 EntryAbility.ets 中:

import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
import { FlashControlWindowController, FlashWindowConfig, FlashWindowStatus,
         FlashBallConfig } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  private flashController: FlashControlWindowController | null = null;
  private currentProgress: number = 0;

  // 应用切入后台:创建控制器、绑定组件、显示闪控窗
  async onBackground() {
    try {
      // 1. 创建控制器实例
      this.flashController = await FlashControlWindowController.create();

      // 2. 构建 ComponentContent(需要 UIContext,从 windowStage 获取)
      const uiContext = this.context.getApplicationContext()
        .getRunningProcessInformation; 
      // 注意:ComponentContent 需要从 windowStage.getMainWindowSync().getUIContext() 获取
      // 具体 UIContext 的获取方式取决于工程实际结构,此处作为示意

      // 3. 绑定进度条和操作按钮(ComponentContent 的实际构造依赖 UIContext)
      // 下方展示逻辑结构,UIContext 的获取见 4.4 节说明

      // 4. 注册状态监听
      this.flashController.flashWindowStatusListener((status: FlashWindowStatus) => {
        this.onFlashWindowStatusChanged(status);
      });

      // 5. 配置闪控球(进度 + 颜色 + 图标 + 标签)
      const ballConfig: FlashBallConfig = {
        progressValue: this.currentProgress,
        progressColor: '#007DFF',
        icon: $r('app.media.startIcon'),
        label: '下载中'
      };
      await this.flashController.setFlashBallConfig(ballConfig);

      // 6. 显示闪控窗(必须在后台调用)
      const windowConfig: FlashWindowConfig = {
        defaultHeight: 200,
        maxHeight: 400
      };
      await this.flashController.showWindow(this.context, windowConfig);

    } catch (err) {
      console.error('FlashControl onBackground error:', JSON.stringify(err));
    }
  }

  // 应用回到前台:隐藏闪控窗
  async onForeground() {
    if (this.flashController) {
      try {
        await this.flashController.hideWindow();
      } catch (err) {
        console.error('FlashControl hideWindow error:', JSON.stringify(err));
      }
    }
  }

  // 应用销毁:清理控制器
  async onDestroy() {
    if (this.flashController) {
      try {
        this.flashController.off('flashWindowStatusChange');
        await this.flashController.destroy();
      } catch (err) {
        console.error('FlashControl destroy error:', JSON.stringify(err));
      } finally {
        this.flashController = null;
      }
    }
  }

  // 处理闪控窗状态变化
  private onFlashWindowStatusChanged(status: FlashWindowStatus) {
    switch (status) {
      case FlashWindowStatus.SHOW:
        // 闪控窗展开,可以在这里刷新 ComponentContent 的数据
        console.info('FlashWindow: expanded');
        break;
      case FlashWindowStatus.HIDE:
        // 闪控窗收起,闪控球继续显示
        console.info('FlashWindow: collapsed, ball visible');
        break;
      case FlashWindowStatus.DESTROY:
        // 系统触发销毁(如用户手动关闭),清理本地引用
        console.info('FlashWindow: destroyed by system');
        this.flashController = null;
        break;
    }
  }
}

4.4 ComponentContent 的构造说明

bindProgressBar 和 bindStartButton 需要传入 ComponentContent<object> 实例。构造 ComponentContent 需要 UIContext 对象,通常从 windowStage.getMainWindowSync().getUIContext() 获取。在 UIAbility.onWindowStageCreate 阶段保存这个引用,是比较可靠的做法:

private uiContext: UIContext | null = null;

onWindowStageCreate(windowStage: window.WindowStage) {
  windowStage.loadContent('pages/Index', (err) => {
    if (err.code) { return; }
    // 保存 UIContext 供后续构造 ComponentContent 使用
    this.uiContext = windowStage.getMainWindowSync().getUIContext();
  });
}

// 在 onBackground 中使用:
async onBackground() {
  if (!this.uiContext || !this.flashController) { return; }

  const progressContent = new ComponentContent(
    this.uiContext,
    wrapBuilder(ProgressBarBuilder),
    { progress: this.currentProgress, color: '#007DFF', label: '下载中...' } as ProgressParams
  );
  const buttonContent = new ComponentContent(
    this.uiContext,
    wrapBuilder(StartButtonBuilder),
    { onPause: () => this.handlePause(), onCancel: () => this.handleCancel() } as ButtonParams
  );

  this.flashController.bindProgressBar(progressContent);
  this.flashController.bindStartButton(buttonContent);
}

4.5 动态更新闪控球进度

下载进度更新时,通过 setFlashBallConfig 把最新进度同步给闪控球:

// 在下载进度回调中调用
async updateDownloadProgress(progress: number) {
  this.currentProgress = progress;
  if (this.flashController) {
    await this.flashController.setFlashBallConfig({
      progressValue: progress,
      progressColor: '#007DFF',
      icon: $r('app.media.startIcon'),
      label: `下载中 ${progress}%`
    });
  }
}

五、几个关键点拆开看

5.1 showWindow 只能在后台调用

这是整个接口设计里最需要关注的约束。showWindow 的设计意图就是服务于后台任务,如果应用仍在前台,调用会报错。因此,showWindow 的调用必须发生在 onBackground 触发之后,不能在前台预先调用。

5.2 闪控球不由应用绘制

FlashBallConfig 只是配置数据(进度值、颜色、图标、标签),闪控球的实际渲染由系统接管。开发者无法自定义小球的形状、位置或交互逻辑,这些都归系统管理。这意味着用户点击小球的行为不需要应用监听,由系统触发闪控窗的展开/收起,并通过 FlashWindowStatus 回调通知应用。

5.3 FlashWindowStatus.DESTROY 不等于应用主动销毁

DESTROY 状态可以由系统主动触发(例如用户手动关闭闪控窗),此时控制器已经失效。如果代码在收到 DESTROY 之后还继续调用 setFlashBallConfig 或 hideWindow,会产生异常。建议在收到 DESTROY 回调时立即将本地 flashController 引用置为 null,并停止后续对控制器的调用。

5.4 组件绑定在显示之前完成

bindProgressBar 和 bindStartButton 需要在 showWindow 之前调用,否则闪控窗展开时内容为空。这两个绑定方法是同步的(返回 void),不需要 await,但顺序不能颠倒。

5.5 仅支持 Phone

这个能力明确只支持 Phone 设备,Tablet、折叠屏大屏模式、2in1 均不在支持范围内。如果应用需要跨设备运行,这里应该做设备类型判断,避免在不支持的设备上调用接口导致异常。

六、容易踩坑的地方

权限未在 module.json5 中声明

ohos.permission.FLASH_CONTROL 缺失时,showWindow 调用会因权限检查失败报错。这个权限不需要动态申请(normal 级别),但必须在 module.json5 的 requestPermissions 中静态声明,漏掉这一步非常常见。

在前台调用 showWindow

如果任务在前台启动,然后立刻调用 showWindow,接口会失败。正确做法是在前台只做初始化准备(创建控制器、绑定组件),等 onBackground 触发后再调用 showWindow。

UIContext 的获取时机

ComponentContent 的构造依赖 UIContext,而 UIContext 在 onWindowStageCreate 之后才可用。如果在 onCreate 阶段就尝试构造 ComponentContent,UIContext 为空,会导致构造失败。建议在 onWindowStageCreate 中保存好 UIContext 引用。

off 的参数是字符串字面量

取消监听的正确写法是 controller.off('flashWindowStatusChange'),注意这里传的是字符串,不是枚举值,写错大小写或者传错参数会导致监听无法取消,内存泄漏风险较高。

七、排查思路

如果闪控窗没有出现,按以下顺序排查:

  1. API 版本:确认工程 compileSdkVersion 和 targetSdkVersion 均 >= 18;
  2. 设备类型:确认运行设备是 Phone,不是模拟器或 Tablet;
  3. 权限:检查 module.json5 中 ohos.permission.FLASH_CONTROL 是否存在;
  4. 调用时机:确认 showWindow 是在 onBackground 触发之后调用的,不是在前台;
  5. 组件绑定顺序:确认 bindProgressBar 和 bindStartButton 在 showWindow 之前调用;
  6. UIContext 是否有效:确认 ComponentContent 构造时传入的 UIContext 不为 null;
  7. 查看控制台日志:FlashControlWindowController.create() 和 showWindow() 都返回 Promise,需要 try/catch 捕获异常,看是否有报错信息。

开发经验总结

  • 闪控球是系统组件,不需要开发者实现;FlashBallConfig 只是向系统传递显示数据,控制权在系统侧。
  • showWindow 的后台限制是整个开发流程中最核心的约束,生命周期管理必须以 onBackground / onForeground 为节点来设计。
  • FlashWindowStatus.DESTROY 是系统主动触发的,与应用调用 destroy() 是两条独立路径,两种情况都需要清理本地引用。
  • ComponentContent 的 UIContext 依赖,建议在 onWindowStageCreate 阶段统一处理好。
  • 该能力当前仅支持 Phone,跨设备应用需要做设备判断。

如果你正在做类似后台任务进度展示的需求,可以思考一下:任务进度回调的更新频率如果很高(比如每秒多次),setFlashBallConfig 是否需要做节流,避免频繁调用影响系统交互体验——这个地方官方文档没有明确限制,但在实际设备上值得关注。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

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

更多推荐