在移动应用开发中,悬浮窗是很场景的功能,比如像微信音视频通话时,为了保证通话过程可以查看和处理其他功能,允许在通话过程将通话页面以悬浮窗展示,本文我们讨论在HarmonyOS Next中如何实现通话效果的悬浮窗功能。
窗口系统是支持悬浮窗的核心能力,先来介绍下HarmonyOS Next的窗口模块。
窗口模块
窗口模块的基石作用
窗口模块在 HarmonyOS Next 中扮演着核心角色。对于应用开发者,它提供了一套完善的界面显示与交互框架,让创意应用的构建更加便捷高效。终端用户则借助它随心掌控应用界面,或聚焦于单一任务,或灵活切换、协同处理多个任务。从操作系统层面看,窗口模块宛如一位智慧的 “调度员”,有序组织不同应用界面,确保系统运行流畅稳定。
窗口模块的多元用途
- 窗口对象的提供者 :HarmonyOS Next 的窗口模块是应用与系统界面呈现的根基。开发者利用它加载绚丽多彩的 UI 界面,将应用的功能与视觉完美融合,让用户直观感受应用魅力。
- 显示关系的组织者 :系统中各类窗口类型丰富,系统窗口如音量条、通知栏等保障系统功能触手可及;应用窗口涵盖主窗口与子窗口,应用主窗口是应用的 “主舞台”,在任务管理界面一目了然;应用子窗口则像贴心的助手,以弹窗、悬浮窗等形式辅助主窗口,虽不在任务管理界面 “露面”,却在应用运行时默默助力。窗口模块精准维护它们的叠加层次与位置属性,用户还能依需求调整,打造专属的桌面布局。
- 窗口装饰与动效的赋予者 :默认的窗口标题栏与边框,集成实用操作按钮与便捷的拖拽缩放功能,简化用户操作流程。而窗口动效则为交互添彩,在窗口显示、隐藏与切换时,流畅自然的动画让视觉过渡舒适顺滑,无需开发者操心,系统自动呈现。
- 输入事件的精准分发员 :依据窗口状态与焦点,触摸、鼠标事件按位置尺寸精准投递,键盘事件直达焦点窗口,开发者通过接口还能定制窗口的触摸与获焦属性,满足多样交互需求。
应用窗口模式的灵活切换
HarmonyOS Next 支持全屏、分屏、自由窗口三种应用窗口模式,彰显强大的多窗口能力。全屏模式让应用独占视野,沉浸体验拉满;分屏模式下,两个应用并肩作战,分界线可灵活拖拽,资源对比、协同处理一键搞定;自由窗口模式赋予用户极致自由,大小位置随心变,多窗口同屏不混乱,点击即刻聚焦,多任务处理效率直线飙升。
窗口API介绍
窗口提供管理窗口的一些基础能力,包括对当前窗口的创建、销毁、各属性设置,以及对各窗口间的管理调度。窗口模块提供以下窗口相关的常用功能:
- Window:当前窗口实例,窗口管理器管理的基本单元。
- WindowStage:窗口管理器。管理各个基本窗口单元。
以下是从该链接中提取出的方法列表:
WindowStage 方法
getMainWindow(callback: AsyncCallback<Window>): void
: 获取该 WindowStage 实例下的主窗口,使用 callback 异步回调。getMainWindow(): Promise<Window>
: 获取该 WindowStage 实例下的主窗口,使用 Promise 异步回调。getMainWindowSync(): Window
: 获取该 WindowStage 实例下的主窗口,同步操作。createSubWindow(name: string, callback: AsyncCallback<Window>): void
: 创建该 WindowStage 实例下的子窗口,使用 callback 异步回调。createSubWindow(name: string): Promise<Window>
: 创建该 WindowStage 实例下的子窗口,使用 Promise 异步回调。createSubWindowWithOptions(name: string, options: SubWindowOptions): Promise<Window>
: 创建该 WindowStage 实例下的子窗口,使用 Promise 异步回调,并可设置子窗口参数。getSubWindow(callback: AsyncCallback<Array<Window>>): void
: 获取该 WindowStage 实例下的所有子窗口,使用 callback 异步回调。getSubWindow(): Promise<Array<Window>>
: 获取该 WindowStage 实例下的所有子窗口,使用 Promise 异步回调。loadContent(path: string, storage: LocalStorage, callback: AsyncCallback<void>): void
: 为当前 WindowStage 的主窗口加载具体页面内容,通过 LocalStorage 传递状态属性,使用 callback 异步回调。loadContent(path: string, storage?: LocalStorage): Promise<void>
: 为当前 WindowStage 的主窗口加载具体页面内容,通过 LocalStorage 传递状态属性,使用 Promise 异步回调。loadContent(path: string, callback: AsyncCallback<void>): void
: 为当前 WindowStage 的主窗口加载具体页面内容,使用 callback 异步回调。loadContentByName(name: string, storage: LocalStorage, callback: AsyncCallback<void>): void
: 为当前 WindowStage 加载命名路由页面,通过 LocalStorage 传递状态属性,使用 callback 异步回调。loadContentByName(name: string, callback: AsyncCallback<void>): void
: 为当前 WindowStage 加载命名路由页面,使用 callback 异步回调。loadContentByName(name: string, storage?: LocalStorage): Promise<void>
: 为当前 WindowStage 加载命名路由页面,通过 LocalStorage 传递状态属性,使用 Promise 异步回调。on(eventType: 'windowStageEvent', callback: Callback<WindowStageEventType>): void
: 开启 WindowStage 生命周期变化的监听。off(eventType: 'windowStageEvent', callback?: Callback<WindowStageEventType>): void
: 关闭 WindowStage 生命周期变化的监听。setDefaultDensityEnabled(enabled: boolean): void
: 设置应用是否使用系统默认 Density。
Window 方法
showWindow(callback: AsyncCallback<void>): void
: 显示当前窗口,使用 callback 异步回调,仅支持系统窗口及应用子窗口,或将已显示的应用主窗口的层级提升至顶部。showWindow(): Promise<void>
: 显示当前窗口,使用 Promise 异步回调。destroyWindow(callback: AsyncCallback<void>): void
: 销毁当前窗口,使用 callback 异步回调。destroyWindow(): Promise<void>
: 销毁当前窗口,使用 Promise 异步回调。moveWindowTo(x: number, y: number, callback: AsyncCallback<void>): void
: 移动窗口位置,使用 callback 异步回调。moveWindowTo(x: number, y: number): Promise<void>
: 移动窗口位置,使用 Promise 异步回调。resize(width: number, height: number, callback: AsyncCallback<void>): void
: 改变当前窗口大小,使用 callback 异步回调。resize(width: number, height: number): Promise<void>
: 改变当前窗口大小,使用 Promise 异步回调。getWindowProperties(): WindowProperties
: 获取当前窗口的属性。getWindowAvoidArea(type: AvoidAreaType): AvoidArea
: 获取当前窗口内容规避的区域。setWindowFocusable(isFocusable: boolean, callback: AsyncCallback<void>): void
: 设置窗口是否为可触状态,使用 callback 异步回调。setWindowFocusable(isFocusable: boolean): Promise<void>
: 设置窗口是否为可触状态,使用 Promise 异步回调。setWindowBackgroundColor(color: string): void
: 设置窗口的背景色。setWindowBrightness(brightness: number, callback: AsyncCallback<void>): void
: 允许应用主窗口设置屏幕亮度值,使用 callback 异步回调。setWindowBrightness(brightness: number): Promise<void>
: 允许应用主窗口设置屏幕亮度值,使用 Promise 异步回调。setWindowSystemBarProperties(systemBarProperties: SystemBarProperties, callback: AsyncCallback<void>): void
: 设置主窗口三键导航栏、状态栏的属性,使用 callback 异步回调。setWindowSystemBarProperties(systemBarProperties: SystemBarProperties): Promise<void>
: 设置主窗口三键导航栏、状态栏的属性,使用 Promise 异步回调。setWindowLayoutFullScreen(isLayoutFullScreen: boolean, callback: AsyncCallback<void>): void
: 设置主窗口或子窗口的布局是否为沉浸式布局,使用 callback 异步回调。setWindowLayoutFullScreen(isLayoutFullScreen: boolean): Promise<void>
: 设置主窗口或子窗口的布局是否为沉浸式布局,使用 Promise 异步回调。isWindowShowing(): boolean
: 判断当前窗口是否已显示。on(type: 'windowSizeChange', callback: Callback<Size>): void
: 开启窗口尺寸变化的监听。off(type: 'windowSizeChange', callback?: Callback<Size>): void
: 关闭窗口尺寸变化的监听。on(type: 'avoidAreaChange', callback: Callback<AvoidAreaOptions>): void
: 开启当前应用窗口系统规避区变化的监听。off(type: 'avoidAreaChange', callback?: Callback<AvoidAreaOptions>): void
: 关闭当前应用窗口系统规避区变化的监听。on(type: 'keyboardHeightChange', callback: Callback<number>): void
: 开启固定态软键盘高度变化的监听。off(type: 'keyboardHeightChange', callback?: Callback<number>): void
: 关闭固定态软键盘高度变化的监听。on(type: 'touchOutside', callback: Callback<void>): void
: 开启本窗口区域范围外的点击事件的监听。off(type: 'touchOutside', callback?: Callback<void>): void
: 关闭本窗口区域范围外的点击事件的监听。on(type: 'screenshot', callback: Callback<void>): void
: 开启截屏事件的监听。off(type: 'screenshot', callback?: Callback<void>): void
: 关闭截屏事件的监听。on(type: 'windowEvent', callback: Callback<WindowEventType>): void
: 开启窗口生命周期变化的监听。off(type: 'windowEvent', callback?: Callback<WindowEventType>): void
: 关闭窗口生命周期变化的监听。on(type: 'windowVisibilityChange', callback: Callback<boolean>): void
: 开启本窗口可见状态变化事件的监听。off(type: 'windowVisibilityChange', callback?: Callback<boolean>): void
: 关闭本窗口可见状态变化事件的监听。on(type: 'noInteractionDetected', timeout: number, callback: Callback<void>): void
: 开启本窗口在指定超时时间内无交互事件的监听。off(type: 'noInteractionDetected', callback?: Callback<void>): void
: 关闭本窗口在指定超时时间内无交互事件的监听。on(type: 'windowStatusChange', callback: Callback<WindowStatusType>): void
: 开启窗口模式变化的监听。off(type: 'windowStatusChange', callback?: Callback<WindowStatusType>): void
: 关闭窗口模式变化的监听。on(type: 'windowRectChange', callback: Callback<RectChangeOptions>): void
: 开启窗口矩形变化的监听。off(type: 'windowRectChange', callback?: Callback<RectChangeOptions>): void
: 关闭窗口矩形变化的监听。on(type: 'subWindowClose', callback: Callback<void>): void
: 子窗口关闭时触发该事件。off(type: 'subWindowClose', callback?: Callback<void>): void
: 关闭子窗口关闭事件的监听。minimize(callback: AsyncCallback<void>): void
: 将窗口最小化,使用 callback 异步回调。minimize(): Promise<void>
: 将窗口最小化,使用 Promise 异步回调。maximize(presentation?: MaximizePresentation): Promise<void>
: 主窗口调用,实现最大化功能,使用 Promise 异步回调。recover(): Promise<void>
: 将主窗口从全屏、最大化、分屏模式下还原为浮动窗口,并恢复到进入该模式之前的大小和位置。getWindowLimits(): WindowLimits
: 获取当前应用窗口的尺寸限制。setWindowLimits(windowLimits: WindowLimits): Promise<WindowLimits>
: 设置当前应用窗口的尺寸限制。setWindowMask(windowMask: Array<Array<number>>): Promise<void>
: 设置异形窗口的掩码。keepKeyboardOnFocus(keepKeyboardFlag: boolean): void
: 窗口获焦时保留由其他窗口创建的软键盘。setWindowDecorVisible(isVisible: boolean): void
: 设置窗口标题栏是否可见。setSubWindowModal(isModal: boolean): Promise<void>
: 设置子窗的模态属性是否启用。setWindowDecorHeight(height: number): void
: 设置窗口的标题栏高度。getTitleButtonRect(): TitleButtonRect
: 获取主窗口或启用装饰的子窗口的标题栏上的最小化、最大化、关闭按钮矩形区域。getWindowStatus(): WindowStatusType
: 获取当前应用窗口的模式。isFocused(): boolean
: 判断当前窗口是否已获焦。createSubWindowWithOptions(name: string, options: SubWindowOptions): Promise<Window>
: 创建主窗口或子窗口下的子窗口。enableLandscapeMultiWindow(): Promise<void>
: 应用部分界面支持横向布局时,在进入该界面时使能,使能后可支持进入横向多窗。disableLandscapeMultiWindow(): Promise<void>
: 应用部分界面支持横向布局时,在退出该界面时去使能,去使能后不支持进入横向多窗。setDialogBackGestureEnabled(enabled: boolean): Promise<void>
: 设置模态窗口是否响应手势返回事件。
HarmonyOS Next 悬浮窗能力实现方法
在介绍悬浮窗的实现方式前先再了解一下HarmonyOS Next提供的窗口类型。
窗口类型
HarmonyOS的窗口模块将窗口界面分为系统窗口、应用窗口两种基本类型。
- 系统窗口:系统窗口指完成系统特定功能的窗口。如音量条、壁纸、通知栏、状态栏、导航栏等。
应用窗口:应用窗口区别于系统窗口,指与应用显示相关的窗口。根据显示内容的不同,应用窗口又分为应用主窗口、应用子窗口两种类型。
- 应用主窗口:应用主窗口用于显示应用界面,会在"任务管理界面"显示。
- 应用子窗口:应用子窗口用于显示应用的弹窗
其中,系统对应用窗口的限制如下:
- 应用主窗口与子窗口存在大小限制,宽度范围:
[320, 2560]
,高度范围:[240, 2560]
,单位为vp。 - 系统窗口存在大小限制,宽度范围:(0, 2560],高度范围:(0, 2560],单位为vp。
针对应用窗口的管理主要体现在:
- 窗口沉浸式能力:指对状态栏、导航栏等系统窗口进行控制,减少状态栏导航栏等系统界面的突兀感,从而使用户获得最佳体验的能力。沉浸式能力只在应用主窗口作为全屏窗口时生效。通常情况下,应用子窗口(弹窗、悬浮窗口等辅助窗口)和处于自由窗口下的应用主窗口无法使用沉浸式能力。
- 悬浮窗:全局悬浮窗口是一种特殊的应用窗口,具备在应用主窗口和对应Ability退至后台后仍然可以在前台显示的能力。悬浮窗口可以用于应用退至后台后,使用小窗继续播放视频,或者为特定的应用创建悬浮球等快速入口。应用在创建悬浮窗口前,需要申请对应的权限。
下面介绍HarmonyOS Next提供的几种窗口能力。
智慧多窗
智慧多窗是一种多任务处理解决方案,它允许用户在同一时间、同一屏幕上以悬浮窗或分屏的方式同时运行多个应用窗口。在智慧多窗的显示模式下,用户可以根据自己的需求,合理安排应用窗口的位置和大小。主要是两种形式:
悬浮窗:悬浮窗是一种在设备屏幕上悬浮的、非全屏的应用窗口。一般用于在已有全屏任务运行的基础上,临时处理另一个任务,或短时间多任务并行使用。如浏览网页的同时回复消息。
分屏:分屏一般用于两个应用长时间并行使用的场景。例如边看购物攻略、边浏览商品;边看视频、边玩游戏;看学习类视频的同时做笔记等。
智慧多窗是系统的一种能力,可以针对整个APP进行设置和适配,不符合我们页面级的悬浮窗场景。
画中画
应用在视频播放、视频会议、视频通话等场景下,可以使用画中画能力将视频内容以小窗(画中画)模式呈现。切换为小窗(画中画)模式后,用户可以进行其他界面操作,提升使用体验。画中画的常见使用场景有以下几种:
- 视频播放。
- 视频通话。
- 视频会议。
- 直播。
效果显示:
画中画窗口提供以下交互方式:
- 单击画中画窗口:如果画中画控制层未显示,则显示画中画控制层,3秒后自动隐藏控制层;如果当前控制层已显示,则隐藏画中画控制层。
- 双击画中画窗口:放大或缩小画中画窗口。
- 拖动画中画窗口:可以将画中画窗口拖动到屏幕任意位置。如果将画中画窗口拖动到屏幕左右边缘,画中画窗口会自动隐藏;隐藏后在屏幕边缘显示画中画隐藏图标,点击该图标后画中画窗口恢复显示。
使用画中画的限制:
- 需要在支持SystemCapability.Window.SessionManager能力的系统上使用该模块,参考系统能力SystemCapability使用指南。
- 基于安全考虑,应用处于后台时不允许通过startPiP启动画中画。针对应用返回后台时需要启动画中画的场景,建议使用setAutoStartEnabled(true)实现自动启动。
另一方面,画中画适合有视频的画面,如果是语音通话场景没有视频画面,不太适合使用画中画,而且画中画对画面的最小宽高有要求,无法满足诉求。
悬浮窗
基于窗口的悬浮窗可以在已有的任务基础上,创建一个始终在前台显示的窗口。即使创建悬浮窗的任务退至后台,悬浮窗仍然可以在前台显示。通常悬浮窗位于所有应用窗口之上,开发者可以创建悬浮窗,并对悬浮窗进行属性设置等操作。
创建WindowType.TYPE_FLOAT即悬浮窗类型的窗口,需要申请ohos.permission.SYSTEM_FLOAT_WINDOW权限,该权限为受控开放权限,仅符合指定场景的在2in1设备上的应用可申请该权限,可申请此权限的特殊场景与功能:
- 多人视频通话
- 屏幕共享
- 当前仅2in1设备应用可申请此权限。
单人音视频通话不符合要求,所以无法申请该权限。
系统子窗口
上面提到的智慧多窗、画中画、悬浮窗都无法满足我们针对音视频通话最小化页面时悬浮窗效果,最后我们的主角子窗口上场,可以基于子窗口的能力来实现悬浮窗效果。
首先可以通过createSubWindow接口创建应用子窗口;接下来设置子窗口属性,子窗口创建成功后,可以改变其大小、位置等,还可以根据应用需要设置窗口背景色、亮度等属性;再接下来加载显示子窗口的具体内容,通过setUIContent和showWindow接口加载显示子窗口的具体内容,最后使用完成后可以销毁子窗口。
下面是最基础的子窗口显示示例:
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
let windowStage_: window.WindowStage | null = null;
let sub_windowClass: window.Window | null = null;
export default class EntryAbility extends UIAbility {
showSubWindow() {
// 1.创建应用子窗口。
if (windowStage_ == null) {
console.error('Failed to create the subwindow. Cause: windowStage_ is null');
}
else {
windowStage_.createSubWindow("mySubWindow", (err: BusinessError, data) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err));
return;
}
sub_windowClass = data;
console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data));
// 2.子窗口创建成功后,设置子窗口的位置、大小及相关属性等。
sub_windowClass.moveWindowTo(300, 300, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to move the window. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in moving the window.');
});
sub_windowClass.resize(500, 500, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to change the window size. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in changing the window size.');
});
// 3.为子窗口加载对应的目标页面。
sub_windowClass.setUIContent("pages/page3", (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to load the content. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in loading the content.');
// 3.显示子窗口。
(sub_windowClass as window.Window).showWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to show the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in showing the window.');
});
});
})
}
}
destroySubWindow() {
// 4.销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。
(sub_windowClass as window.Window).destroyWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in destroying the window.');
});
}
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage_ = windowStage;
// 开发者可以在适当的时机,如主窗口上按钮点击事件等,创建子窗口。并不一定需要在onWindowStageCreate调用,这里仅作展示
this.showSubWindow();
}
onWindowStageDestroy() {
// 开发者可以在适当的时机,如子窗口上点击关闭按钮等,销毁子窗口。并不一定需要在onWindowStageDestroy调用,这里仅作展示
this.destroySubWindow();
}
};
上面是直接在onWindowStageCreate里面创建子窗口,如果想在代码任意地方创建,可以在onWindowStageCreate中将window.WindowStage通过AppStorage.setOrCreate('windowStage', windowStage);进行全局存储,然后在某个page页面通过点击按钮创建子窗口:
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed to load the content. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in loading the content.');
})
// 给Index页面传递windowStage
AppStorage.setOrCreate('windowStage', windowStage);
}
// Index.ets
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
let windowStage_: window.WindowStage | undefined = undefined;
let sub_windowClass: window.Window | undefined = undefined;
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
private CreateSubWindow(){
// 获取windowStage
windowStage_ = AppStorage.get('windowStage');
// 1.创建应用子窗口。
if (windowStage_ == null) {
console.error('Failed to create the subwindow. Cause: windowStage_ is null');
}
else {
windowStage_.createSubWindow("mySubWindow", (err: BusinessError, data) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err));
return;
}
sub_windowClass = data;
console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data));
// 2.子窗口创建成功后,设置子窗口的位置、大小及相关属性等。
sub_windowClass.moveWindowTo(300, 300, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to move the window. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in moving the window.');
});
sub_windowClass.resize(500, 500, (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to change the window size. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in changing the window size.');
});
// 3.为子窗口加载对应的目标页面。
sub_windowClass.setUIContent("pages/subWindow", (err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to load the content. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in loading the content.');
// 3.显示子窗口。
(sub_windowClass as window.Window).showWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to show the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in showing the window.');
});
});
})
}
}
private destroySubWindow(){
// 4.销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。
(sub_windowClass as window.Window).destroyWindow((err: BusinessError) => {
let errCode: number = err.code;
if (errCode) {
console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in destroying the window.');
});
}
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
Button(){
Text('CreateSubWindow')
.fontSize(24)
.fontWeight(FontWeight.Normal)
}.width(220).height(68)
.margin({left:10, top:60})
.onClick(() => {
this.CreateSubWindow()
})
Button(){
Text('destroySubWindow')
.fontSize(24)
.fontWeight(FontWeight.Normal)
}.width(220).height(68)
.margin({left:10, top:60})
.onClick(() => {
this.destroySubWindow()
})
}
.width('100%')
}
.height('100%')
}
}
// subWindow.ets
@Entry
@Component
struct SubWindow {
@State message: string = 'Hello subWindow';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
这里面通过sub_windowClass.setUIContent加载对应页面,如果想基于路由加载页面,可以使用sub_windowClass.loadContentByName,子窗口页面配置对应路由和LocalStorage来接收参数:
export const entryName : string = 'voip/CallWindowPage';
@Entry({ routeName: entryName, storage : LocalStorage.getShared() })
@Component
export struct CallWindowPage {
...
}
接下来还要实现子窗口拖动效果,需要给子窗口的对应页面增加手势:
// subWindow.ets
export const entryName : string = 'voip/CallWindowPage';
@Entry({ routeName: entryName, storage : LocalStorage.getShared() })
@Component
struct SubWindow {
@State message: string = 'Hello subWindow';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
gesture(
PanGesture(this.panOption)
.onActionStart((event: GestureEvent) => {})
.onActionUpdate((event: GestureEvent) => {
this.windowPosition.x += event.offsetX;
this.windowPosition.y += event.offsetY;
let top = RtcConstants.DEFAULT_HEIGHT;
let bottom = display.getDefaultDisplaySync().height - this.subWindow.getWindowProperties().windowRect.height
- top;
if (this.windowPosition.y < top) {
this.windowPosition.y = top;
} else if (this.windowPosition.y > bottom) {
this.windowPosition.y = bottom;
}
this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y);
})
.onActionEnd((event: GestureEvent) => {
if (this.windowPosition.x >= display.getDefaultDisplaySync().width / 2) {
this.windowPosition.x = display.getDefaultDisplaySync().width -
this.subWindow.getWindowProperties().windowRect.width;
} else if (event.offsetX < display.getDefaultDisplaySync().width / 2) {
this.windowPosition.x = 0;
}
let top = RtcConstants.DEFAULT_HEIGHT;
let bottom = display.getDefaultDisplaySync().height - this.subWindow.getWindowProperties().windowRect.height
- top;
if (this.windowPosition.y < top) {
this.windowPosition.y = top;
} else if (this.windowPosition.y > bottom) {
this.windowPosition.y = bottom;
}
this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y);
})
}
}
到这里音视频通话悬浮窗页面实现完成了,在里面做具体的业务逻辑即可。
总结
在移动应用开发中,悬浮窗功能很重要,如微信音视频通话时可将通话页面以悬浮窗展示。本文讨论在 HarmonyOS Next 中实现该功能。窗口模块是核心,提供界面显示与交互框架,支持窗口对象提供、显示关系组织、窗口装饰与动效赋予、输入事件分发等功能。HarmonyOS Next 支持全屏、分屏、自由窗口三种应用窗口模式。窗口 API 包括 WindowStage 方法和 Window 方法,用于窗口的创建、销毁、属性设置和管理调度。HarmonyOS Next 提供智慧多窗、画中画、悬浮窗等窗口能力,但这些无法完全满足音视频通话悬浮窗需求,最终通过应用子窗口实现悬浮窗效果,包括创建子窗口、设置属性、加载内容和销毁子窗口等步骤。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。