适用版本:HarmonyOS NEXT / HarmonyOS 7(闪控球 API 20 起支持;闪控窗 API 26.0.0 起支持,仅 Stage 模型)
写给谁看:需要"应用外继续用"场景的鸿蒙开发者——金融盯盘、直播、巡检、计时类应用尤甚。
要解决的问题:轻量窗口不是一个东西,而是三个。选错形态,轻则体验割裂,重则权限直接被拒。
目录
- 一、先选型:三种窗口,各管一段
- 二、权限与硬约束:这些是"红线",不是"坑"
- 2.1 闪控窗的约束
- 2.2 声明并申请权限
- 三、闪控窗全生命周期:可运行代码
- 3.1 创建与启动
- 3.2 状态回调:这是"启动成功"的唯一证据
- 3.3 尺寸限制变化与停止
- 3.4 错误码对照表:把异常分支写全
- 3.5 运行结果与验证方法
- 四、闪控球:另一种形态,更克制
- 五、三个真实的坑
- 1:闪控球是受限权限,不是想用就能用
- 2:`start()` 返回成功 ≠ 已经显示
- 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 声明并申请权限
// entry/src/main/module.json5 { "module": { "requestPermissions": [ { "name": "ohos.permission.FLOAT_VIEW", "reason": "$string:reason_float_view", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ] } }// 运行时申请(user_grant 权限必须走这一步)import{abilityAccessCtrl,bundleManager,common,Permissions}from'@kit.AbilityKit';import{BusinessError}from'@kit.BasicServicesKit';exportasyncfunctionrequestFloatViewPermission(ctx:common.UIAbilityContext):Promise<boolean>{constatManager=abilityAccessCtrl.createAtManager();try{constresult=awaitatManager.requestPermissionsFromUser(ctx,['ohos.permission.FLOAT_VIEW']asPermissions[]);// authResults[0] === 0 表示已授权returnresult.authResults[0]===0;}catch(err){conste=errasBusinessError;console.error(`request permission failed:${e.code}${e.message}`);returnfalse;}}权限申请是唯一必须由用户参与的一步:FLOAT_VIEW属于user_grant,不能靠声明静默获得。因此工程上要在调用链最前面就把它处理好,否则后面所有floatView调用都会以201失败。
三、闪控窗全生命周期:可运行代码
闪控窗挂在@kit.ArkUI,起始版本26.0.0,系统能力为SystemCapability.Window.SessionManager。建议先用canIUse()判断设备是否支持,再走完整调用链。
生命周期可以概括为五个动作:创建控制器 → 启动 → 更新内容 → 用户交互 → 停止。下面按这条链逐步给代码。
只看调用顺序容易漏掉两个"隐藏步骤",用时序图表示更清楚——注意"权限申请"必须在create之前,而"启动成功"只能由状态回调证明:
图中最关键的一条约定是:start()的 Promise resolve 只代表请求被受理,真正的成功信号是onStateChange回调里的STARTED。工程上大量"代码没报错但窗口没出来"的问题,根因都落在这条约定上。
3.1 创建与启动
import{floatView,window}from'@kit.ArkUI';import{common}from'@kit.AbilityKit';import{BusinessError}from'@kit.BasicServicesKit';privatefloatViewController:floatView.FloatViewController|undefined=undefined;asyncfunctionstartFloatView(ctx:common.UIAbilityContext):Promise<void>{// ① 能力开关查询:探测的第一道闸if(!floatView.isFloatViewEnabled()){console.info('float view not supported on this device');return;}try{// ② 创建控制器:context 必须是有效的 UIAbilityContextif(!this.floatViewController){constconfig:floatView.FloatViewConfiguration={context:ctx,templateType:floatView.FloatViewTemplateType.ROUNDED_RECTANGLE};this.floatViewController=awaitfloatView.create(config);this.registerStateChangeCallback();this.registerLimitsChangeCallback();}// ③ 挂载内容页:路径必须与 main_pages.json 注册的 src 完全一致,否则 1300016awaitthis.floatViewController.setUIContext('pages/FloatViewPage');// ④ 先取尺寸上下限,再设置窗口尺寸(单位是 px,不是 vp)constlimits:floatView.FloatViewLimits=floatView.getFloatViewLimits(floatView.FloatViewTemplateType.ROUNDED_RECTANGLE);constsize:window.Size={width:limits.maxSize.width,height:limits.maxSize.height};awaitthis.floatViewController.setWindowSize(size);// ⑤ start() 返回不等于成功,必须等 onStateChange 收到 STARTEDawaitthis.floatViewController.start();console.info('float view start requested');}catch(err){conste=errasBusinessError;console.error(`create float view failed. code:${e.code}, message:${e.message}`);}}预期输出(启动成功后):
float view enabled: true float view state changed: 1 <- STARTED float view start requested3.2 状态回调:这是"启动成功"的唯一证据
start()的 Promise 只代表请求已受理。真正判定启动成功,要看状态回调是否收到STARTED;用户关闭窗口时系统也会主动通知,应用侧必须把这个状态接住。
functionregisterStateChangeCallback():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 尺寸限制变化与停止
functionregisterLimitsChangeCallback():void{this.floatViewController?.onLimitsChange((limits:floatView.FloatViewLimits)=>{// 设备形态变化(如折叠屏展开)时限制可能改变,需动态跟随console.info(`limits changed: min=${limits.minSize}, max=${limits.maxSize}`);});}asyncfunctionstopFloatView():Promise<void>{awaitthis.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()可把两者绑定,用户点"缩小"即可在两个形态间切换。
import{floatingBall}from'@kit.ArkUI';import{Want}from'@kit.AbilityKit';import{BusinessError}from'@kit.BasicServicesKit';if(!floatingBall.isFloatingBallEnabled()){return;// 当前设备不支持闪控球}constcontroller:floatingBall.FloatingBallController=awaitfloatingBall.create({context:this.getUIContext().getHostContext()});// 点击回到应用主窗口controller.on('click',()=>{constwant: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');}});constparams:floatingBall.FloatingBallParams={template:floatingBall.FloatingBallTemplate.EMPHATIC,title:'25:00',content:'专注中',backgroundColor:'#0A84FF'};awaitcontroller.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