适用版本 :HarmonyOS NEXT / HarmonyOS 7(闪控球 API 20 起支持;闪控窗 API 26.0.0 起支持,仅 Stage 模型)
写给谁看 :需要"应用外继续用"场景的鸿蒙开发者------金融盯盘、直播、巡检、计时类应用尤甚。
要解决的问题 :轻量窗口不是一个东西,而是三个。选错形态,轻则体验割裂,重则权限直接被拒。
目录
-
- 一、先选型:三种窗口,各管一段
- 二、权限与硬约束:这些是"红线",不是"坑"
-
- [2.1 闪控窗的约束](#2.1 闪控窗的约束)
- [2.2 声明并申请权限](#2.2 声明并申请权限)
- 三、闪控窗全生命周期:可运行代码
-
- [3.1 创建与启动](#3.1 创建与启动)
- [3.2 状态回调:这是"启动成功"的唯一证据](#3.2 状态回调:这是"启动成功"的唯一证据)
- [3.3 尺寸限制变化与停止](#3.3 尺寸限制变化与停止)
- [3.4 错误码对照表:把异常分支写全](#3.4 错误码对照表:把异常分支写全)
- [3.5 运行结果与验证方法](#3.5 运行结果与验证方法)
- 四、闪控球:另一种形态,更克制
- 五、三个真实的坑
-
- 1:闪控球是受限权限,不是想用就能用
- [2:`start()` 返回成功 ≠ 已经显示](#2:
start()返回成功 ≠ 已经显示) - [3:把 px 当 vp 用](#3:把 px 当 vp 用)
- 六、选型建议
- 七、适用边界与限制
- 八、小结
- 参考
一、先选型:三种窗口,各管一段
多任务切换是高频场景里最隐蔽的效率杀手:行情突变时手忙脚乱地切 App,等切回来最好的买点已经错过;看直播想瞄一眼弹幕就得离开画面。HarmonyOS 给出的答案是悬浮在应用界面之上的轻量化窗口,但它有三种形态:
| 形态 | 定位 | 展示内容 | 典型场景 | 版本 / 权限 |
|---|---|---|---|---|
| 闪控球 | 少量关键信息,应用外一键操作 | 一小块信息(如涨跌幅、剩余时间) | 抢单、记账、比价、计时 | API 20+;USE_FLOAT_BALL(受限权限) |
| 闪控窗 | 更多关键信息,应用外便捷操作、实时监测 | 信息卡片 + 轻量操作 | 金融盯盘、直播、歌词、巡检 | API 26.0.0+;FLOAT_VIEW(user_grant) |
| 系统悬浮窗 | 承载应用全量功能 | 完整应用界面 | 应用内多功能悬浮 | 视接入方式而定 |
一句话总结:闪控窗承载"单一功能或局部信息",系统悬浮窗承载全量功能。轻量,是前两者的共同标签。
选型不是"哪个更高级",而是"业务到底要展示多少信息"。下面这张决策图把三条分支一次性画清楚,照着走即可:

读这张图有三个要点:第一,"展示什么"决定形态 ------信息量与交互复杂度不同,落点就不同;第二,闪控球多了一道业务白名单 闸门,不通过就只能退回应用内提醒;第三,闪控窗多了一道版本 + 模型闸门,不通过需要降级。这两道闸门是选型阶段最容易被忽略的部分,下文会展开。
二、权限与硬约束:这些是"红线",不是"坑"
2.1 闪控窗的约束
闪控窗从 API 26.0.0 才开始支持,且约束比闪控球更"硬":
- 仅 Stage 模型:相关能力只在 Stage 模型下可用;
- 单应用仅一个 :同一个应用只能启动一个闪控窗,启动多个会返回 错误码 1300033;
- 与其它悬浮窗口互斥 :若已启动闪控球或画中画窗口,必须先停止它们再启动闪控窗,否则报 1300034;
- 仅前台可启动:出于安全考虑,应用必须在前台才能拉起闪控窗;
- 需要用户授权 :
ohos.permission.FLOAT_VIEW是 user_grant 权限,必须声明并申请。
这些写进了官方约束,开发前先看约束表,能省掉一整天调试。
2.2 声明并申请权限
json5
// entry/src/main/module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.FLOAT_VIEW",
"reason": "$string:reason_float_view",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
}
]
}
}
typescript
// 运行时申请(user_grant 权限必须走这一步)
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export async function requestFloatViewPermission(ctx: common.UIAbilityContext): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
try {
const result = await atManager.requestPermissionsFromUser(
ctx, ['ohos.permission.FLOAT_VIEW'] as Permissions[]
);
// authResults[0] === 0 表示已授权
return result.authResults[0] === 0;
} catch (err) {
const e = err as BusinessError;
console.error(`request permission failed: ${e.code} ${e.message}`);
return false;
}
}
权限申请是唯一必须由用户参与 的一步:FLOAT_VIEW 属于 user_grant,不能靠声明静默获得。因此工程上要在调用链最前面就把它处理好,否则后面所有 floatView 调用都会以 201 失败。
三、闪控窗全生命周期:可运行代码
闪控窗挂在 @kit.ArkUI,起始版本 26.0.0 ,系统能力为 SystemCapability.Window.SessionManager。建议先用 canIUse() 判断设备是否支持,再走完整调用链。

生命周期可以概括为五个动作:创建控制器 → 启动 → 更新内容 → 用户交互 → 停止。下面按这条链逐步给代码。
只看调用顺序容易漏掉两个"隐藏步骤",用时序图 表示更清楚------注意"权限申请"必须在 create 之前,而"启动成功"只能由状态回调证明:

图中最关键的一条约定是:start() 的 Promise resolve 只代表请求被受理,真正的成功信号是 onStateChange 回调里的 STARTED。工程上大量"代码没报错但窗口没出来"的问题,根因都落在这条约定上。
3.1 创建与启动
typescript
import { floatView, window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
private floatViewController: floatView.FloatViewController | undefined = undefined;
async function startFloatView(ctx: common.UIAbilityContext): Promise<void> {
// ① 能力开关查询:探测的第一道闸
if (!floatView.isFloatViewEnabled()) {
console.info('float view not supported on this device');
return;
}
try {
// ② 创建控制器:context 必须是有效的 UIAbilityContext
if (!this.floatViewController) {
const config: floatView.FloatViewConfiguration = {
context: ctx,
templateType: floatView.FloatViewTemplateType.ROUNDED_RECTANGLE
};
this.floatViewController = await floatView.create(config);
this.registerStateChangeCallback();
this.registerLimitsChangeCallback();
}
// ③ 挂载内容页:路径必须与 main_pages.json 注册的 src 完全一致,否则 1300016
await this.floatViewController.setUIContext('pages/FloatViewPage');
// ④ 先取尺寸上下限,再设置窗口尺寸(单位是 px,不是 vp)
const limits: floatView.FloatViewLimits =
floatView.getFloatViewLimits(floatView.FloatViewTemplateType.ROUNDED_RECTANGLE);
const size: window.Size = {
width: limits.maxSize.width,
height: limits.maxSize.height
};
await this.floatViewController.setWindowSize(size);
// ⑤ start() 返回不等于成功,必须等 onStateChange 收到 STARTED
await this.floatViewController.start();
console.info('float view start requested');
} catch (err) {
const e = err as BusinessError;
console.error(`create float view failed. code: ${e.code}, message: ${e.message}`);
}
}
预期输出(启动成功后):
text
float view enabled: true
float view state changed: 1 <- STARTED
float view start requested
3.2 状态回调:这是"启动成功"的唯一证据
start() 的 Promise 只代表请求已受理 。真正判定启动成功,要看状态回调是否收到 STARTED;用户关闭窗口时系统也会主动通知,应用侧必须把这个状态接住。
typescript
function registerStateChangeCallback(): void {
this.floatViewController?.onStateChange(
(state: floatView.FloatViewState, stopReason?: floatView.FloatViewStopReason) => {
console.info(`float view state changed: ${state}, reason: ${stopReason}`);
if (state === floatView.FloatViewState.STOPPED) {
// 用户点标题栏 × 关闭时,stopReason 为 TITLE_BAR_STOP_CLICK
// 此处应清理控制器并同步业务状态
this.floatViewController?.offStateChange();
this.floatViewController = undefined;
}
}
);
}
3.3 尺寸限制变化与停止
typescript
function registerLimitsChangeCallback(): void {
this.floatViewController?.onLimitsChange((limits: floatView.FloatViewLimits) => {
// 设备形态变化(如折叠屏展开)时限制可能改变,需动态跟随
console.info(`limits changed: min=${limits.minSize}, max=${limits.maxSize}`);
});
}
async function stopFloatView(): Promise<void> {
await this.floatViewController?.stop();
}
3.4 错误码对照表:把异常分支写全
官方把这些红线直接编码进了错误码,处置方式写在表里,比运行时报错再查快得多:
| 错误码 | 含义 | 处置 |
|---|---|---|
| 201 | FLOAT_VIEW 未授权 |
引导用户到设置页授权 |
| 801 | 设备不支持该能力 | 降级为应用内悬浮或普通页面 |
| 1300016 | 模板类型 / 页面路径 / 窗口尺寸非法 | 核对 main_pages.json 与尺寸范围 |
| 1300030 | 重复操作(重复注册回调、重复启停) | 加状态判断,避免重入 |
| 1300031 | 当前状态不支持该操作 | 按状态机顺序调用 |
| 1300032 | 恢复主窗失败(要求 STARTED 且用户曾点击过) | 检查状态前置条件 |
| 1300033 | 同应用只能启动一个闪控窗 | 复用已有控制器,不要重复 create |
| 1300034 | 与闪控球 / 画中画冲突 | 先停掉其它悬浮窗口 |
把这张表画成决策图,线上排障时就可以顺着 error.code 直接定位到处置动作,不必再回翻文档:

⚠️ 风险提示 :闪控窗与闪控球、画中画互斥。启动路径上必须先清理已有悬浮窗口,否则会稳定复现 1300034。这类错误在"只测一种形态"的开发阶段极易漏掉。
3.5 运行结果与验证方法
代码写完后,按下面三步验证,避免"看起来启动了其实没有":
- 看能力探测 :
isFloatViewEnabled()返回false说明设备不支持,不必继续往下走; - 看状态回调 :日志里出现
state changed: 1(STARTED)才算启动成功;只有float view start requested而没有状态回调,说明启动被拒绝; - 看权限结果 :
authResults[0]为0是已授权、为-1是拒绝;拒绝时要走"引导授权"的兜底路径,而不是反复重试。
开发期最容易踩的验证误区是只看 start() 有没有抛异常------它不抛异常,不代表窗口已经显示在屏幕上。
四、闪控球:另一种形态,更克制
闪控球从 API 20 起支持,入口是 @kit.ArkUI 的 floatingBall。四个模板:STATIC(图标+标题)、NORMAL(标题+内容)、EMPHATIC(图标+标题+内容)、SIMPLE(纯标题)。
它与闪控窗是"胶囊与内容"的关系------点击闪控球展开闪控窗,floatView.bind() 可把两者绑定,用户点"缩小"即可在两个形态间切换。
typescript
import { floatingBall } from '@kit.ArkUI';
import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
if (!floatingBall.isFloatingBallEnabled()) {
return; // 当前设备不支持闪控球
}
const controller: floatingBall.FloatingBallController =
await floatingBall.create({ context: this.getUIContext().getHostContext() });
// 点击回到应用主窗口
controller.on('click', () => {
const want: Want = { bundleName: 'com.example.app', abilityName: 'EntryAbility' };
controller.restoreMainWindow(want).catch((err: BusinessError) => {
console.error(`restore failed: ${err.code} ${err.message}`);
});
});
// STOPPED 时回收控制器,避免持有引用
controller.on('stateChange', (state: floatingBall.FloatingBallState) => {
if (state === floatingBall.FloatingBallState.STOPPED) {
controller.off('click');
controller.off('stateChange');
}
});
const params: floatingBall.FloatingBallParams = {
template: floatingBall.FloatingBallTemplate.EMPHATIC,
title: '25:00',
content: '专注中',
backgroundColor: '#0A84FF'
};
await controller.startFloatingBall(params);
闪控球的规格很克制:整体宽 70--98vp、高 40vp,标题与内容不支持自定义字号 。它是"一眼可见",不是一个迷你页面。上面这段代码里 title: '25:00' 就是典型的计时场景------把剩余时间挂在球上,用户不离开当前应用也能看到进度。
五、三个真实的坑
1:闪控球是受限权限,不是想用就能用
ohos.permission.USE_FLOAT_BALL 目前仅对题目搜索、账单记录、商品比价、抢单、翻译 等场景,以及金融类应用的实时盯盘 场景开放。接入后需在以上场景范围内使用,否则会被限制。做之前先确认你的业务是否在白名单内------这是最容易被忽略、代价也最大的一条。
2:start() 返回成功 ≠ 已经显示
start() 只表示请求被受理。必须等 onStateChange 收到 STARTED 才能认为启动完成;同理,stop() 之后也要靠回调确认状态。以回调为准,而不是以 Promise 为准。
3:把 px 当 vp 用
getFloatViewLimits() 返回的尺寸单位是 px 而非 vp,直接按 vp 的直觉去设置会导致窗口尺寸异常。官方建议:先取 limits,再按范围设置,并监听 onLimitsChange 动态跟随。
六、选型建议
- 只要"一眼可见 + 一键操作"(抢单、比价、计时剩余)→ 闪控球;
- 需要"信息卡片 + 实时监测 + 轻操作"(盯盘、直播、巡检)→ 闪控窗;
- 需要把应用主功能整体悬浮(分屏协作、多任务)→ 系统悬浮窗。
再补一条工程经验:在业务允许的情况下,闪控球 + 闪控窗组合是最优解------平时收成球不打扰,需要时展成窗看细节,两者共用一套控制器生命周期。
七、适用边界与限制
- 闪控窗仅支持 Stage 模型,FA 模型工程需先迁移。
- 未在白名单的业务不要规划闪控球,否则做到了接入也无法合规上线。
- 单实例是硬约束:不要设计"一个应用开多个闪控窗"的产品形态。
- 版本敏感 :闪控窗起始 26.0.0、闪控球起始 20 起,低版本设备必须做
canIUse/isXxxEnabled判断后降级。
八、小结
回到开头:轻量窗口的核心是"克制"------用最小的形态,承载最关键的一小块信息。选型先于编码:先想清楚业务是"一眼可见"还是"实时监测",再决定用球还是用窗;然后老老实实对着约束表和错误码表,把权限、单实例、互斥这三条红线守住。
你的应用适合用哪种形态?欢迎在评论区说说场景,我可以帮你判断该选哪一种。
参考
- 闪控窗开发指导(含
floatView调用链与约束):https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/float-view-guide - 闪控窗 API 参考
@ohos.window.floatView(含错误码与状态枚举):https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/js-apis-floatview?docScope=all - 闪控球开发指导(含受限权限白名单说明):https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/floatingball-guide
- 本次征文活动页面:https://harmonyosdev.csdn.net/6ad1cde9536f457592b05595.html