鸿蒙开发:基于子窗口实现应用内悬浮窗(含完整代码示例)

在现代移动应用中,悬浮窗/悬浮球是一种非常实用的交互方式,常用于展示快捷入口、实时通知、视频播放等场景。例如:

  • 聊天应用中的小助手按钮
  • 视频应用的画中画功能
  • 游戏或工具类 App 的全局操作面板

HarmonyOS 提供了 子窗口(SubWindow)机制,结合 Window 模块和手势控制能力,开发者可以轻松构建一个支持拖拽、动画靠边、跨页面保留位置、响应点击事件的悬浮窗组件。


🎯 功能需求概述

我们希望实现以下核心功能:

编号

功能描述

✅ 场景一

支持动态添加/移除悬浮窗,样式可定制(圆形 & 小视频窗口)

✅ 场景二

子窗口创建后,主窗口仍能正常响应系统返回手势(如侧滑返回)

✅ 场景三

悬浮窗支持拖拽并自动靠边显示;跳转页面后仍保持位置不变

✅ 场景四

悬浮窗内部点击触发主窗口 Router / Navigation 页面跳转

✅ 场景五

窗口大小自适应内容组件变化

✅ 场景六

支持隐藏和销毁悬浮窗

✅ 场景七

视频类应用支持画中画后台播放与桌面返回自动恢复


🧱 技术选型说明

我们使用 HarmonyOS 提供的如下关键模块完成悬浮窗功能:

  • @ohos.window:窗口管理模块,用于创建子窗口、设置布局、监听焦点等
  • Router / Navigation:用于实现主窗口页面跳转逻辑
  • GestureEvent / PanGesture:用于实现拖拽移动
  • AppStorage:存储公共变量如 windowStage、导航栈信息
  • componentUtils:获取组件尺寸用于窗口自适应调整
  • pipWindow:画中画功能专用接口

🛠️ 核心实现步骤

