标准悬浮窗开发指导
=========

#### 场景介绍

标准悬浮窗是悬浮在桌面或其他应用界面上的小型窗口，为应用提供灵活的窗口管理能力。应用可以在小窗口中展示内容或提供快捷操作，用户可以在进行其他界面操作的同时查看标准悬浮窗内容，提升使用体验。

标准悬浮窗可与闪控球联合使用，在标准悬浮窗与闪控球互相绑定后，用户点击可触发标准悬浮窗与闪控球互相切换。

**全局悬浮窗和标准悬浮窗对比**

*   共同点：全局悬浮窗和标准悬浮窗均为一种特殊的应用辅助窗口，具备在应用主窗口和对应Ability退至后台后仍然可以在前台显示的能力。可以用于应用退至后台后，使用其继续显示UI，例如音乐应用用于显示桌面歌词等。
*   区别：
    *   全局悬浮窗由开发者管理并实现UI绘制，无统一UI及动效。
    *   标准悬浮窗由系统管理并统一绘制UI，动效更为高端精致。
    *   标准悬浮窗支持和闪控球联合使用，实现更复杂场景。
    *   全局悬浮窗仅支持在2in1设备上使用。
    *   标准悬浮窗支持在Phone、Tablet、2in1设备上使用。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/dd/v3/kFIj7ABDRN2fJpSaELbpuQ/note_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260612T073325Z&HW-CC-Expire=86400&HW-CC-Sign=57E91431A616B6CD6CBBDC8DF6A90BB7F5C872C01001496731488F04635D86B1)

从API版本26.0.0开始，支持使用标准悬浮窗能力。

#### 约束与限制

*   基于安全考虑，仅允许应用在前台时启动标准悬浮窗，并且需要具有ohos.permission.FLOAT\_VIEW权限。
    
*   同一个应用只能启动一个标准悬浮窗。在启动多个标准悬浮窗时，会返回错误码1300033。
    
*   同一个应用已启动闪控球或画中画窗口时，无法启动标准悬浮窗，需先停止闪控球或画中画悬浮窗口。
    
*   仅支持在Stage模型下使用标准悬浮窗相关能力。
    

#### 交互方式

标准悬浮窗提供以下交互方式：

*   **拖动窗口**：可以手动拖拽标准悬浮窗拖动热区改变位置，拖拽时自动避让状态栏、导航条、输入法键盘等系统组件。
    
*   **标题栏操作**：点击标题栏关闭按钮可关闭标准悬浮窗。在标准悬浮窗与闪控球互相绑定后，用户点击缩小按钮可触发标准悬浮窗切换为闪控球。标准悬浮窗与闪控球未互相绑定时，用户点击最小化按钮可触发标准悬浮窗收起到侧边栏。
    
*   **垃圾桶删除**：在Phone（非自由多窗模式）和Tablet（非自由多窗模式、非电脑模式）设备上，支持拖拽窗口到垃圾桶区域（底部中部区域）松手即可删除。
    
*   **侧边栏功能**：在Phone（非自由多窗模式）和Tablet（非自由多窗模式、非电脑模式）设备上，标准悬浮窗可进入系统侧边栏暂存。
    
*   **与闪控球切换**：绑定状态下，用户点击可触发标准悬浮窗与闪控球互相切换；切换为闪控球后，标准悬浮窗状态变为IN\_FLOATING\_BALL。
    

#### 规格限制

#### \[h2\]窗口尺寸相关限制

