鸿蒙系统闪控球和闪控窗功能详解与开发适配指南
一、闪控球
1.1 场景与定位
闪控球是一种在设备屏幕上悬浮的非全屏应用窗口,以小圆球形式呈现,用于展示少量关键信息并支持应用外一键操作,点击后可自动拉起原应用对应界面。其设计目标是实现跨应用快捷操作,典型场景包括:
- 抢单
- 记账
- 比价
- 翻译
- 题目搜索
- 账单记录
闪控球适用于需要快速触达核心功能、但无需长时间驻留大量内容的交互模式。
1.2 基础交互
- 创建:应用接入闪控球功能后,用户可通过应用内指定入口手动开启闪控球。点击闪控球,跳转至原应用对应界面。
- 数量限制:系统最多支持 2 个闪控球,一个应用内仅支持创建 1 个闪控球。超出最大个数限制时,新闪控球会替换最早启动的闪控球。
- 最小化:支持拖拽收入侧边条。
- 删除:用户可将单个或多个闪控球整体拖拽至底部垃圾桶删除,也可以长按后通过菜单删除。
- 位置记忆:关闭闪控球会记录当前位置,下一次打开功能时自动展示在上次关闭时的位置。旋转屏幕或重启设备会恢复到默认位置,默认位置位于屏幕右上侧。
1.3 视觉规格
1.3.1 模板结构
闪控球提供四种固定模板,应用可根据业务诉求选择接入(模板由系统管理,应用不能定制 UI):
| 模板类型 | 布局组成 | 说明 |
|---|---|---|
| 静态布局(STATIC) | 图标 + 标题 | 支持图标和标题,创建后无法更新内容 |
| 普通文本布局(NORMAL) | 标题 + 内容 | 支持标题和内容,可动态更新 |
| 强调文本布局(EMPHATIC) | 图标 + 标题 + 内容 | 支持图标、标题和内容,可动态更新 |
| 纯文本布局(SIMPLE) | 仅标题(可双行展示) | 只支持标题,可动态更新 |
标题和内容不支持自定义字体大小,由系统统一控制。
1.3.2 尺寸规格
闪控球宽度根据传入内容自动切换档位,高度固定不变。不同设备类型的尺寸(包含底板)如下:
| 设备类型 | 默认尺寸 (vp) | 档位1 | 档位2 | 档位3 | 档位4 |
|---|---|---|---|---|---|
| 直板机(竖屏/横屏) | 70×40 | 70×40 | 80×40 | 90×40 | 98×40 |
| 双折叠展开态 | 70×40 | 70×40 | 80×40 | 90×40 | 98×40 |
| 三折叠展开态 | 70×40 | 70×40 | 80×40 | 90×40 | 98×40 |
| 平板/电脑 | 104×60 | 104×60 | 114×60 | 124×60 | 132×60 |
- 泛手机设备(直板机、折叠屏展开态)内部底板距离左右边距为 4vp;平板/电脑为 6vp。
- 当两个应用闪控球合并展示时,整体高度为 76vp(手机规格)。
1.4 接口说明(@ohos.window.floatingBall)
| 接口 | 描述 |
|---|---|
isFloatingBallEnabled(): boolean | 判断当前设备是否支持闪控球功能 |
create(config: FloatingBallConfiguration): Promise<FloatingBallController> | 创建闪控球控制器 |
startFloatingBall(params: FloatingBallParams): Promise<void> | 启动闪控球 |
updateFloatingBall(params: FloatingBallParams): Promise<void> | 更新闪控球信息 |
stopFloatingBall(): Promise<void> | 停止闪控球 |
on(type: 'stateChange', callback: Callback<FloatingBallState>): void | 开启闪控球生命周期状态监听 |
off(type: 'stateChange', callback?: Callback<FloatingBallState>): void | 关闭闪控球生命周期状态监听 |
on(type: 'click', callback: Callback<void>): void | 开启闪控球点击事件监听 |
off(type: 'click', callback?: Callback<void>): void | 关闭闪控球点击事件监听 |
getFloatingBallWindowInfo(): Promise<FloatingBallWindowInfo> | 获取闪控球窗口信息 |
restoreMainWindow(want: Want): Promise<void> | 恢复应用主窗口,加载指定页面 |
1.5 完整开发步骤与示例代码
1.5.1 权限与约束
- 必须申请 ohos.permission.USE_FLOAT_BALL 权限(受限权限,需在指定场景下使用)。
- 仅允许应用在前台时启动闪控球。
- 支持 Phone、Tablet、PC/2in1 设备,从 API version 20 开始支持。
- 需 DevEco Studio 6.0.1 Release 及以上版本的模拟器支持。
1.5.2 工具类封装(Utils.ts)
以下代码封装了闪控球的创建、更新、停止逻辑,以及图标加载工具方法。
// Utils.ts
// 该页面提供工具类,展示闪控球的创建、更新、关闭逻辑
import hilog from '@ohos.hilog';
import image from '@ohos.multimedia.image';
import { BusinessError } from '@kit.BasicServicesKit';
import { floatingBall } from '@kit.ArkUI';
import { Want } from '@kit.AbilityKit';
import { ContextUtil } from './ContextUtil';
const DOMAIN: number = 0xF811;
const TAG: string = '[Sample_FloatingBall]';
const BUNDLE_NAME: string = ContextUtil.context.abilityInfo.bundleName;
export class Utils {
public static getRawfilePixelMapSync(path: string): image.PixelMap {
try {
const BUFFER = ContextUtil.context.resourceManager.getRawFileContentSync(path);
const IMAGE_SOURCE: image.ImageSource = image.createImageSource(BUFFER.buffer as ArrayBuffer);
hilog.debug(DOMAIN, TAG, `Get rawfile pixelMap path '${path}' successfully`);
return IMAGE_SOURCE.createPixelMapSync();
} catch (e) {
hilog.error(DOMAIN, TAG, `Get rawfile pixelMap path '${path}' failed, error: ${e}`);
throw e as Error;
}
}
// 闪控球启动逻辑
public static async onClickCreateFloatingBall(
floatingBallController: floatingBall.FloatingBallController | undefined,
template: floatingBall.FloatingBallTemplate,
onActiveRowChange: (value: number) => void,
title: string = 'title',
content: string = 'content',
backgroundColor: string = '#0ff77c',
icon?: image.PixelMap): Promise<void> {
// 注册点击回调事件
try {
floatingBallController?.on('click', () => {
hilog.debug(DOMAIN, TAG, `FloatingBall onClickEvent`);
let want: Want = {
bundleName: BUNDLE_NAME,
abilityName: 'MainAbility'
}
// 使用promise异步回调恢复主窗口
floatingBallController?.restoreMainWindow(want)
.then(() => {
hilog.debug(DOMAIN, TAG, `Success in restoring FloatingBall main window`);
}).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG, `failed to restore FloatingBall main window. code: ${err.code}, message: ${err.message}`);
})
})
} catch (e) {
hilog.error(DOMAIN, TAG, `Failed to register click listener: ${e}`);
}
// 注册状态变化事件
try {
floatingBallController?.on('stateChange',
(state: floatingBall.FloatingBallState) => {
hilog.debug(DOMAIN, TAG, `FloatingBall stateCange: ${state}`);
if(state === floatingBall.FloatingBallState.STOPPED) {
floatingBallController?.off('click')
floatingBallController?.off('stateChange')
floatingBallController = undefined;
// 执行状态更新回调
onActiveRowChange?.(-1);
}
})
} catch (e) {
hilog.error(DOMAIN, TAG, `Failed to register stateChange listener: ${e}`);
}
// 最后启动闪控球
let startParams: floatingBall.FloatingBallParams = icon? {
template: template,
title: title,
content: content,
backgroundColor: backgroundColor,
icon: icon
} : {
template: template,
title: title,
content: content,
backgroundColor: backgroundColor
}
try {
floatingBallController?.startFloatingBall(startParams)
.then(() => {
hilog.debug(DOMAIN, TAG, `succeed in starting FloatingBall`);
}).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG, `failed to start FloatingBall. code: ${err.code}, message: ${err.message}`);
})
} catch (e) {
console.error('startFloatingBall Error', e)
}
}
// 闪控球更新逻辑
public static onClickUpdateFloatingBall(
floatingBallController: floatingBall.FloatingBallController | undefined,
template: floatingBall.FloatingBallTemplate,
title: string = 'newTitle',
content: string = 'newContent',
icon?: image.PixelMap): void {
// 更新时给标题、内容随机使用数字后缀
let random_string: string = Math.floor(Math.random() * 100).toString();
let updateParams: floatingBall.FloatingBallParams = icon ? {
template: template,
title: title + random_string,
content: content + random_string,
backgroundColor: '#f6ea0a',
icon: icon
} : {
template: template,
title: title + random_string,
content: content + random_string,
backgroundColor: '#f6ea0a',
}
try {
floatingBallController?.updateFloatingBall(updateParams).then(() => {
hilog.debug(DOMAIN, TAG, `Succeed in updating FloatingBall`);
}).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG, `failed to update FloatingBall. code: ${err.code}, message: ${err.message}`);
})
} catch (e) {
console.error('updateFloatingBall Error:', e)
}
}
// 闪控球停止逻辑
public static onClickStopFloatingBall(floatingBallController: floatingBall.FloatingBallController | undefined): void {
// stop 是异步流程,需要通过 stateChange 状态回调获取实际删除结果
floatingBallController?.stopFloatingBall().then(() => {
hilog.debug(DOMAIN, TAG, `Succeed in stopping FloatingBall`);
}).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG, `failed to stop FloatingBall. code: ${err.code}, message: ${err.message}`);
})
}
}
1.5.3 主页面实现(Index.ets)
以下页面通过按钮展示四种模板的创建、更新与关闭操作,并管理闪控球控制器的生命周期。
// Index.ets
// 该页面利用按钮点击事件展示闪控球基本操作
import hilog from '@ohos.hilog';
import image from '@ohos.multimedia.image';
import { floatingBall } from '@kit.ArkUI';
import { Utils } from '../util/Utils';
const DOMAIN: number = 0xF811;
const TAG: string = '[Sample_FloatingBall]';
@Entry
@Component
struct Index {
// 当前可用的行,-1 表示全部行可见
@State private activeRow: number = -1;
// 声明闪控球控制器
private floatingBallController: floatingBall.FloatingBallController | undefined = undefined;
// 缓存 icon 图标(静态布局)
private cachedIcon1: image.PixelMap | undefined = undefined;
// 缓存 icon 图标(强调文本布局)
private cachedIcon2: image.PixelMap | undefined = undefined;
// activeRow 的状态更新函数(确保闪控球销毁时,activeRow的值更新为-1)
private activeRowChange = (value: number) => {this.activeRow = value};
// 判断某个布局是否可用(是否置灰)
private isEnabled(rowInex: number): boolean {
return this.activeRow === -1 || this.activeRow === rowInex;
}
build() {
Column({space: 12}) {
// 静态布局,支持标题和图标,该布局在创建后无法修改
Row({space: 6}) {
Button('STATIC').onClick( async () => {
// 请在组件内获取context,确保this.getUIContext().getHostContext()返回的结果是UIAbilityContext
if (!this.floatingBallController) {
this.floatingBallController = await floatingBall.create({
context: this.getUIContext().getHostContext()
})
}
if (this.floatingBallController) {
// 仅当没有缓存 cachedIcon1 时才加载;有缓存时,直接使用;
if (!this.cachedIcon1) {
let pixelMap = Utils.getRawfilePixelMapSync('books.png'); // 图片尺寸有最大限制
if (pixelMap) {
this.cachedIcon1 = pixelMap; // 把图标缓存起了
hilog.debug(DOMAIN, TAG, `Success to load icon PixelMap`);
} else {
hilog.error(DOMAIN, TAG, `Failed to load icon PixelMap`);
}
}
Utils.onClickCreateFloatingBall(this.floatingBallController,
floatingBall.FloatingBallTemplate.STATIC, this.activeRowChange, 'title', 'content', '#0ff77c', this.cachedIcon1)
this.activeRow = 0;
}
})
.enabled(this.isEnabled(0))
// 更新闪控球信息(该布局在创建后无法更新,按钮永久置灰)
Button('Update1').enabled(false)
// 关闭闪控球
Button('Close1').onClick(() => {
Utils.onClickStopFloatingBall(this.floatingBallController);
this.activeRow = -1; // 关闭后恢复所有行显示
})
.enabled(this.isEnabled(0))
}
.width('100%')
.justifyContent(FlexAlign.Center)
// 普通文本布局,支持标题和内容
Row({space: 6}) {
Button('NORMAL').onClick( async () => {
if (!this.floatingBallController) {
this.floatingBallController = await floatingBall.create({
context: this.getUIContext().getHostContext()
})
}
if (this.floatingBallController) {
Utils.onClickCreateFloatingBall(this.floatingBallController,
floatingBall.FloatingBallTemplate.NORMAL, this.activeRowChange, 'title', 'content')
this.activeRow = 1;
}
})
.enabled(this.isEnabled(1))
// 更新闪控球信息
Button('Update2').onClick(() => Utils.onClickUpdateFloatingBall(this.floatingBallController,
floatingBall.FloatingBallTemplate.NORMAL))
.enabled(this.isEnabled(1))
// 关闭闪控球
Button('Close2').onClick(() => {
Utils.onClickStopFloatingBall(this.floatingBallController);
this.activeRow = -1;
})
.enabled(this.isEnabled(1))
}
.width('100%')
.justifyContent(FlexAlign.Center)
// 强调文本布局,支持标题、图标和内容
Row({space: 6}) {
Button('EMPHATIC').onClick( async () => {
if (!this.floatingBallController) {
this.floatingBallController = await floatingBall.create({
context: this.getUIContext().getHostContext()
})
}
if (this.floatingBallController) {
if(!this.cachedIcon2) {
let pixelMap = Utils.getRawfilePixelMapSync('video.png');
if (pixelMap) {
this.cachedIcon2 = pixelMap;
hilog.debug(DOMAIN, TAG, `Success to load icon PixelMap`);
} else {
hilog.debug(DOMAIN, TAG, `Failed to load icon PixelMap`);
}
}
Utils.onClickCreateFloatingBall(this.floatingBallController,
floatingBall.FloatingBallTemplate.EMPHATIC, this.activeRowChange, '16', 'Min', '#0ff77c', this.cachedIcon2)
this.activeRow = 2;
}
})
.enabled(this.isEnabled(2))
// 更新闪控球信息
Button('Update3').onClick(() => Utils.onClickUpdateFloatingBall(this.floatingBallController,
floatingBall.FloatingBallTemplate.EMPHATIC, '', 'Min', this.cachedIcon2))
.enabled(this.isEnabled(2))
// 关闭闪控球
Button('Close3').onClick(() => {
Utils.onClickStopFloatingBall(this.floatingBallController);
this.activeRow = -1;
})
.enabled(this.isEnabled(2))
}
.width('100%')
.justifyContent(FlexAlign.Center)
// 纯文本布局,只支持标题
Row({space: 6}) {
Button('SIMPLE').onClick( async () => {
if (!this.floatingBallController) {
this.floatingBallController = await floatingBall.create({
context: this.getUIContext().getHostContext()
})
}
if (this.floatingBallController) {
Utils.onClickCreateFloatingBall(this.floatingBallController,
floatingBall.FloatingBallTemplate.SIMPLE, this.activeRowChange, 'title')
this.activeRow = 3;
}
})
.enabled(this.isEnabled(3))
// 更新闪控球信息
Button('Update4').onClick(() => Utils.onClickUpdateFloatingBall(this.floatingBallController,
floatingBall.FloatingBallTemplate.SIMPLE))
.enabled(this.isEnabled(3))
// 关闭闪控球
Button('Close4').onClick(() => {
Utils.onClickStopFloatingBall(this.floatingBallController);
this.activeRow = -1;
})
.enabled(this.isEnabled(3))
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
1.5.4 关键实现说明
- 控制器创建:通过
floatingBall.create({ context: this.getUIContext().getHostContext() })获取控制器,context 必须是UIAbilityContext。 - 事件注册:在启动前注册
click和stateChange回调。点击回调中调用restoreMainWindow恢复主界面;状态回调中监听到STOPPED时需清理监听并置空控制器。 - 图标缓存:为避免重复加载,将
PixelMap缓存为成员变量。图片尺寸有最大限制,建议使用合适尺寸的资源。 - 模板更新限制:
STATIC模板创建后不可更新,因此对应更新按钮永久置灰。 - 行状态管理:
activeRow控制当前激活的模板行,同一时间只允许一个闪控球处于活动状态,其他行按钮置灰。
二、闪控窗
2.1 场景与定位
闪控窗是悬浮于桌面或其他应用界面上的小型窗口,展示区域较大,可承载更多关键信息,支持应用外便捷操作和实时监测。其设计目标是实现跨应用高频、轻量交互,典型场景包括:
- 金融盯盘
- 游戏直播
- 需要持续监控数据的业务
闪控窗由系统管理并统一绘制 UI(提供标题栏、拖拽区、内容区),但允许应用在内容区加载自定义页面,比闪控球具有更强的信息承载能力。
2.2 基础交互
- 创建:用户通过应用内指定入口开启闪控窗。点击闪控窗跳转至原应用对应界面。系统内最多显示一个闪控窗。
- 与闪控球切换:若应用同时接入闪控窗和闪控球,两者可在绑定后互相切换(通过缩小按钮或点击闪控球)。
- 最小化:支持拖拽收入侧边条,或通过“最小化”按钮收入侧边条。
- 删除:可拖拽至底部垃圾桶删除,或直接点击关闭按钮删除。
- 拖动:可以手动拖拽闪控窗拖动热区改变位置,拖拽时自动避让状态栏、导航条、输入法键盘等系统组件。
2.3 视觉规格
2.3.1 窗口显示状态
| 状态 | 窗口样式 | 应用场景 |
|---|---|---|
| 常规态 | 矩形,在最大最小尺寸范围内自由设定宽高 | 大多数场景 |
| Mini态 | 横向细长条矩形(高度固定为 32vp,宽度同常规态) | 游戏直播、盯盘等对遮挡要求高的场景 |
2.3.2 窗口模板
提供常规态与 mini 态两种标准模板,均包含 titlebar 布局、拖拽区、内容区。如果业务同时接入两种形态,需在窗口内容区自行配置切换按钮。
2.3.3 尺寸限制(不同设备)
闪控窗尺寸有最大/最小限制,应用可按需在区间内定义。建议通过 getFloatViewLimits() 获取推荐范围,并通过 onLimitsChange() 监听变化。
各设备类型的尺寸限制如下(表格中“窗口宽度”指闪控窗当前宽度):
| 设备类型 | 最小宽度 | 最小高度 | 最大宽度 | 最大高度 |
|---|---|---|---|---|
| 直板机(竖屏) | 屏幕宽度30% | 窗口宽度×75% | 屏幕宽度×90% | 窗口宽度×75% |
| 直板机(横屏) | 直板机竖屏屏幕宽度30% | 窗口宽度×75% | 屏幕宽度×45% | 窗口宽度×75% |
| 双折叠展开态 | 折叠态竖屏屏幕宽度30% | 窗口宽度×75% | 屏幕宽度×55% | 窗口宽度×75% |
| 三折叠展开态 | 折叠态竖屏屏幕宽度30% | 窗口宽度×75% | 屏幕宽度×50% | 窗口宽度×75% |
| 平板/电脑 | 屏幕宽度×10% | 窗口宽度×75% | 屏幕宽度×50% | 窗口宽度×75% |
Mini 态高度固定为 32vp,宽度同常规态规格。
2.4 与系统悬浮窗的区别
| 对比项 | 闪控窗 | 系统悬浮窗 |
|---|---|---|
| 功能承载 | 单一功能或局部信息,无法覆盖全应用业务 | 应用全量功能,可内部切换和跳转 |
| 悬浮方式 | 支持应用内或跨应用悬浮 | 跨应用悬浮 |
| 窗口限制 | 仅限执行本任务相关功能 | 无此限制 |
| UI 绘制 | 系统统一绘制标题栏等,内容区可自定义 | 开发者完全自绘 |
| 动效 | 高端精致,系统统一 | 开发者自控 |
| 设备支持 | Phone、Tablet、2in1 | 仅 2in1 |
2.5 完整开发步骤与示例代码
2.5.1 权限与约束
- 必须申请 ohos.permission.FLOAT_VIEW(user_grant 权限),需动态授权。
- 仅允许应用在前台时启动闪控窗。
- 同一个应用只能启动一个闪控窗,启动多个会返回错误码 1300033。
- 同一应用已启动闪控球或画中画窗口时,无法启动闪控窗,需先停止后者。
- 仅支持 Stage 模型,从 API 26.0.0 开始支持。
- 模板类型:目前支持
FloatViewTemplateType.ROUNDED_RECTANGLE(圆角矩形)和FloatViewTemplateType.RHORIZONTAL_BAR(水平条状矩形)。
2.5.2 基础场景:单独操作闪控窗(Index_plain.ets)
以下代码展示闪控窗的独立创建、启动、尺寸设置与生命周期管理。
// 应用初始化创建,申请用户授权
aboutToAppear(): void {
let flag = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION;
let bundleInfo = bundleManager.getBundleInfoForSelfSync(flag);
let atManager = abilityAccessCtrl.createAtManager();
atManager.verifyAccessToken(bundleInfo.appInfo.accessTokenId, 'ohos.permission.FLOAT_VIEW').then(data => {
console.info('Permission check: ' + JSON.stringify(data));
this.result = data === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED ? '权限已授权' : '权限未授权';
});
let enable: boolean = floatView.isFloatViewEnabled();
console.info(TAG + 'floatView enabled is: ' + enable);
}
// 确认用户授权状态
requestPermission(): void {
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionsFromUser(getContext(this), ['ohos.permission.FLOAT_VIEW'] as Permissions[])
.then((data) => {
console.info(TAG + `grant result: ${data.authResults}.`);
this.result = data.authResults[0] === 0 ? '权限已授权' : '权限被拒绝';
})
.catch((reason: BusinessError) => {
console.error(TAG + `requestPermissionsFromUser failed, ${reason?.code}, ${reason?.message}.`);
});
}
导入模块、声明闪控窗控制器并声明页面视图组件:
// 声明闪控窗控制器
private floatViewController: floatView.FloatViewController | undefined = undefined;
@State result: string = '';
// 创建闪控窗
async createWindow(): Promise<void> {
try {
await this.createWindowPlain();
} catch (err) {
console.error(TAG +
`Failed to create float view controller. Cause: ${(err as BusinessError).code}, message: ${(err as
BusinessError).message}`);
}
}
使用 create() 接口创建闪控窗控制器实例后注册状态变化事件回调,使用 setUIContext() 设置闪控窗页面内容,通过 getFloatViewLimits() 获取窗口尺寸限制,通过 setWindowSize() 设置合适的窗口大小,通过 start() 接口启动闪控窗:
// 启动闪控窗
async createWindowPlain(): Promise<void> {
if (!this.floatViewController) {
let ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
let floatConfig: floatView.FloatViewConfiguration = {
context: ctx,
templateType: floatView.FloatViewTemplateType.ROUNDED_RECTANGLE
};
// 创建闪控窗控制器实例
this.floatViewController = await floatView.create(floatConfig);
// 注册状态变化事件回调
this.registerStateChangeCallback();
// 注册尺寸变化事件回调
this.registerLimitsChangeCallback();
}
// 设置闪控窗页面内容
// FloatViewPage为闪控窗页面,由应用根据实际业务实现
await this.floatViewController.setUIContext('pages/FloatViewPage');
// 获取闪控窗尺寸限制,设置闪控窗大小
let limits: floatView.FloatViewLimits =
floatView.getFloatViewLimits(floatView.FloatViewTemplateType.ROUNDED_RECTANGLE);
let size: window.Size = {
width: limits.maxSize.width,
height: limits.maxSize.height
};
await this.floatViewController.setWindowSize(size);
// 启动闪控窗
await this.floatViewController.start();
console.info(TAG + 'Float view started in unbind state');
}
(可选)通过 onLimitsChange() 监听闪控窗尺寸限制变化,动态调整窗口尺寸,通过 onStateChange() 监听闪控窗状态变化,绑定状态变化回调:
// 注册闪控窗尺寸限制变化回调函数
public registerLimitsChangeCallback(): void {
this.floatViewController?.onLimitsChange((limits: floatView.FloatViewLimits) => {
console.info(TAG + `Limits changed: minSize=${limits.minSize}, maxSize=${limits.maxSize}`);
});
}
// 注册闪控窗状态变化回调函数
public registerStateChangeCallback(): void {
this.floatViewController?.onStateChange((info: floatView.FloatViewStateChangeInfo) => {
console.info(TAG + `State changed: ${info.state}, reason: ${info.stopReason}`);
if (info.state === floatView.FloatViewState.STOPPED) {
this.floatViewController?.offStateChange();
this.floatViewController?.offLimitsChange();
this.floatViewController = undefined;
}
});
}
在 onStateChange() 监听 start() 完成后,通过 stop() 停止闪控窗:
// 删除闪控窗
deleteAll(): void {
console.info(TAG + 'Deleting all');
if (this.floatViewController) {
this.floatViewController.stop().then(() => {
console.info(TAG + 'Float view stopped');
this.floatViewController = undefined;
}).catch((err: BusinessError) => {
console.error(TAG + `Failed to delete float view: ${err.code}`);
this.floatViewController = undefined;
});
}
}
三、闪控窗与闪控球绑定使用(复杂场景)
闪控窗可与闪控球绑定使用,实现以下能力:
- 绑定成功后,调用任一控制器的启动接口会同时创建闪控窗窗口和闪控球窗口,同一时刻仅展示其中一个窗口。
- 绑定成功后,闪控窗窗口与闪控球窗口支持用户点击触发的互相切换,点击闪控球不会触发 click 回调。
- 绑定成功后,调用任一控制器的停止接口会同时销毁闪控窗窗口和闪控球窗口。
- 绑定需要同时具有
ohos.permission.USE_FLOAT_BALL和ohos.permission.FLOAT_VIEW权限。
3.1 完整绑定示例(Index_advanced.ets)
3.1.1 权限申请与初始化
// 应用初始化创建,申请用户授权
aboutToAppear(): void {
let flag = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION;
let bundleInfo = bundleManager.getBundleInfoForSelfSync(flag);
let atManager = abilityAccessCtrl.createAtManager();
atManager.verifyAccessToken(bundleInfo.appInfo.accessTokenId, 'ohos.permission.FLOAT_VIEW').then(data => {
console.info('Permission check: ' + JSON.stringify(data));
this.result = data === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED ? '权限已授权' : '权限未授权';
});
let enable: boolean = floatView.isFloatViewEnabled();
console.info(TAG + 'floatView enabled is: ' + enable);
}
// 查询用户授权状态
requestPermission(): void {
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionsFromUser(getContext(this), ['ohos.permission.FLOAT_VIEW'] as Permissions[])
.then((data) => {
console.info(TAG + `grant result: ${data.authResults}.`);
this.result = data.authResults[0] === 0 ? '权限已授权' : '权限被拒绝';
})
.catch((reason: BusinessError) => {
console.error(TAG + `requestPermissionsFromUser failed, ${reason?.code}, ${reason?.message}.`);
});
}
3.1.2 导入模块并声明控制器
// 声明闪控窗控制器
private floatViewController: floatView.FloatViewController | undefined = undefined;
// 声明闪控球控制器
private floatingBallController: floatingBall.FloatingBallController | undefined = undefined;
@State result: string = '';
@State bindState: BindState = BindState.BIND;
@State statusText: string = '当前状态: 绑定';
@State currentDisplay: string = 'none';
// 创建闪控窗
async createWindow(): Promise<void> {
try {
// 绑定状态下:如果闪控球句柄已存在,先绑定闪控球和闪控窗
if (this.bindState === BindState.BIND) {
await this.createWindowAdvanced();
} else {
await this.createWindowPlain();
}
} catch (err) {
console.error(TAG +
`Failed to create float view controller. Cause: ${(err as BusinessError).code}, message: ${(err as
BusinessError).message}`);
}
}
3.1.3 创建控制器并绑定启动
使用 floatView.create() 接口创建闪控窗控制器实例,使用 floatingBall.create() 接口创建闪控球控制器实例:
async createWindowAdvanced(): Promise<void> {
if (!this.floatingBallController) {
let ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
let ballConfig: floatingBall.FloatingBallConfiguration = {
context: ctx
};
// 创建闪控球控制器实例
this.floatingBallController = await floatingBall.create(ballConfig);
}
if (!this.floatViewController) {
let ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
let floatConfig: floatView.FloatViewConfiguration = {
context: ctx,
templateType: floatView.FloatViewTemplateType.ROUNDED_RECTANGLE
};
// 创建闪控窗实例
this.floatViewController = await floatView.create(floatConfig);
}
this.registerStateChangeCallback();
this.registerLimitsChangeCallback();
await this.bindControllersAndStart();
}
// Bind状态创建复杂场景悬浮窗,否则创建plain悬浮窗
async bindFloatViewAndBall(): Promise<void> {
this.bindState = BindState.BIND;
this.statusText = '当前状态: 绑定';
this.currentDisplay = 'none';
console.info(TAG + 'State updated to BIND');
}
使用 floatView.bind() 接口将闪控窗控制器与闪控球控制器绑定,绑定时需要配置闪控球参数。使用 setUIContext() 设置闪控窗页面内容,通过 getFloatViewLimits() 获取窗口尺寸限制,通过 setWindowSize() 设置合适的窗口大小,通过 start() 接口启动闪控窗(绑定状态下会同时启动闪控球):
private async bindControllersAndStart(): Promise<void> {
if (!this.floatViewController || !this.floatingBallController) {
console.warn(TAG + 'Controllers not ready for binding');
return;
}
const fvController = this.floatViewController;
const fbController = this.floatingBallController;
const ballParams: floatingBall.FloatingBallParams = {
template: floatingBall.FloatingBallTemplate.EMPHATIC,
title: "标题",
content: "正文"
};
try {
// 将闪控窗控制器与闪控球控制器绑定
await floatView.bind(fvController, fbController, ballParams);
// 设置闪控窗页面内容
// FloatViewPage为闪控窗页面,由应用根据实际业务实现
await fvController.setUIContext('pages/FloatViewPage');
// 获取窗口尺寸大小约束并设置窗口大小
let limits: floatView.FloatViewLimits =
floatView.getFloatViewLimits(floatView.FloatViewTemplateType.ROUNDED_RECTANGLE);
let size: window.Size = {
width: limits.maxSize.width,
height: limits.maxSize.height
};
await fvController.setWindowSize(size);
// 启动闪控窗(绑定状态下会同时启动闪控球)
await fvController.start();
} catch (err) {
console.error(TAG +
`Bind and start failed. Cause: ${(err as BusinessError).code}, message: ${(err as BusinessError).message}`);
}
}
3.1.4 停止与解绑
通过 stop() 停止闪控窗,绑定状态下会同时停止闪控球:
deleteAll(): void {
console.info(TAG + 'Deleting all (ball and window)');
// 删除闪控窗
if (this.floatViewController) {
this.floatViewController.stop().then(() => {
console.info(TAG + 'Float view stopped');
this.floatViewController = undefined;
}).catch((err: BusinessError) => {
console.error(TAG + `Failed to delete float view: ${err.code}`);
this.floatViewController = undefined;
});
}
// 删除闪控球
if (this.floatingBallController) {
this.floatingBallController.stopFloatingBall().then(() => {
console.info(TAG + 'Floating ball stopped');
this.floatingBallController = undefined;
}).catch((err: BusinessError) => {
console.error(TAG + `Failed to delete floating ball: ${err.code}`);
this.floatingBallController = undefined;
});
}
}
(可选)通过 floatView.unbind() 解绑闪控窗与闪控球:
unbindFloatViewAndBall(): void {
this.bindState = BindState.UNBIND;
this.statusText = '当前状态: 解绑';
console.info(TAG + 'State updated to UNBIND');
if (!this.floatViewController || !this.floatingBallController) {
console.warn(TAG + 'Controllers not initialized, unbinding not executed');
return;
}
floatView.unbind(this.floatViewController, this.floatingBallController).then(() => {
console.info(TAG + 'Succeeded in unbinding');
}).catch((err: BusinessError) => {
console.error(TAG + `Unbind failed. Code: ${err.code}, Message: ${err.message}`);
});
}
四、闪控窗与防窥保护组合使用(进阶场景)
闪控窗支持与防窥保护功能联动使用。当用户通过闪控窗查看敏感信息时,系统将自动通过传感器检测周围环境,一旦识别到非机主的窥视行为,会立即在闪控窗上拉起蒙层以遮盖内容,防止隐私泄露。用户也可根据情况手动解除此保护。
防窥保护基于 SystemCapability.Security.DlpAntiPeep 能力实现。识别逻辑为:系统将长期通过人脸解锁的用户标记为机主。当传感器检测到非机主与机主同时注视屏幕时,会通过回调向应用发送被窥视状态通知(HIDE),应用据此触发隐私保护动作。
4.1 权限与前提条件
本场景需要申请以下权限:
ohos.permission.FLOAT_VIEWohos.permission.DLP_GET_HIDE_STATUS
还需要在系统设置 > 隐私与安全 > 防窥保护中打开对应应用开关。可通过 canIUse('SystemCapability.Security.DlpAntiPeep') 判断当前设备是否支持防窥保护。
4.2 防窥保护工具类封装
封装防窥保护工具类,核心能力为:检测设备是否支持防窥功能、查询系统开关状态、获取当前窥视状态,并在必要时调用系统蒙层接口进行隐私遮挡。
// 判断是否支持防窥保护
export function canUseAntiPeep(): boolean {
return canIUse('SystemCapability.Security.DlpAntiPeep');
}
// 判断防窥保护是否打开
export async function isAntiPeepOn(): Promise<boolean> {
try {
let result: boolean = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
console.info(TAG + `isAntiPeepOn isDlpAntiPeepSwitchOn success. ${result}`)
return result;
} catch (err) {
console.error(TAG + `[isAntiPeepOn] isDlpAntiPeepSwitchOn failed.${JSON.stringify(err)}`);
return false;
}
}
// 获取防窥保护状态
export function getAntiPeepInfo(): dlpAntiPeep.DlpAntiPeepStatus {
try {
let dlpAntiPeepStatus = dlpAntiPeep.getDlpAntiPeepInfo();
console.info(TAG + `getDlpHideInfo success. ${JSON.stringify(dlpAntiPeepStatus)}`);
return dlpAntiPeepStatus;
} catch (err) {
console.info(TAG + `getDlpHideInfo failed. ${JSON.stringify(err)}`);
return -1;
}
}
// 开启全局保护
export function showSystemMaskLayer(windowId: number): Promise<boolean> {
return new Promise((resolve) => {
try {
if (canUseAntiPeep()) {
dlpAntiPeep.setAntiPeepMaskLayer(windowId).catch((err: BusinessError) => {
console.error(
TAG + `Execute setAntiPeepMaskLayer failed. error code:${err.code}, error message:${err.message}`);
resolve(false);
}).then(() => {
console.info(TAG + `setAntiPeepMaskLayer success`);
resolve(true);
})
} else {
resolve(false);
}
} catch (err) {
console.error(TAG + `Call setAntiPeepMaskLayer failed. ${JSON.stringify(err)}`);
resolve(false);
}
});
}
注册防窥保护状态监听,调用 dlpAntiPeep.on('dlpAntiPeep') 接口注册防窥保护状态监听。其中 PASS 表示当前设备屏幕无人窥视,HIDE 表示有除机主以外的人在窥视设备屏幕:
export interface AntiPeepCallback {
onStatusChanged: (status: dlpAntiPeep.DlpAntiPeepStatus) => Promise<void>;
}
export function listenOnAntiPeepStatus(antiPeepCB: AntiPeepCallback): boolean {
try {
console.info(TAG + `start on('dlpAntiPeep')`);
dlpAntiPeep.on('dlpAntiPeep', (dlpAntiPeepStatus: dlpAntiPeep.DlpAntiPeepStatus) => {
if (antiPeepCB) {
antiPeepCB.onStatusChanged(dlpAntiPeepStatus);
} else {
console.warn(TAG + `antiPeepCB is empty`);
}
});
console.info(TAG + `on('dlpAntiPeep') ok`);
return true;
} catch (err) {
console.error(TAG + `dlpAntiPeep.on failed. ${JSON.stringify(err)}`);
return false;
}
}
4.3 主页面集成防窥保护
初始化防窥状态并注册回调,申请闪控窗权限。初始化时依次检查设备支持情况、开关状态与当前状态。声明及初始化防窥回调处理函数,回调处理逻辑为:当状态为 HIDE 时,获取应用主窗并调用 setAntiPeepMaskLayer() 拉起系统蒙层。
isListenOn: boolean = false;
isPeep: boolean = false;
isGranted: boolean = false;
private floatViewController: floatView.FloatViewController | undefined = undefined;
antiPeepCB: AntiPeepCallback = {
onStatusChanged: async (status: dlpAntiPeep.DlpAntiPeepStatus): Promise<void> => {
await this.handleAntiPeepStatus(status);
}
};
// 声明防窥回调处理函数
private async handleAntiPeepStatus(status: dlpAntiPeep.DlpAntiPeepStatus) {
console.info(TAG + `[handleAntiPeepStatus] ${status}`);
switch (status) {
case dlpAntiPeep.DlpAntiPeepStatus.PASS:
console.info(TAG + 'DlpAntiPeepStatus is PASS');
break;
case dlpAntiPeep.DlpAntiPeepStatus.HIDE:
// 从存储中获取已保存的窗口信息
let window: window.Window = AppStorage.get('MAIN_WINDOW') as window.Window;
const windowId: number = window.getUIContext().getWindowId() as number;
console.info(TAG + `DlpAntiPeepStatus is HIDE ${windowId}`);
dlpAntiPeep.setAntiPeepMaskLayer(windowId)
.then((result) => {
console.info(TAG, `setAntiPeepMaskLayer success + ${result}`);
})
.catch((err: BusinessError) => {
console.error(TAG,
`Execute setAntiPeepMaskLayer failed. error code:${err.code}, error message:${err.message}`);
})
break;
default:
break;
}
}
// 初始化防窥状态及注册回调
private initAntiPeepStatus() {
if (canUseAntiPeep()) {
isAntiPeepOn().then((opened) => {
if (opened) {
let info = getAntiPeepInfo();
this.handleAntiPeepStatus(info);
this.isListenOn = listenOnAntiPeepStatus(this.antiPeepCB);
if (this.isListenOn) {
console.info(TAG + 'succeed in listenOnAntiPeepStatus ')
}
} else {
try {
this.getUIContext().getPromptAction().showToast({
message: $r('app.string.anti_peep_not_enable')
});
} catch (error) {
console.error(TAG + 'show toast error. code =' + error.code + ', message =' + error.message);
}
}
})
} else {
try {
this.getUIContext().getPromptAction().showToast({
message: $r('app.string.device_not_supported')
});
} catch (error) {
console.error(TAG + 'show toast error. code =' + error.code + ', message =' + error.message);
}
}
}
// 确认用户授权状态
private requestPermission(): void {
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
let hostContext = this.getUIContext().getHostContext();
atManager.requestPermissionsFromUser(hostContext, ['ohos.permission.FLOAT_VIEW'] as Permissions[])
.then((data) => {
console.info(TAG + `grant result: ${data.authResults}.`);
this.isGranted = data.authResults[0] === 0;
})
.catch((reason: BusinessError) => {
console.error(TAG + `requestPermissionsFromUser failed, ${reason?.code}, ${reason?.message}.`);
});
}
// 应用初始化创建,申请用户授权
aboutToAppear(): void {
this.initAntiPeepStatus();
this.requestPermission();
let enable: boolean = floatView.isFloatViewEnabled();
console.info(TAG + 'floatView enabled is: ' + enable);
}
创建并启动闪控窗,使用 floatView.create() 创建控制器,通过 setUIContext()、getFloatViewLimits()、setWindowSize()、start() 完成页面内容设置与启动,并通过 onStateChange()、onRectChange()、onLimitsChange() 注册状态与尺寸限制变化回调:
// 启动闪控窗
async createWindowAntiPeep(): Promise<void> {
if (!this.floatViewController) {
let ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
let floatConfig: floatView.FloatViewConfiguration = {
context: ctx,
templateType: floatView.FloatViewTemplateType.ROUNDED_RECTANGLE
};
// 创建闪控窗控制器实例
this.floatViewController = await floatView.create(floatConfig);
// 注册状态变化事件回调
this.registerStateChangeCallback();
// 注册尺寸变化事件回调
this.registerLimitsChangeCallback();
}
// 设置闪控窗页面内容
// FloatViewPage为闪控窗页面,由应用根据实际业务实现
await this.floatViewController.setUIContext('pages/FloatViewPage');
// 获取闪控窗尺寸限制,设置闪控窗大小
let limits: floatView.FloatViewLimits =
floatView.getFloatViewLimits(floatView.FloatViewTemplateType.ROUNDED_RECTANGLE);
let size: window.Size = {
width: limits.maxSize.width,
height: limits.maxSize.height
};
await this.floatViewController.setWindowSize(size);
// 启动闪控窗
await this.floatViewController.start();
console.info(TAG + 'Float view started in unbind state');
}
// 注册闪控窗尺寸限制变化回调函数
public registerLimitsChangeCallback(): void {
this.floatViewController?.onLimitsChange((limits: floatView.FloatViewLimits) => {
console.info(TAG + `Limits changed: minSize=${limits.minSize}, maxSize=${limits.maxSize}`);
});
}
// 注册闪控窗状态变化回调函数
public registerStateChangeCallback(): void {
this.floatViewController?.onStateChange((info: floatView.FloatViewStateChangeInfo) => {
console.info(TAG + `State changed: ${info.state}, reason: ${info.stopReason}`);
if (info.state === floatView.FloatViewState.STOPPED) {
this.floatViewController?.offStateChange();
this.floatViewController?.offLimitsChange();
this.floatViewController = undefined;
}
});
}
通过 stop() 停止闪控窗:
// 停止闪控窗
deleteAll(): void {
console.info(TAG + 'Deleting all');
if (this.floatViewController) {
this.floatViewController.stop().then(() => {
console.info(TAG + 'Float view stopped');
}).catch((err: BusinessError) => {
console.error(TAG + `Failed to delete float view: ${err.code} reason ${err.message}`);
});
}
}
五、共性约束与最佳实践
5.1 权限与启动条件
- 闪控球:需
ohos.permission.USE_FLOAT_BALL,仅限指定场景(抢单、比价、盯盘等),违规使用将受处罚。 - 闪控窗:需
ohos.permission.FLOAT_VIEW(user_grant 权限),需动态申请。 - 两者均仅允许应用在前台时启动。
5.2 数量与互斥
- 闪控球:每应用最多 1 个,系统最多 2 个。
- 闪控窗:系统最多 1 个,每应用最多 1 个。
- 同一应用已启动闪控球或画中画窗口时,无法启动闪控窗,需先停止前者。
5.3 生命周期管理
- 必须注册
stateChange回调,在STOPPED状态时主动取消所有监听(off('click')、off('stateChange')等)并释放控制器引用,避免内存泄漏。 - 闪控球的位置具有记忆功能(关闭后记录位置,下次启动自动恢复),但旋转屏幕或重启设备会恢复到默认位置(屏幕右上侧)。
5.4 尺寸适配建议
- 闪控球:宽度自动适配内容,高度固定,无需开发者额外计算。
- 闪控窗:务必先调用
getFloatViewLimits()获取当前设备的推荐尺寸范围,再通过setWindowSize()设置合理大小;同时监听onLimitsChange()以动态响应配置变化。若设置超出范围,系统会自动裁剪,实际尺寸通过onRectChange()回调获取。
5.5 设备兼容性
- 闪控球:支持 Phone、Tablet、PC/2in1(从 API 20 开始),模拟器支持需 DevEco Studio 6.0.1 Release 及以上。
- 闪控窗:支持 Phone、Tablet、2in1(从 API 26.0.0 开始)。
- 均要求 Stage 模型。
六、总结
闪控球与闪控窗构成 HarmonyOS 轻量化窗口体系的两级载体:
- 闪控球侧重“一键触达”,信息精炼、交互直接,适合高频快捷入口;
- 闪控窗侧重“轻量驻留”,信息丰富、可自定义内容区,适合持续监测和轻任务处理;
- 两者可绑定协同,实现从“小球”到“小窗”的平滑切换,满足复杂场景需求;
- 开发时需严格遵循权限、数量、生命周期及尺寸适配规则,结合防窥保护等系统能力可进一步提升安全性。
开发者应根据业务诉求选择合适的形态,并严格遵循官方约束,确保用户体验的一致性与系统稳定性。
更多推荐


所有评论(0)