1️⃣ 获取 WindowStage 并保存到全局(EntryAbility)
// EntryAbility.ts
import router from '@ohos.router';export default class EntryAbility extends UIAbility {onWindowStageCreate(windowStage: window.WindowStage): void {// 设置主窗口页面windowStage.loadContent('pages/Index', (err, data) => {if (err.code) {console.error('Failed to load content:', JSON.stringify(err));}});// 保存 windowStage 到全局globalThis.windowStage = windowStage;}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

💡 注意:通过 globalThisAppStorage 保存 windowStage,以便后续在页面中使用。


2️⃣ 创建子窗口并设置基础样式
// 创建子窗口
function createSubWindow() {const windowStage = globalThis.windowStage;windowStage.createSubWindow("mySubWindow", (err, subWindow) => {if (err.code !== 0) {console.error('创建子窗口失败', err.message);return;}try {// 加载子窗口页面subWindow.setUIContent("pages/SubWindowPage");// 设置背景透明(无白边)subWindow.setWindowBackgroundColor("#00000000");// 设置初始位置和大小subWindow.moveWindowTo(0, 200); // x=0, y=200subWindow.resize(vp2px(75), vp2px(75)); // 宽高 75vp// 全屏布局不避让安全区subWindow.setWindowLayoutFullScreen(true);// 显示子窗口subWindow.showWindow();} catch (e) {console.error('初始化子窗口出错', e);}});
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

3️⃣ 实现拖拽并自动靠边(带动画效果)
@State position: { x: number; y: number } = { x: 0, y: 200 };Column().width(vp2px(75)).height(vp2px(75)).gesture(PanGesture({ direction: PanDirection.All }).onActionStart(() => {console.info('拖拽开始');}).onActionUpdate((event: GestureEvent) => {this.position.x += event.offsetX;this.position.y += event.offsetY;this.subWindow.moveWindowTo(this.position.x, this.position.y);}).onActionEnd((event: GestureEvent) => {const displayWidth = display.getDefaultDisplaySync().width;const windowWidth = this.subWindow.getWindowProperties().windowRect.width;if (event.offsetX > 0) {this.position.x = displayWidth - windowWidth; // 靠右} else {this.position.x = 0; // 靠左}this.subWindow.moveWindowTo(this.position.x, this.position.y);}))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

4️⃣ 主窗口响应点击事件(Router / Navigation 跳转)
使用 Router 跳转主窗口页面
.onClick(() => {globalThis.windowStage.getMainWindowSync().getUIContext().getRouter().pushUrl({ url: 'pages/DetailPage' });
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
使用 Navigation 跳转(需配合 AppStorage)
const navPathStack = AppStorage.get<NavPathStack>('navPathStack');.onClick(() => {navPathStack.pushPath({ name: 'pageOne' });
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

5️⃣ 自动适配窗口大小(基于组件变化)
@State subWindow: window.Window = null;
private flag: boolean = true;
private listener = component.createEventObserver('COMPONENT_ID');if (this.flag) {Image($r('app.media.icon1')).id('COMPONENT_ID').width(75).height(75).onClick(() => {this.flag = false;this.listener.on('layout', () => {this.subWindow.resize(componentUtils.getRectangleById('COMPONENT_ID').size.width,componentUtils.getRectangleById('COMPONENT_ID').size.height);});});
} else {Image($r('app.media.icon2')).id('COMPONENT_ID').width(100).height(100).onClick(() => {this.flag = true;this.listener.on('layout', () => {this.subWindow.resize(...);});});
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

6️⃣ 控制悬浮窗显隐与销毁
// 最小化
Button('Minimize').onClick(() => {this.subWindow.minimize();});// 销毁
Button('Destroy').onClick(() => {window.findWindow("mySubWindow").destroyWindow();});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

7️⃣ 实现视频画中画功能(PiP)
import pipWindow from '@ohos.pipWindow';let pipController: pipWindow.PiPController;startPip() {let config: pipWindow.PiPConfiguration = {context: getContext(this),componentController: this.mXComponentController,templateType: pipWindow.PiPTemplateType.VIDEO_PLAY,contentWidth: 800,contentHeight: 600};pipWindow.create(config).then(controller => {this.pipController = controller;this.pipController.setAutoStartEnabled(true); // 返回桌面自动开启 PiPthis.pipController.startPiP();}).catch(e => {console.error('启动画中画失败:', e);});
}stopPip() {if (this.pipController) {this.pipController.stopPiP();}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

⚙️ 其他注意事项

问题

解答

如何获取 windowStage

onWindowStageCreate 中用 AppStorageglobalThis 保存

子窗口是否支持跨应用?

❌ 不支持,只能用于应用内部

默认窗口大小是多少?

若未设置,默认为除去安全区外的屏幕区域

可否在 UIExtension 中使用子窗口?

❌ 不行,UIExtension 没有窗口对象

HAR/HSP 是否可用?

✅ 只要能获取到 windowStage 即可使用


📦 示例工程地址

完整项目源码已上传至 Gitee:

🔗  GitHub / Gitee 下载链接


✅ 总结

通过 HarmonyOS 提供的子窗口机制,我们可以非常灵活地实现各类悬浮窗功能,包括:

  • ✅ 自定义外观与布局
  • ✅ 拖拽靠边智能定位
  • ✅ 跨页面状态保留
  • ✅ 响应主窗口路由跳转
  • ✅ 窗口自适应大小
  • ✅ 画中画后台播放

这为构建更高级别的多窗口协同应用打下了坚实基础。


🔥 推荐阅读

  •  HarmonyOS 窗口管理官方文档
  •  ArkTS 开发指南 - 多窗口编程
  •  鸿蒙多端通信与数据共享最佳实践

如果你正在开发企业级 App 或需要统一用户界面风格的应用,不妨尝试使用子窗口 + UDMF + 画中画等多种能力组合,打造真正“沉浸式”的用户体验!

📌 欢迎收藏本博客,并关注后续更多 HarmonyOS 实战案例更新!