HarmonyOS开发:系统级窗口——系统弹窗与悬浮窗
HarmonyOS开发:系统级窗口——系统弹窗与悬浮窗
📌 核心要点:系统级窗口是鸿蒙窗口体系中的"特权窗口",能穿透应用层显示弹窗和悬浮窗,搞懂窗口层级和创建方式是关键。
背景与动机
来电弹窗、低电量提醒、导航悬浮球——这些UI元素有个共同特点:不管你在哪个应用里,它们都能显示在最上层。
普通应用能做吗?不能。普通应用的窗口被限制在自己的层级里,最多只能在自己的页面上弹个对话框。想在其他应用上面弹窗?想都别想。
但系统应用可以。通过系统级窗口API,系统签名应用可以创建悬浮窗、系统弹窗、全局提示——这些窗口的z-order比普通应用窗口高,能穿透显示。
不过,系统级窗口的创建和管理有一套严格的规则。层级搞错了窗口被遮挡,类型选错了创建失败,权限不够直接报错。这篇就把系统级窗口的层级、创建、管理讲清楚。
核心原理
窗口层级体系
鸿蒙的窗口从底到顶分多个层级:
z-order 从低到高:
┌────────────────────────────────┐
│ 系统安全窗口 (ALERT_WINDOW) │ ← 最高层:系统警告
├────────────────────────────────┤
│ 系统浮窗 (FLOAT_WINDOW) │ ← 高层:来电弹窗、悬浮球
├────────────────────────────────┤
│ 系统状态栏 (STATUS_BAR) │
├────────────────────────────────┤
│ 应用浮窗 (APPLICATION_FLOAT) │ ← 应用级悬浮窗
├────────────────────────────────┤
│ 应用主窗口 (MAIN_WINDOW) │ ← 普通应用窗口
├────────────────────────────────┤
│ 系统导航栏 (NAVIGATION_BAR) │
└────────────────────────────────┘
系统级窗口的z-order比应用窗口高,所以能"穿透"显示。但不同类型的系统窗口之间也有层级差异,ALERT_WINDOW比FLOAT_WINDOW更高。
窗口类型与权限
| 窗口类型 | 枚举值 | 权限要求 | 典型场景 |
|---|---|---|---|
| 应用主窗口 | TYPE_MAIN |
无 | 应用主界面 |
| 应用子窗口 | TYPE_SUB |
无 | 对话框、弹窗 |
| 应用浮窗 | TYPE_FLOAT |
ohos.permission.SYSTEM_FLOAT_WINDOW |
应用内悬浮窗 |
| 系统浮窗 | TYPE_SYSTEM_FLOAT |
系统签名 | 来电弹窗、悬浮球 |
| 系统告警窗 | TYPE_ALERT |
系统签名+特权 | 系统警告、低电量 |
系统级窗口的创建流程
flowchart TD
A[创建系统级窗口] --> B{窗口类型}
B -->|系统浮窗| C[TYPE_SYSTEM_FLOAT]
B -->|系统告警窗| D[TYPE_ALERT]
C --> E[检查系统签名]
D --> E
E -->|有签名| F[创建窗口]
E -->|无签名| G[Permission Denied]
F --> H[设置窗口属性]
H --> H1[窗口尺寸]
H --> H2[窗口位置]
H --> H3[背景透明度]
H --> H4[触摸事件穿透]
H --> I[加载UI内容]
I --> J[显示窗口]
classDef type fill:#e3f2fd,stroke:#2196f3,color:#0d47a1
classDef check fill:#fff3e0,stroke:#ff9800,color:#e65100
classDef fail fill:#fce4ec,stroke:#f44336,color:#b71c1c
classDef success fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
classDef config fill:#f3e5f5,stroke:#9c27b0,color:#4a148c
class B,C,D type
class E check
class G fail
class F,J success
class H,H1,H2,H3,H4 config
代码实战
基础用法:创建系统浮窗
// common/SystemWindowManager.ets - 系统窗口管理工具
import window from '@ohos.window';
import { BusinessError } from '@ohos.base';
class SystemWindowManager {
private tag: string = 'SystemWindowManager';
private systemFloatWindow: window.Window | null = null;
// 创建系统浮窗
async createSystemFloatWindow(context: Context): Promise<window.Window | null> {
try {
// 创建系统浮窗
const config: window.Configuration = {
name: 'SystemFloatWindow',
windowType: window.WindowType.TYPE_SYSTEM_FLOAT,
ctx: context,
};
this.systemFloatWindow = await window.createWindow(config);
// 设置窗口位置和大小
await this.systemFloatWindow.moveTo(100, 200);
await this.systemFloatWindow.resize(300, 150);
// 设置窗口背景透明
await this.systemFloatWindow.setWindowBackgroundColor('#00000000');
// 设置窗口可触摸
this.systemFloatWindow.setTouchable(true);
console.info(this.tag, '系统浮窗创建成功');
return this.systemFloatWindow;
} catch (err) {
const error = err as BusinessError;
console.error(this.tag, `创建系统浮窗失败: ${error.message}`);
return null;
}
}
// 显示系统浮窗
async showFloatWindow(): Promise<boolean> {
if (!this.systemFloatWindow) {
console.error(this.tag, '浮窗未创建');
return false;
}
try {
await this.systemFloatWindow.showWindow();
console.info(this.tag, '系统浮窗显示成功');
return true;
} catch (err) {
console.error(this.tag, `显示浮窗失败: ${JSON.stringify(err)}`);
return false;
}
}
// 隐藏系统浮窗
async hideFloatWindow(): Promise<boolean> {
if (!this.systemFloatWindow) {
return false;
}
try {
await this.systemFloatWindow.hideWindow();
return true;
} catch (err) {
console.error(this.tag, `隐藏浮窗失败: ${JSON.stringify(err)}`);
return false;
}
}
// 销毁系统浮窗
async destroyFloatWindow(): Promise<void> {
if (!this.systemFloatWindow) {
return;
}
try {
await this.systemFloatWindow.destroyWindow();
this.systemFloatWindow = null;
console.info(this.tag, '系统浮窗已销毁');
} catch (err) {
console.error(this.tag, `销毁浮窗失败: ${JSON.stringify(err)}`);
}
}
// 移动浮窗位置
async moveFloatWindow(x: number, y: number): Promise<void> {
if (!this.systemFloatWindow) {
return;
}
try {
await this.systemFloatWindow.moveTo(x, y);
} catch (err) {
console.error(this.tag, `移动浮窗失败: ${JSON.stringify(err)}`);
}
}
}
export default new SystemWindowManager();
进阶用法:系统弹窗实现
系统弹窗比浮窗层级更高,用于紧急通知和确认操作:
// common/SystemAlertDialog.ets - 系统告警弹窗
import window from '@ohos.window';
import { BusinessError } from '@ohos.base';
type DialogCallback = () => void;
interface AlertDialogConfig {
title: string;
message: string;
positiveText?: string;
negativeText?: string;
onPositive?: DialogCallback;
onNegative?: DialogCallback;
autoDismiss?: boolean;
timeout?: number; // 自动消失时间(毫秒)
}
class SystemAlertDialog {
private tag: string = 'SystemAlertDialog';
private alertWindow: window.Window | null = null;
private autoDismissTimer: number = -1;
// 显示系统告警弹窗
async show(context: Context, config: AlertDialogConfig): Promise<boolean> {
try {
// 先销毁已有的弹窗
await this.dismiss();
// 创建告警窗口
const windowConfig: window.Configuration = {
name: 'SystemAlertDialog',
windowType: window.WindowType.TYPE_ALERT,
ctx: context,
};
this.alertWindow = await window.createWindow(windowConfig);
// 全屏显示
const display = this.alertWindow.getProperties();
await this.alertWindow.resize(display.windowRect.width, display.windowRect.height);
await this.alertWindow.moveTo(0, 0);
// 半透明背景
await this.alertWindow.setWindowBackgroundColor('#80000000');
this.alertWindow.setTouchable(true);
// 设置UI内容(实际项目中通过setUIContent加载页面)
// this.alertWindow.setUIContent('pages/SystemDialogPage', () => {});
await this.alertWindow.showWindow();
// 自动消失
if (config.autoDismiss && config.timeout) {
this.autoDismissTimer = setTimeout(() => {
this.dismiss();
}, config.timeout) as unknown as number;
}
console.info(this.tag, `系统弹窗显示: ${config.title}`);
return true;
} catch (err) {
const error = err as BusinessError;
console.error(this.tag, `显示系统弹窗失败: ${error.message}`);
return false;
}
}
// 关闭弹窗
async dismiss(): Promise<void> {
if (this.autoDismissTimer !== -1) {
clearTimeout(this.autoDismissTimer);
this.autoDismissTimer = -1;
}
if (this.alertWindow) {
try {
await this.alertWindow.destroyWindow();
this.alertWindow = null;
console.info(this.tag, '系统弹窗已关闭');
} catch (err) {
console.error(this.tag, `关闭弹窗失败: ${JSON.stringify(err)}`);
}
}
}
// 更新弹窗内容
async updateContent(title: string, message: string): Promise<void> {
if (!this.alertWindow) {
return;
}
// 实际项目中通过状态变量更新UI
console.info(this.tag, `更新弹窗内容: ${title}`);
}
}
export default new SystemAlertDialog();
完整示例:可拖拽悬浮球
一个可拖拽的系统悬浮球,点击展开功能面板:
// pages/FloatingBallPage.ets - 悬浮球页面(加载到系统浮窗中)
import window from '@ohos.window';
@Entry
@Component
struct FloatingBallPage {
@State isExpanded: boolean = false;
@State ballX: number = 300;
@State ballY: number = 800;
@State offsetX: number = 0;
@State offsetY: number = 0;
// 悬浮球菜单项
private menuItems = [
{ icon: '🏠', label: '主页', action: 'home' },
{ icon: '🔒', label: '锁屏', action: 'lock' },
{ icon: '📸', label: '截图', action: 'screenshot' },
{ icon: '🔦', label: '手电筒', action: 'flashlight' },
];
build() {
Stack() {
if (this.isExpanded) {
// 展开菜单
Column() {
Text('快捷操作')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 16 })
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
ForEach(this.menuItems, (item: Record<string, string>) => {
Column() {
Text(item.icon)
.fontSize(28)
Text(item.label)
.fontSize(11)
.fontColor('#666666')
.margin({ top: 4 })
}
.width(70)
.height(70)
.justifyContent(FlexAlign.Center)
.backgroundColor('#ffffff')
.borderRadius(12)
.margin(8)
.shadow({ radius: 4, color: '#1a000000', offsetY: 2 })
.onClick(() => {
this.handleMenuAction(item.action);
})
}, (item: Record<string, string>) => item.action)
}
// 关闭按钮
Text('收起')
.fontSize(13)
.fontColor('#999999')
.margin({ top: 16 })
.onClick(() => {
this.isExpanded = false;
})
}
.width(200)
.padding(16)
.backgroundColor('#f5f5f5')
.borderRadius(16)
.shadow({ radius: 8, color: '#33000000', offsetY: 4 })
} else {
// 悬浮球
Stack() {
Circle({ width: 48, height: 48 })
.fill('#1976d2')
.shadow({ radius: 4, color: '#331976d2', offsetY: 2 })
Text('⚡')
.fontSize(24)
.fontColor('#ffffff')
}
.width(56)
.height(56)
.gesture(
GestureGroup(GestureMode.Exclusive,
// 拖拽手势
PanGesture()
.onActionStart((event) => {
this.offsetX = 0;
this.offsetY = 0;
})
.onActionUpdate((event) => {
this.ballX += event.offsetX - this.offsetX;
this.ballY += event.offsetY - this.offsetY;
this.offsetX = event.offsetX;
this.offsetY = event.offsetY;
})
.onActionEnd(() => {
// 吸附到屏幕边缘
this.snapToEdge();
}),
// 点击手势
TapGesture()
.onAction(() => {
this.isExpanded = true;
})
)
)
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignContent(Alignment.Center)
}
// 吸附到屏幕边缘
private snapToEdge() {
const screenWidth = 360; // 实际使用时动态获取
if (this.ballX < screenWidth / 2) {
this.ballX = 20;
} else {
this.ballX = screenWidth - 76;
}
}
// 处理菜单操作
private handleMenuAction(action: string) {
console.info('FloatingBall', `执行操作: ${action}`);
this.isExpanded = false;
switch (action) {
case 'home':
// 返回主页
break;
case 'lock':
// 锁屏
break;
case 'screenshot':
// 截图
break;
case 'flashlight':
// 手电筒
break;
}
}
}
踩坑与注意事项
坑一:没有系统签名创建系统浮窗失败
TYPE_SYSTEM_FLOAT和TYPE_ALERT窗口类型需要系统签名。普通应用调用createWindow时传入这些类型,直接抛异常。
如果你只是想在应用内显示悬浮窗,用TYPE_FLOAT类型,配合ohos.permission.SYSTEM_FLOAT_WINDOW权限就行。不需要系统签名。
坑二:系统浮窗遮挡键盘
系统浮窗的z-order很高,连输入法键盘都能挡住。用户在别的应用里打字,你的浮窗把键盘盖住了,这体验就废了。
解决办法:监听键盘弹出事件,键盘弹出时把浮窗往上移:
// 监听键盘避让区域变化
mainWindow.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_KEYBOARD) {
const keyboardHeight = data.area.bottomRect.height;
if (keyboardHeight > 0) {
// 键盘弹出,浮窗上移
this.moveFloatWindow(this.ballX, this.ballY - keyboardHeight);
} else {
// 键盘收起,浮窗回位
this.moveFloatWindow(this.ballX, this.originalBallY);
}
}
});
坑三:悬浮窗触摸事件穿透问题
你创建了悬浮窗,但触摸事件穿透到了下面的应用,悬浮窗自己反而收不到触摸。
原因:窗口的touchable属性默认是false。需要手动设置为true:
// 创建窗口后设置可触摸
this.systemFloatWindow.setTouchable(true);
反过来,如果你想让悬浮窗不拦截触摸事件(比如只是一个显示型的浮窗),保持false就行。
坑四:系统弹窗无法覆盖状态栏
你创建了TYPE_ALERT类型的窗口,但发现状态栏区域还是显示在最上层,弹窗被状态栏挡住了。
原因:TYPE_ALERT窗口默认不覆盖系统栏区域。需要额外设置全屏布局:
// 让弹窗覆盖状态栏区域
await this.alertWindow.setWindowLayoutFullScreen(true);
坑五:多个系统窗口的层级冲突
你同时创建了系统浮窗和系统弹窗,但发现弹窗被浮窗挡住了。
原因:同类型窗口的z-order由创建顺序决定,后创建的在上层。不同类型窗口的z-order由类型决定。
解决办法:如果需要精确控制层级,使用setWindowZLevel接口(需要HarmonyOS 6+):
// 设置窗口层级(数值越大越在上层)
await this.alertWindow.setWindowZLevel(100);
await this.floatWindow.setWindowZLevel(50);
HarmonyOS 6适配说明
HarmonyOS 6在系统级窗口方面有几个重要变化:
-
窗口层级精确控制:新增
setWindowZLevel接口,可以精确设置窗口的z-order值,不再依赖创建顺序。 -
系统弹窗模板:新增
SystemDialog接口,提供预置的弹窗模板(确认框、输入框、选择框),不需要自己写UI。
// HarmonyOS 6 系统弹窗模板
import systemDialog from '@ohos.systemDialog';
const dialog = new systemDialog.ConfirmDialog();
dialog.title = '系统警告';
dialog.message = '存储空间不足,请清理';
dialog.positiveButton = { text: '立即清理', action: () => {} };
dialog.negativeButton = { text: '稍后', action: () => {} };
await dialog.show();
-
悬浮窗安全区域适配:系统浮窗新增
setFloatingWindowSafeArea接口,自动避开状态栏和导航栏区域。 -
窗口动画增强:系统级窗口支持自定义入场/退场动画,可以设置弹簧动画、渐变动画等效果。
适配建议:新项目优先使用SystemDialog模板创建系统弹窗,减少自定义UI的工作量。使用setWindowZLevel精确控制多窗口层级。
总结
系统级窗口是系统应用的"特权武器",能穿透应用层显示弹窗和悬浮窗。但窗口层级、类型选择、触摸事件、权限要求——每个环节都有坑。
核心要点回顾:
- 系统浮窗用
TYPE_SYSTEM_FLOAT,系统弹窗用TYPE_ALERT,都需要系统签名 - 窗口层级从低到高:应用主窗口 → 应用浮窗 → 系统浮窗 → 系统告警窗
- 悬浮窗要设置
touchable(true),否则收不到触摸事件 - 注意键盘遮挡问题,监听避让区域变化调整位置
- 多窗口场景用
setWindowZLevel控制层级
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 窗口层级和触摸事件处理比较复杂 |
| 使用频率 | ⭐⭐⭐ 来电弹窗、悬浮球、系统通知等场景必用 |
| 重要程度 | ⭐⭐⭐⭐ 系统级交互的核心能力 |
- 点赞
- 收藏
- 关注作者
评论(0)