头图

在移动应用开发中,悬浮窗是很场景的功能,比如像微信音视频通话时,为了保证通话过程可以查看和处理其他功能,允许在通话过程将通话页面以悬浮窗展示,本文我们讨论在HarmonyOS Next中如何实现通话效果的悬浮窗功能。

窗口系统是支持悬浮窗的核心能力,先来介绍下HarmonyOS Next的窗口模块。

窗口模块

窗口模块的基石作用

窗口模块在 HarmonyOS Next 中扮演着核心角色。对于应用开发者,它提供了一套完善的界面显示与交互框架,让创意应用的构建更加便捷高效。终端用户则借助它随心掌控应用界面,或聚焦于单一任务,或灵活切换、协同处理多个任务。从操作系统层面看,窗口模块宛如一位智慧的 “调度员”,有序组织不同应用界面,确保系统运行流畅稳定。

窗口模块的多元用途

  1. 窗口对象的提供者 :HarmonyOS Next 的窗口模块是应用与系统界面呈现的根基。开发者利用它加载绚丽多彩的 UI 界面,将应用的功能与视觉完美融合,让用户直观感受应用魅力。
  2. 显示关系的组织者 :系统中各类窗口类型丰富,系统窗口如音量条、通知栏等保障系统功能触手可及;应用窗口涵盖主窗口与子窗口,应用主窗口是应用的 “主舞台”,在任务管理界面一目了然;应用子窗口则像贴心的助手,以弹窗、悬浮窗等形式辅助主窗口,虽不在任务管理界面 “露面”,却在应用运行时默默助力。窗口模块精准维护它们的叠加层次与位置属性,用户还能依需求调整,打造专属的桌面布局。
  3. 窗口装饰与动效的赋予者 :默认的窗口标题栏与边框,集成实用操作按钮与便捷的拖拽缩放功能,简化用户操作流程。而窗口动效则为交互添彩,在窗口显示、隐藏与切换时,流畅自然的动画让视觉过渡舒适顺滑,无需开发者操心,系统自动呈现。
  4. 输入事件的精准分发员 :依据窗口状态与焦点,触摸、鼠标事件按位置尺寸精准投递,键盘事件直达焦点窗口,开发者通过接口还能定制窗口的触摸与获焦属性,满足多样交互需求。

应用窗口模式的灵活切换

HarmonyOS Next 支持全屏、分屏、自由窗口三种应用窗口模式,彰显强大的多窗口能力。全屏模式让应用独占视野,沉浸体验拉满;分屏模式下,两个应用并肩作战,分界线可灵活拖拽,资源对比、协同处理一键搞定;自由窗口模式赋予用户极致自由,大小位置随心变,多窗口同屏不混乱,点击即刻聚焦,多任务处理效率直线飙升。
image.png

窗口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提供的几种窗口能力。

智慧多窗

智慧多窗是一种多任务处理解决方案,它允许用户在同一时间、同一屏幕上以悬浮窗或分屏的方式同时运行多个应用窗口。在智慧多窗的显示模式下,用户可以根据自己的需求,合理安排应用窗口的位置和大小。主要是两种形式:

  • 悬浮窗:悬浮窗是一种在设备屏幕上悬浮的、非全屏的应用窗口。一般用于在已有全屏任务运行的基础上,临时处理另一个任务,或短时间多任务并行使用。如浏览网页的同时回复消息。

    • image.png
  • 分屏:分屏一般用于两个应用长时间并行使用的场景。例如边看购物攻略、边浏览商品;边看视频、边玩游戏;看学习类视频的同时做笔记等。

    • image.png

    智慧多窗是系统的一种能力,可以针对整个APP进行设置和适配,不符合我们页面级的悬浮窗场景。

画中画

应用在视频播放、视频会议、视频通话等场景下,可以使用画中画能力将视频内容以小窗(画中画)模式呈现。切换为小窗(画中画)模式后,用户可以进行其他界面操作,提升使用体验。画中画的常见使用场景有以下几种:

  • 视频播放。
  • 视频通话。
  • 视频会议。
  • 直播。

效果显示:
image.png

画中画窗口提供以下交互方式:

  • 单击画中画窗口:如果画中画控制层未显示,则显示画中画控制层,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 提供智慧多窗、画中画、悬浮窗等窗口能力,但这些无法完全满足音视频通话悬浮窗需求,最终通过应用子窗口实现悬浮窗效果,包括创建子窗口、设置属性、加载内容和销毁子窗口等步骤。


轻口味
35.2k 声望5.3k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei