【鸿蒙心迹】闪控窗开发笔记:悬浮窗的权限申请与生命周期管理
【鸿蒙心迹】闪控窗开发笔记:悬浮窗的权限申请与生命周期管理
摘要:本文围绕鸿蒙闪控窗(系统级悬浮窗)的开发实践展开,先厘清闪控窗与普通窗口在显示层级、生命周期、权限要求上的核心差异,再拆解「安装时声明 + 运行时引导用户手动开启」的两级权限体系与窗口管理核心 API,最后以悬浮操作按钮为例,完整走通权限声明、权限校验、窗口创建、UI 加载、拖动交互、窗口销毁六个步骤,并总结权限被拒、窗口不显示、应用退出后残留、点击事件穿透四类高频坑的排查思路。适合有安卓悬浮窗经验、想在鸿蒙上实现类似能力的开发者参考。
做工具类应用的开发者,迟早会遇到一个需求:在别的应用上面显示一个小窗口——可能是悬浮按钮、可能是通知弹窗、可能是实时数据浮层。在安卓上这叫悬浮窗(System Alert Window),在鸿蒙上叫闪控窗。名字不同,踩的坑一样多:权限申请被拒、窗口创建后不显示、生命周期管理混乱导致窗口泄漏。
如果你有安卓悬浮窗开发经验,或者想在鸿蒙上实现类似功能,这篇笔记会帮你少走很多弯路。读完后你会掌握:闪控窗的概念和权限体系、窗口创建与销毁的完整流程、生命周期管理的最佳实践。
本文分三层:第一层建立闪控窗的整体认知;第二层拆解权限和生命周期原理;第三层是完整的实战落地和踩坑排错。
入门认知:闪控窗是什么
一句话定义
闪控窗是鸿蒙提供的系统级悬浮窗口能力——应用可以在其他应用之上显示一个透明或半透明的窗口,不受当前前台应用的影响。
打个比方:普通应用窗口就像舞台上的演员,只能在轮到自己时才上台表演;闪控窗就像舞台旁边的字幕屏,不管台上谁在表演,字幕始终悬浮在最前面显示。
闪控窗 vs 普通窗口
| 对比维度 | 普通窗口 | 闪控窗 |
|---|---|---|
| 显示层级 | 应用自己的窗口层级 | 系统最顶层,覆盖所有应用 |
| 生命周期 | 随应用前后台切换 | 独立于应用生命周期 |
| 用户感知 | 应用内可见 | 全局可见,跨应用 |
| 权限要求 | 无特殊权限 | 需要系统悬浮窗权限 |
| 典型场景 | 页面、对话框 | 悬浮按钮、通知浮层、实时数据 |
表1:普通窗口与闪控窗的核心差异
全景图示
图1:闪控窗的完整生命周期链路
适用边界
闪控窗适合:全局通知、悬浮操作按钮、实时数据监控浮层、来电弹窗。不适合:应用内的对话框(用普通弹窗即可)、全屏覆盖(用Ability切换更合适)、大量交互内容(闪控窗应保持轻量)。
进阶原理:权限体系与窗口管理
闪控窗的权限机制
闪控窗涉及两级权限:
| 权限级别 | 权限名 | 申请方式 | 说明 |
|---|---|---|---|
| 安装时 | ohos.permission.SYSTEM_FLOAT_WINDOW | module.json5声明 | 基础权限声明 |
| 运行时 | 悬浮窗授权 | 设置页引导 | 用户需手动在设置中开启 |
表2:闪控窗的两级权限
[注意] 鸿蒙的悬浮窗权限和安卓不同——安卓用 SYSTEM_ALERT_WINDOW 可以运行时弹框申请,鸿蒙必须引导用户到系统设置中手动开启,不能弹框直接申请。这是安全策略的差异。
两级权限的申请与校验流程如下:
图2:闪控窗两级权限的申请与校验流程
窗口管理的核心API
鸿蒙窗口管理通过 window 模块实现,闪控窗相关的核心API:
| API | 作用 | 调用时机 |
|---|---|---|
| window.createWindow | 创建窗口 | 初始化闪控窗 |
| window.setWindowProperties | 设置位置/大小/透明度 | 创建后配置 |
| window.setUIContent | 加载UI内容 | 创建后加载 |
| window.showWindow | 显示窗口 | 配置完成后 |
| window.destroyWindow | 销毁窗口 | 不再需要时 |
表3:闪控窗核心API
生命周期管理原理
闪控窗的生命周期独立于应用Ability的生命周期,这是它最强大也最危险的特点:
图2:闪控窗生命周期与应用生命周期的关系
[坑] 闪控窗不会随应用销毁自动消失!如果应用被杀但闪控窗没销毁,窗口会残留在屏幕上变成"幽灵窗口",只有重启设备才能清除。这是新手最容易犯的严重错误。
常见误区
误区1:闪控窗可以随意创建多个
每创建一个闪控窗都会占用系统窗口资源,创建过多会导致系统窗口管理器压力过大,新窗口可能创建失败。建议同时不超过2-3个闪控窗。
误区2:闪控窗的UI和普通页面一样写
闪控窗的UI应该极简——一个按钮、一段文字、一个小图标。不要在闪控窗里放复杂列表、嵌套滚动、大量动画,这会导致严重的性能问题和用户体验灾难。
高级应用:实现一个悬浮操作按钮
实战步骤
第一步:声明权限
module.json5:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
"reason": "$string:float_window_reason",
"usedScene": {
"abilities": ["MainAbility"],
"when": "always"
}
}
]
}
}
第二步:检查并引导授权
import { window } from '@kit.ArkUI'
import { abilityAccessCtrl } from '@kit.AbilityKit'
async function checkFloatWindowPermission(): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager()
const tokenId = await atManager.getAccessTokenId()
try {
const status = await atManager.checkAccessToken(tokenId, 'ohos.permission.SYSTEM_FLOAT_WINDOW')
if (status === 0) {
return true
}
// 权限未授予,引导用户到设置页
return false
} catch (error) {
console.error(`权限检查失败: ${error}`)
return false
}
}
[注意] checkAccessToken 返回0表示已授权,非0表示未授权。不要用 grantAccessToken 尝试自动授权——悬浮窗权限不支持运行时弹框授权,必须引导用户手动到设置中开启。
第三步:创建闪控窗
let floatWindow: window.Window | null = null
async function createFloatWindow() {
// 检查权限
const hasPermission = await checkFloatWindowPermission()
if (!hasPermission) {
// 引导用户到设置页开启悬浮窗权限
promptAction.showToast({ message: '请到设置中开启悬浮窗权限' })
return
}
// 创建窗口
const config: window.Configuration = {
name: 'float_button',
windowType: window.WindowType.TYPE_FLOAT,
ctx: this.context
}
floatWindow = await window.createWindow(config)
// 设置窗口属性
await floatWindow.setWindowProperties({
width: 80,
height: 80,
transparent: true,
outsideTouchable: true
})
// 加载UI内容
await floatWindow.setUIContent('pages/FloatButtonPage')
// 显示窗口
await floatWindow.showWindow()
}
关键参数解释:
windowType: TYPE_FLOAT— 声明为闪控窗类型,这是最关键的参数transparent: true— 窗口背景透明,只显示UI内容部分outsideTouchable: true— 窗口外的区域可以触摸穿透,不影响下层应用操作
第四步:闪控窗的UI页面
FloatButtonPage.ets:
@Entry
@Component
struct FloatButtonPage {
build() {
Stack() {
Circle({ width: 60, height: 60 })
.fill('#007DFF')
.shadow({ radius: 8, color: '#40007DFF', offsetX: 0, offsetY: 2 })
Image($r('app.media.float_icon'))
.width(30)
.height(30)
}
.width(80)
.height(80)
.onClick(() => {
// 点击悬浮按钮,拉起主应用
// 或者切换悬浮窗内容
})
}
}
第五步:窗口拖动
悬浮按钮通常需要支持拖动,让用户调整位置:
let lastX: number = 0
let lastY: number = 0
// 在FloatButtonPage中添加拖动手势
.gesture(
PanGesture()
.onActionStart((event: GestureEvent) => {
lastX = event.fingerList[0].localX
lastY = event.fingerList[0].localY
})
.onActionUpdate((event: GestureEvent) => {
const deltaX = event.fingerList[0].localX - lastX
const deltaY = event.fingerList[0].localY - lastY
// 更新窗口位置
floatWindow?.setWindowProperties({
posX: currentPosX + deltaX,
posY: currentPosY + deltaY
})
})
)
第六步:销毁窗口(关键!)
async function destroyFloatWindow() {
if (floatWindow) {
await floatWindow.destroyWindow()
floatWindow = null
}
}
[坑] destroyWindow 是异步的,调用后不要立即创建同名窗口——系统需要时间清理窗口资源。建议销毁后至少等待100ms再创建新窗口,否则可能报 “window already exists”。
把六个步骤串起来,完整的创建与销毁流程如下:
图3:悬浮操作按钮的完整实现流程
踩坑排错
坑1:createWindow报 “permission denied”
现象:调用 window.createWindow 直接抛权限错误。
原因:SYSTEM_FLOAT_WINDOW 权限未授予。
解决:先调用 checkAccessToken 检查权限,未授权时引导用户到设置页。不要假设权限已授予。
坑2:窗口创建成功但不显示
现象:createWindow 和 showWindow 都没报错,但屏幕上看不到任何东西。
排查步骤:
- 检查
transparent是否设为true——如果窗口透明但UI内容也是透明的,就什么都看不到 - 检查窗口位置
posX/posY——可能位置在屏幕外 - 检查
setUIContent的页面路径是否正确——路径错误时窗口存在但内容为空 - 检查UI页面是否有实际可见内容——Stack里如果只有透明元素也看不到
坑3:应用退出后闪控窗残留
现象:用户从最近任务列表划掉应用,但悬浮按钮还在屏幕上。
原因:应用被系统杀死时,onDestroy 不一定被调用,闪控窗没有被销毁。
解决:在 onBackground 中隐藏窗口(而非销毁),在应用重新前台时再显示。同时注册 application.onWindowStageDestroy 回调,在窗口阶段销毁时清理闪控窗:
windowStage.getMainWindow().on('windowStageDestroy', () => {
destroyFloatWindow()
})
坑4:闪控窗内点击事件穿透到下层应用
现象:点击悬浮按钮时,下层应用也响应了点击。
原因:outsideTouchable 设为true导致整个窗口区域都允许穿透,包括UI内容区域。
解决:outsideTouchable 只控制窗口透明区域的穿透行为。UI内容区域的点击不会穿透。如果仍然穿透,检查UI页面是否正确设置了 onClick 事件——没有onClick的区域才会穿透。
四个高频坑的排查路径可以归纳为下面这棵决策树:
图4:闪控窗常见问题的排查决策树
最佳实践
- 权限检查先行:每次创建闪控窗前检查权限,不要假设已授权
- UI保持极简:闪控窗只放一个按钮或一段文字,不要复杂交互
- 生命周期绑定:在onBackground隐藏、onForeground显示、onWindowStageDestroy销毁
- 拖动支持:悬浮按钮必须支持拖动,否则遮挡用户操作
- 销毁要彻底:destroyWindow后置null,避免引用残留
- 窗口数量控制:同时不超过2-3个闪控窗
总结
核心要点:
- 闪控窗是系统级悬浮窗口,独立于应用生命周期,跨应用显示
- 权限分两级:安装时声明 + 运行时引导用户手动开启,不能弹框直接申请
- 闪控窗不会随应用销毁自动消失——必须在生命周期回调中主动销毁,否则产生幽灵窗口
- UI必须极简,复杂交互会导致性能问题和体验灾难
最佳实践清单:
- 创建前检查权限,未授权引导到设置页
- UI只放一个按钮或一段文字
- onBackground隐藏、onForeground显示、onWindowStageDestroy销毁
- 悬浮按钮支持拖动
- destroyWindow后置null
- 同时不超过2-3个闪控窗
你在闪控窗开发中遇到过什么坑?评论区分享,一起把悬浮窗做稳。
更多推荐




所有评论(0)