使用全局闪控球实现应用外截图
==============

#### 问题现象

如何实现始终在前台展示的闪控球，即使应用退到后台也不影响闪控球的展示，以及点击该闪控球可以实现应用外截图功能？

#### 效果预览

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/23/v3/YMcEW6iXS4ixe_nCS5E5Iw/zh-cn_image_0000002628405560.gif?HW-CC-KV=V1&HW-CC-Date=20260709T134011Z&HW-CC-Expire=86400&HW-CC-Sign=73D4CDAF82D687FEB18BFA63608228A9E83CF2A07F7F8B3780BDFBF2098680AB "点击放大")

#### 背景知识

*   [floatingBall](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatingball)闪控球是一种在设备屏幕上悬浮的非全屏应用窗口，为应用提供临时的全局能力，完成跨应用交互。应用可以将关键信息（如比价、搜题或抢单等）以小窗（闪控球）模式呈现。切换为小窗（闪控球）模式后，用户可以进行其他界面操作，提升使用体验。使用约束参考[约束与限制](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/floatingball-guide#section121816252291)。
*   [screenshot.capture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-screenshot#screenshotcapture14)获取屏幕全屏截图，在API version 21之前，该接口在2in1设备、Tablet设备中可正常调用，在其他设备中返回801错误码。从API version 21开始，该接口在Phone设备、2in1设备、Tablet设备中可正常调用。
*   [getFloatingBallWindowInfo](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatingball#getfloatingballwindowinfo)获得闪控球窗口信息，其返回的windowId仅用于screenshot.capture截图时排除闪控球本身，参考[CaptureOption](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-screenshot#captureoption14)。

#### 解决方案

可以使用闪控球实现始终在前台展示的闪控球窗口，示例可参考[全局闪控球开发指导](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/floatingball-guide)。实现应用外截图可利用闪控球实现，具体实现步骤参考如下。

*   使用闪控球及屏幕截图需要申请ohos.permission.USE\_FLOAT\_BALL及ohos.permission.CUSTOM\_SCREEN\_CAPTURE权限，module.json5中配置参考如下：
    
    "requestPermissions": \[
      {
        "name": "ohos.permission.USE\_FLOAT\_BALL"
      },
      {
        "name": "ohos.permission.CUSTOM\_SCREEN\_CAPTURE",
        "reason": "$string:capture",
        "usedScene" : {
          "abilities" : \[
            "EntryAbility"
          \],
          "when" : "inuse"
        }
      }
    \],
    
*   Utils.ets：使用[create()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatingball#floatingballcreate)接口创建闪控球控制器；通过[startFloatingBall()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatingball#startfloatingball)接口启动闪控球；通过[updateFloatingBall()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatingball#updatefloatingball)更新闪控球信息，以此控制闪控球展示的内容；通过[stopFloatingBall()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatingball#stopfloatingball)停止闪控球。当不再需要显示闪控球时，可根据业务需要关闭闪控球。
    
    _// 该页面提供工具类，展示闪控球的创建、更新、关闭逻辑_
    import hilog from '@ohos.hilog';
    import image from '@ohos.multimedia.image';
    import { BusinessError } from '@kit.BasicServicesKit';
    import { floatingBall } from '@kit.ArkUI';
    import { ContextUtil } from './ContextUtil';
    
    const DOMAIN: number = 0xF811;
    const TAG: string = '\[Sample\_FloatingBall\]';
    
    export class Utils {
      public static getRawfilePixelMapSync(path: string): image.PixelMap {
        try {
          const BUFFER = ContextUtil.context.resourceManager.getRawFileContentSync(path);
          const IMAGE\_SOURCE: image.ImageSource = image.createImageSource(BUFFER.buffer as ArrayBuffer);
          hilog.debug(DOMAIN, TAG, \`Get rawfile pixelMap path '${path}' successfully\`);
          return IMAGE\_SOURCE.createPixelMapSync();
        } catch (e) {
          hilog.error(DOMAIN, TAG, \`Get rawfile pixelMap path '${path}' failed, error: ${e}\`);
          throw e as Error;
        }
      }
    
      _// 闪控球启动逻辑_
      public static async onClickCreateFloatingBall(
        floatingBallController: floatingBall.FloatingBallController | undefined,
        template: floatingBall.FloatingBallTemplate,
        onActiveRowChange: (value: number) => void, _// 接收状态更新回调函数_
        title: string = 'title',
        content: string = 'content',
        backgroundColor: string = '#0ff77c',
        icon?: image.PixelMap): Promise<void> {
    
       _ // 注册 监听状态变化事件_
        floatingBallController?.on('stateChange',
          (state: floatingBall.FloatingBallState) => {
            hilog.debug(DOMAIN, TAG, \`FloatingBall stateCange: ${state}\`);
            if (state === floatingBall.FloatingBallState.STOPPED) {
              floatingBallController?.off('click')
              floatingBallController?.off('stateChange')
              floatingBallController = undefined;
           _   // 执行状态更新回调_
              onActiveRowChange?.(-1);
            }
          })
      _  // 最后启动闪控球_
        let startParams: floatingBall.FloatingBallParams = icon ? {
          template: template,
          title: title,
          content: content,
          backgroundColor: backgroundColor,
          icon: icon
        } : {
          template: template,
          title: title,
          content: content,
          backgroundColor: backgroundColor
        }
        try {
          floatingBallController?.startFloatingBall(startParams)
            .then(() => {
              hilog.debug(DOMAIN, TAG, \`succeed in starting FloatingBall\`);
            }).catch((err: BusinessError) => {
            hilog.error(DOMAIN, TAG, \`failed to start FloatingBall. code: ${err.code}, message: ${err.message}\`);
          })
        } catch (e) {
          console.error('startFloatingBall Error', e)
        }
      }
    
      _// 闪控球更新逻辑_
      public static onClickUpdateFloatingBall(
        floatingBallController: floatingBall.FloatingBallController | undefined,
        template: floatingBall.FloatingBallTemplate,
        title: string = 'newTitle',
        content: string = 'newContent',
        icon?: image.PixelMap): void {
      _  // 更新时给标题、内容 随机使用数字后缀_
        let random\_string: string = Math.floor(Math.random() \* 100).toString();
        let updateParams: floatingBall.FloatingBallParams = icon ? {
          template: template,
          title: title + random\_string,
          content: content + random\_string,
          backgroundColor: '#f6ea0a',
          icon: icon
        } : {
          template: template,
          title: title + random\_string,
          content: content + random\_string,
          backgroundColor: '#f6ea0a',
        }
        try {
          floatingBallController?.updateFloatingBall(updateParams).then(() => {
            hilog.debug(DOMAIN, TAG, \`Succeed in updating FloatingBall\`);
          }).catch((err: BusinessError) => {
            hilog.error(DOMAIN, TAG, \`failed to update FloatingBall. code: ${err.code}, message: ${err.message}\`);
          })
        } catch (e) {
          console.error('updateFloatingBall Error:', e)
        }
      }
    
     _ // 闪控球停止逻辑_
      public static onClickStopFloatingBall(floatingBallController: floatingBall.FloatingBallController | undefined): void {
        // stop 是异步流程，需要通过 stateChange 状态回调获取实际删除结果
        floatingBallController?.stopFloatingBall().then(() => {
          hilog.debug(DOMAIN, TAG, \`Succeed in stopping FloatingBall\`);
        }).catch((err: BusinessError) => {
          hilog.error(DOMAIN, TAG, \`failed to stop FloatingBall. code: ${err.code}, message: ${err.message}\`);
        })
      }
    }
    
*   ContextUtil.ets：公共类，定义全局可使用的context，在使用时直接获取使用。
    
    import { common } from '@kit.AbilityKit';
    import { window } from '@kit.ArkUI';
    
    export class ContextUtil {
      public static context: common.UIAbilityContext;
      public static win: window.Window;
    }
    
*   EntryAbility.ets：获取context并赋值给ContextUtil.context。
    
    onCreate(): void {
      try {
        this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR\_MODE\_NOT\_SET);
      } catch (err) {
        hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
      }
      ContextUtil.context = this.context;
      hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
    }
    
*   Index.ets：主页面，实现创建或关闭闪控球，并在闪控球[on('click')](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-floatingball#onclick)回调中使用screenshot.capture获取截图，应用退后台闪控球也始终在前台显示，点击闪控球可获取应用外截图。
    
    import hilog from '@ohos.hilog';
    import image from '@ohos.multimedia.image';
    import { floatingBall, screenshot } from '@kit.ArkUI';
    import { Utils } from '../util/Utils';
    import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
    
    const DOMAIN: number = 0xF811;
    const TAG: string = '\[Sample\_FloatingBall\]';
    
    const PERMISSION\_ARRAY: Permissions\[\] = \['ohos.permission.CUSTOM\_SCREEN\_CAPTURE'\];
    
    async function requestPermissionFromUser(context: Context): Promise<number\[\]> {
      const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
      const requestStatus = await atManager.requestPermissionsFromUser(context, PERMISSION\_ARRAY);
      return requestStatus?.authResults;
    }
    
    @Entry
    @Component
    struct Index {
     _ // 当前可用的行，-1 表示全部行可见_
      @State private activeRow: number = -1;
     _ // 声明闪控球控制器_
      private floatingBallController: floatingBall.FloatingBallController | undefined = undefined;
      _// 缓存 icon 图标_
      private cachedIcon: image.PixelMap | undefined = undefined;
      @State pixMap: image.PixelMap | undefined = undefined;
     _ // activeRow 的状态更新函数（确保闪控球销毁时，activeRow的值更新为-1）_
      private activeRowChange = (value: number) => {
        this.activeRow = value;
      };
    
      aboutToAppear(): void {
        requestPermissionFromUser(this.getUIContext().getHostContext() as Context);
      }
    
      build() {
        Column({ space: 12 }) {
        _  // 强调文本布局，支持标题、图标和内容_
          Row({ space: 6 }) {
            Button('Open FloatBall').onClick(async () => {
           _   // 请在组件内获取context，确保this.getUIContext().getHostContext()返回的结果是UIAbilityContext_
              if (!this.floatingBallController) {
                this.floatingBallController = await floatingBall.create({
                  context: this.getUIContext().getHostContext()
                });
              }
           _   // 须自行替换rawfile下闪控球图标！！！_
              if (this.floatingBallController) {
              _  // 仅当没有缓存 cachedIcon2 时才加载；有缓存时，直接使用；_
                if (!this.cachedIcon) {
                  let pixelMap = Utils.getRawfilePixelMapSync('video.png'); _// 图片尺寸有最大限制_
                  if (pixelMap) {
                    this.cachedIcon = pixelMap; _// 把图标缓存起了_
                    hilog.debug(DOMAIN, TAG, \`Success to load icon PixelMap\`);
                  } else {
                    hilog.debug(DOMAIN, TAG, \`Failed to load icon PixelMap\`);
                  }
                }
                Utils.onClickCreateFloatingBall(this.floatingBallController,
                  floatingBall.FloatingBallTemplate.EMPHATIC, this.activeRowChange, '16', 'Min', '#0ff77c', this.cachedIcon);
                this.activeRow = 2;
    
             _   // 在闪控球点击回调中进行截图_
                this.floatingBallController?.on('click', () => {
                  hilog.debug(DOMAIN, TAG, \`FloatingBall onClickEvent\`);
    
               _   // 获取闪控球windowId，用于截图时屏蔽闪控球_
                  let windowIds: Array<number> = \[\];
                  let captureOption: screenshot.CaptureOption = {
                    'displayId': 0,
                    'blackWindowIds': windowIds
                  };
    
                  this.floatingBallController?.getFloatingBallWindowInfo()
                    .then((data: floatingBall.FloatingBallWindowInfo) => {
                      windowIds.push(data.windowId);
    
                      try {
                     _   // 截屏操作_
                        let promise = screenshot.capture(captureOption);
                        promise.then((pixelMap: image.PixelMap) => {
                          this.pixMap = pixelMap;
                          console.info('Succeeded in saving screenshot. Pixel bytes number: ' +
                          pixelMap.getPixelBytesNumber());
                        }).catch((err: BusinessError) => {
                          console.error(\`Failed to save screenshot. Code: ${err.code}, message: ${err.message}\`);
                        });
                      } catch (exception) {
                        console.error(\`Failed to save screenshot. Code: ${exception.code}, message: ${exception.message}\`);
                      }
                      ;
                      console.info('Succeeded in getting floating ball window info. Info: ' + JSON.stringify(data));
                  }).catch((err: BusinessError) => {
                    console.error(\`Failed to get floating ball window info. Cause code: ${err.code}, message: ${err.message}\`);
                  });
    
                });
              }
            });
         _   // 关闭闪控球_
            Button('Close FloatBall').onClick(() => {
              Utils.onClickStopFloatingBall(this.floatingBallController);
              this.activeRow = -1; _// 关闭后恢复所有行显示_
            });
          }
          .width('100%')
          .justifyContent(FlexAlign.Center);
    
       _   // 截图回显_
          Image(this.pixMap)
            .width('80%')
            .height('80%');
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center);
      }
    }
    

#### 常见FAQ

Q：为什么申请ohos.permission.USE\_FLOAT\_BALL权限一直不通过？

A：闪控球设计的核心交互路径是跨应用的交互操作，并且回到应用内处理业务逻辑，比如使用闪控球获取其他应用截图，然后回到应用内处理截图数据，若不符合此场景，则申请权限会被驳回。