HarmonyOS 闪控球 + 闪控窗:两种悬浮形态如何完成状态切换【鸿蒙心迹】

👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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'),注意这里传的是字符串,不是枚举值,写错大小写或者传错参数会导致监听无法取消,内存泄漏风险较高。
七、排查思路
如果闪控窗没有出现,按以下顺序排查:
- API 版本:确认工程 compileSdkVersion 和 targetSdkVersion 均 >= 18;
- 设备类型:确认运行设备是 Phone,不是模拟器或 Tablet;
- 权限:检查
module.json5中ohos.permission.FLASH_CONTROL是否存在; - 调用时机:确认
showWindow是在onBackground触发之后调用的,不是在前台; - 组件绑定顺序:确认
bindProgressBar和bindStartButton在showWindow之前调用; - UIContext 是否有效:确认
ComponentContent构造时传入的UIContext不为 null; - 查看控制台日志:
FlashControlWindowController.create()和showWindow()都返回Promise,需要try/catch捕获异常,看是否有报错信息。
开发经验总结
- 闪控球是系统组件,不需要开发者实现;
FlashBallConfig只是向系统传递显示数据,控制权在系统侧。 showWindow的后台限制是整个开发流程中最核心的约束,生命周期管理必须以onBackground/onForeground为节点来设计。FlashWindowStatus.DESTROY是系统主动触发的,与应用调用destroy()是两条独立路径,两种情况都需要清理本地引用。ComponentContent的UIContext依赖,建议在onWindowStageCreate阶段统一处理好。- 该能力当前仅支持 Phone,跨设备应用需要做设备判断。
如果你正在做类似后台任务进度展示的需求,可以思考一下:任务进度回调的更新频率如果很高(比如每秒多次),setFlashBallConfig 是否需要做节流,避免频繁调用影响系统交互体验——这个地方官方文档没有明确限制,但在实际设备上值得关注。
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。
更多推荐




所有评论(0)