目录

闪控窗开发指导

场景介绍

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

闪控窗适用于需要在独立小窗口中持续展示应用内容或提供快捷操作的场景。比如股市盯盘应用、直播应用。

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

从API版本26.0.0开始,支持使用闪控窗能力。

约束与限制

交互方式

闪控窗提供以下交互方式:

规格限制

[h2]窗口尺寸相关限制

[h2]模板类型

闪控窗支持的模板类型可见FloatViewTemplateType ,不同模板类型效果可见闪控窗视觉规格 。

前提条件

使用闪控窗需要申请ohos.permission.FLOAT_VIEW,此权限为user_grant权限,需要声明权限并向用户申请授权。

开发场景

[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.checkAccessToken(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();
      let hostContext = this.getUIContext().getHostContext();
      atManager.requestPermissionsFromUser(hostContext, ['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()接口创建闪控窗控制器实例后注册状态变化事件回调。

  4. 使用setUIContext()设置闪控窗页面内容。

  5. 通过getFloatViewLimits()获取窗口尺寸限制,通过setWindowSize()设置合适的窗口大小。

  6. 通过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()监听闪控窗尺寸限制变化,动态调整窗口尺寸, 通过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()监听start()完成后,通过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]复杂场景:与闪控球绑定使用

闪控窗可与闪控球绑定使用,实现以下能力:

绑定需要同时具有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.checkAccessToken(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()接口创建闪控窗控制器实例,使用floatingBall.create()接口创建闪控球控制器实例。

    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()接口将闪控窗控制器与闪控球控制器绑定,绑定时需要配置闪控球参数。

  5. 使用setUIContext()设置闪控窗页面内容。

  6. 通过getFloatViewLimits()获取窗口尺寸限制,通过setWindowSize()设置合适的窗口大小。

  7. 通过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()停止闪控窗,绑定状态下会同时停止闪控球。

    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()解绑闪控窗与闪控球。

    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}`);
      });
    }

[h2]复杂场景:与防窥保护组合使用

闪控窗支持与防窥保护功能联动使用。当用户通过闪控窗查看敏感信息时,系统将自动通过传感器检测周围环境,一旦识别到非机主的窥视行为,会立即在闪控窗上拉起蒙层以遮盖内容,防止隐私泄露。用户也可根据情况手动解除此保护。

防窥保护基于SystemCapability.Security.DlpAntiPeep能力实现。识别逻辑为:系统将长期通过人脸解锁的用户标记为机主。当传感器检测到非机主与机主同时注视屏幕时,会通过回调向应用发送被窥视状态通知(HIDE),应用据此触发隐私保护动作。

前提条件

  1. 封装防窥保护工具类,核心能力为:检测设备是否支持防窥功能、查询系统开关状态、获取当前窥视状态,并在必要时调用系统蒙层接口进行隐私遮挡。

    // 判断是否支持防窥保护
    export function canUseAntiPeep(): boolean {
      return canIUse('SystemCapability.Security.DlpAntiPeep');
    }
    
    // 判断防窥保护是否打开
    export async function isAntiPeepOn(): Promise<boolean> {
      try {
        let result: boolean = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
        console.info(TAG + `isAntiPeepOn isDlpAntiPeepSwitchOn success. ${result}`)
        return result;
      } catch (err) {
        console.error(TAG + `[isAntiPeepOn] isDlpAntiPeepSwitchOn failed.${JSON.stringify(err)}`);
        return false;
      }
    }
    
    // 获取防窥保护状态
    export function getAntiPeepInfo(): dlpAntiPeep.DlpAntiPeepStatus {
      try {
        let dlpAntiPeepStatus = dlpAntiPeep.getDlpAntiPeepInfo();
        console.info(TAG + `getDlpHideInfo success. ${JSON.stringify(dlpAntiPeepStatus)}`);
        return dlpAntiPeepStatus;
      } catch (err) {
        console.info(TAG + `getDlpHideInfo failed. ${JSON.stringify(err)}`);
        return -1;
      }
    }
    
    // 开启全局保护
    export function showSystemMaskLayer(windowId: number): Promise<boolean> {
      return new Promise((resolve) => {
        try {
          if (canUseAntiPeep()) {
            dlpAntiPeep.setAntiPeepMaskLayer(windowId).catch((err: BusinessError) => {
              console.error(
                TAG + `Execute setAntiPeepMaskLayer failed. error code:${err.code}, error message:${err.message}`);
              resolve(false);
            }).then(() => {
              console.info(TAG + `setAntiPeepMaskLayer success`);
              resolve(true);
            })
          } else {
            resolve(false);
          }
        } catch (err) {
          console.error(TAG + `Call setAntiPeepMaskLayer failed. ${JSON.stringify(err)}`);
          resolve(false);
        }
      });
    }
  2. 注册防窥保护状态监听。

    调用dlpAntiPeep.on('dlpAntiPeep')接口注册防窥保护状态监听。其中PASS表示当前设备屏幕无人窥视,HIDE表示有除机主以外的人在窥视设备屏幕。

    export interface AntiPeepCallback {
      onStatusChanged: (status: dlpAntiPeep.DlpAntiPeepStatus) => Promise<void>;
    }
    
    export function listenOnAntiPeepStatus(antiPeepCB: AntiPeepCallback): boolean {
      try {
        console.info(TAG + `start on('dlpAntiPeep')`);
        dlpAntiPeep.on('dlpAntiPeep', (dlpAntiPeepStatus: dlpAntiPeep.DlpAntiPeepStatus) => {
          if (antiPeepCB) {
            antiPeepCB.onStatusChanged(dlpAntiPeepStatus);
          } else {
            console.warn(TAG + `antiPeepCB is empty`);
          }
        });
        console.info(TAG + `on('dlpAntiPeep') ok`);
        return true;
      } catch (err) {
        console.error(TAG + `dlpAntiPeep.on failed. ${JSON.stringify(err)}`);
        return false;
      }
    }
  3. 初始化防窥状态并注册回调,申请闪控窗权限。

    初始化时依次检查设备支持情况、开关状态与当前状态。

    声明及初始化防窥回调处理函数,回调处理逻辑为:当状态为HIDE时,获取应用主窗并调用setAntiPeepMaskLayer()拉起系统蒙层。

    isListenOn: boolean = false;
    isPeep: boolean = false;
    isGranted: boolean = false;
    private floatViewController: floatView.FloatViewController | undefined = undefined;
    antiPeepCB: AntiPeepCallback = {
      onStatusChanged: async (status: dlpAntiPeep.DlpAntiPeepStatus): Promise<void> => {
        await this.handleAntiPeepStatus(status);
      }
    };
    
    // 声明防窥回调处理函数
    private async handleAntiPeepStatus(status: dlpAntiPeep.DlpAntiPeepStatus) {
      console.info(TAG + `[handleAntiPeepStatus] ${status}`);
      switch (status) {
        case dlpAntiPeep.DlpAntiPeepStatus.PASS:
          console.info(TAG + 'DlpAntiPeepStatus is PASS');
          break;
        case dlpAntiPeep.DlpAntiPeepStatus.HIDE:
          // 从存储中获取已保存的窗口信息
          let window: window.Window = AppStorage.get('MAIN_WINDOW') as window.Window;
          const windowId: number = window.getUIContext().getWindowId() as number;
          console.info(TAG + `DlpAntiPeepStatus is HIDE ${windowId}`);
          dlpAntiPeep.setAntiPeepMaskLayer(windowId)
            .then((result) => {
              console.info(TAG, `setAntiPeepMaskLayer success + ${result}`);
            })
            .catch((err: BusinessError) => {
              console.error(TAG,
                `Execute setAntiPeepMaskLayer failed. error code:${err.code}, error message:${err.message}`);
            })
          break;
        default:
          break;
      }
    }
    
    // 初始化防窥状态及注册回调
    private initAntiPeepStatus() {
      if (canUseAntiPeep()) { // Check if the device is supported
        isAntiPeepOn().then((opened) => {
          if (opened) {
            let info = getAntiPeepInfo();
            this.handleAntiPeepStatus(info);
            this.isListenOn = listenOnAntiPeepStatus(this.antiPeepCB);
            if (this.isListenOn) {
              console.info(TAG + 'succeed in listenOnAntiPeepStatus ')
            }
          } else {
            try {
              this.getUIContext().getPromptAction().showToast({
                message: $r('app.string.anti_peep_not_enable')
              });
            } catch (error) {
              console.error(TAG + 'show toast error. code =' + error.code + ', message =' + error.message);
            }
          }
        })
      } else {
        try {
          this.getUIContext().getPromptAction().showToast({
            message: $r('app.string.device_not_supported')
          });
        } catch (error) {
          console.error(TAG + 'show toast error. code =' + error.code + ', message =' + error.message);
        }
      }
    }
    
    // 确认用户授权状态
    private requestPermission(): void {
      let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
      let hostContext = this.getUIContext().getHostContext();
      atManager.requestPermissionsFromUser(hostContext, ['ohos.permission.FLOAT_VIEW'] as Permissions[])
        .then((data) => {
          console.info(TAG + `grant result: ${data.authResults}.`);
          this.isGranted = data.authResults[0] === 0;
        })
        .catch((reason: BusinessError) => {
          console.error(TAG + `requestPermissionsFromUser failed, ${reason?.code}, ${reason?.message}.`);
        });
    }
    
    // 应用初始化创建,申请用户授权
    aboutToAppear(): void {
      this.initAntiPeepStatus();
      this.requestPermission();
      let enable: boolean = floatView.isFloatViewEnabled();
      console.info(TAG + 'floatView enabled is: ' + enable);
    }
  4. 创建并启动闪控窗。

    使用floatView.create()创建控制器,通过setUIContext()、getFloatViewLimits()、setWindowSize()、start()完成页面内容设置与启动,并通过onStateChange()、onRectChange()、onLimitsChange()注册状态与尺寸限制变化回调。

    // 启动闪控窗
    async createWindowAntiPeep(): 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');
    }
    
    // 注册闪控窗尺寸限制变化回调函数
    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;
        }
      });
    }
  5. 通过stop()停止闪控窗。

    // 停止闪控窗
    deleteAll(): void {
      console.info(TAG + 'Deleting all');
      if (this.floatViewController) {
        this.floatViewController.stop().then(() => {
          console.info(TAG + 'Float view stopped');
        }).catch((err: BusinessError) => {
          console.error(TAG + `Failed to delete float view: ${err.code} reason ${err.message}`);
        });
      }
    }