*   标准悬浮窗的窗口尺寸限制、宽高比限制通过[getFloatViewLimits()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#floatviewgetfloatviewlimits)接口获取，返回FloatViewLimits对象。
    
    建议在设置窗口大小前先调用getFloatViewLimits()获取推荐范围，并通过[onLimitsChange()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#onlimitschange)监听限制变化；若设置的窗口大小超出推荐范围，系统会将窗口大小调整到范围内，实际窗口大小可通过[onRectChange()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#onrectchange)监听获取。
    
*   标准悬浮窗属性（windowId等）通过[getWindowProperties()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#getwindowproperties)获取，返回FloatViewProperties对象。
    

#### \[h2\]模板类型

目前支持圆角矩形模板类型FloatViewTemplateType.ROUNDED\_RECTANGLE的标准悬浮窗。

#### 前提条件

使用标准悬浮窗需要申请ohos.permission.FLOAT\_VIEW，此权限为user\_grant权限，需要[声明权限](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/declare-permissions)并[向用户申请授权](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/request-user-authorization)。

#### 开发场景

#### \[h2\]基础场景：单独操作标准悬浮窗

1.  使用标准悬浮窗前请先参考[前提条件](#前提条件)申请ohos.permission.FLOAT\_VIEW权限。
    
    // 应用初始化创建，申请用户授权
    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}.\`);
        });
    }
    
2.  导入模块、声明标准悬浮窗控制器并声明页面视图组件。
    
    // 声明标准悬浮窗控制器
    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}\`);
      }
    }
    
3.  使用[create()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#floatviewcreate)接口创建标准悬浮窗控制器实例后注册状态变化事件回调。
    
4.  使用[setUIContext()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#setuicontext)设置标准悬浮窗页面内容。
    
5.  通过[getFloatViewLimits()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#floatviewgetfloatviewlimits)获取窗口尺寸限制，通过[setWindowSize()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#setwindowsize)设置合适的窗口大小。
    
6.  通过[start()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#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');
    }
    
7.  （可选）通过[onLimitsChange()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#onlimitschange)监听标准悬浮窗尺寸限制变化，动态调整窗口尺寸, 通过[onStateChange()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#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;
        }
      });
    }
    
8.  在[onStateChange()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#onstatechange)监听[start()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#start)完成后，通过[stop()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#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;
        });
      }
    }
    

#### \[h2\]复杂场景：与闪控球绑定使用

标准悬浮窗可与闪控球绑定使用，实现以下能力：

*   绑定成功后，调用任一控制器的启动接口均会同时创建标准悬浮窗窗口和闪控球窗口，同一时刻仅展示其中一个窗口。
*   绑定成功后，标准悬浮窗窗口与闪控球窗口支持用户点击触发的互相切换, 点击闪控球不会触发click回调。
*   绑定成功后，调用任一控制器的停止接口会同时销毁标准悬浮窗窗口和闪控球窗口。

绑定需要同时具有ohos.permission.USE\_FLOAT\_BALL和ohos.permission.FLOAT\_VIEW权限。

1.  使用标准悬浮窗和闪控球前请先参考[前提条件](#前提条件)申请ohos.permission.FLOAT\_VIEW和ohos.permission.USE\_FLOAT\_BALL权限。
    
    // 应用初始化创建，申请用户授权
    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}.\`);
        });
    }
    
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.  使用[floatView.create()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#floatviewcreate)接口创建标准悬浮窗控制器实例，使用[floatingBall.create()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatingball#floatingballcreate)接口创建闪控球控制器实例。
    
    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');
    }
    
4.  使用[floatView.bind()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#floatviewbind)接口将标准悬浮窗控制器与闪控球控制器绑定，绑定时需要配置闪控球参数。
    
5.  使用[setUIContext()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#setuicontext)设置标准悬浮窗页面内容。
    
6.  通过[getFloatViewLimits()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#floatviewgetfloatviewlimits)获取窗口尺寸限制，通过[setWindowSize()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#setwindowsize)设置合适的窗口大小。
    
7.  通过[start()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#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}\`);
      }
    }
    
8.  通过[stop()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#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;
        });
      }
    }
    
9.  （可选）通过[floatView.unbind()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatview#floatviewunbind)解绑标准悬浮窗与闪控球。
    
    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}\`);
      });
    }