场景描述:

众多应用期望在系统截屏后实现应用内截图功能。在实现应用内截图时,由于系统截屏后不能直接返回截图信息,且应用无法直接读取相册的截图数据,从而使开发者在实现应用内截图时遇到一系列问题,主要包括以下几个场景:

  1. 触发系统截图时,应用内同步截图并进行二次处理(提示反馈、分析等):
    1. 用户截屏后,app 内展示截屏内容浮窗,作为问题反馈的入口,点击后可跳转对应页面,并且把这个图片显示在反馈页面上。
    2. 将应用内某页面的截图内容分享给微信。
    3. 社交类应用,识别到系统截屏后,在聊天页面弹出"可能想发送的图片",展示相册最新截图。
  2. 地图类应用,点击应用内截图按钮,截取组件内所有内容(包括隐藏在可见区外的)。
  3. 购物类应用,商品详情页截图后,生成商品详情+商品二维码的页面展示,可以保存至系统相册,以便分享至其他应用。

方案:

  • 触发系统截图时,应用内同步截图并进行二次处理(提示反馈、分析等)

在系统截图后,应用无法通过监听系统截图回调获取截图信息,应用可以重新获取当前窗口的屏幕截图,将其传递完成分享。

核心代码

  1. 通过on('screenshot')监听系统截屏并展示应用内截图页面:
    aboutToAppear(): void {
      try { //监听系统截图时间,打开截屏页面     
        this.windowStage_.getMainWindowSync().on('screenshot', () => {
          this.globalCustomDialog()
        });
      } catch (exception) {
        console.error(`Failed to register callback. Cause code: ${exception.code}, message: ${exception.message}`);
      }
    }

    截图页面使用全局自定义弹窗实现,通过配置offset控制浮窗位置,isModal或者autoCancel可以解决点击浮窗以外的区域消失的情况,具体使用可参考openCustomDialog

    globalCustomDialog = () => {
      PromptActionClass.setContext(this.getUIContext());
      PromptActionClass.setOptions({
        alignment: DialogAlignment.Center,
        offset: { dx: 0, dy: 0 },
        autoCancel: false,
        isModal: true
      });
      PromptActionClass.openDialog('全局弹窗')
    }
    static openDialog(message: string) {
      PromptActionClass.contentNode = new ComponentContent(PromptActionClass.ctx, wrapBuilder(buildText), new Params());
      if (PromptActionClass.contentNode !== null) {
        PromptActionClass.ctx.getPromptAction()
          .openCustomDialog(PromptActionClass.contentNode, PromptActionClass.options)
          .then(() => {
            console.log(TAG, 'OpenCustomDialog complete.')
          })
          .catch((error: BusinessError) => {
            let message = (error as BusinessError).message;
            let code = (error as BusinessError).code;
            console.log(TAG, `OpenCustomDialog args error code is ${code}, message is ${message}`);
          })
      }
    }
  2. 获取当前屏幕截图,进行数据处理:

    在截图页面加载时获取当前屏幕截图信息,这里需要对pixelMap进行压缩处理为uri或者base64。这里的示例是将pixelMap处理为base64传递:

    aboutToAppear(): void {
      // 监听窗口截图事件,获取截图信息    
      this.windowStage_.getMainWindowSync()
        .snapshot((err: BusinessError, pixelMap: image.PixelMap) => {
          // 转换成base64       
          const imagePackerApi: image.ImagePacker = image.createImagePacker();
          let packOpts: image.PackingOption = { format: 'image/jpeg', quality: 100 };
          imagePackerApi.packing(pixelMap, packOpts).then((data: ArrayBuffer) => {
            let buf: buffer.Buffer = buffer.from(data);
            this.uri = 'data:image/jpeg;base64,' + buf.toString('base64', 0, buf.length);
          })
          const errCode: number = err.code;
          if (errCode) {
            console.error(`Failed to snapshot window. Cause code: ${err.code}, message: ${err.message}`);
            return;
          }
          console.info('Succeeded in snapshotting window. Pixel bytes number: ' + pixelMap.getPixelBytesNumber());
          // PixelMap使用完后及时释放内存
          pixelMap.release();
        });
    }
  3. 点击分享按钮,进行数据传递:

    对于应用内分享可以直接通过Router或者Navigation传递数据,对于应用之间的分享可参考应用间跳转通过want信息进行传递。

    Button("问题反馈").onClick(() => {
      PromptActionClass.closeDialog()
      router.pushUrl({ url: "pages/fankui", params: { uri: this.uri } })
    })
  4. 接收方的处理:
    • 接收方通过router.getParam或者this.pageStack.getParamByName("页面名称")接收图片信息,并进行页面展示,对于应用间的分享,可以在onnewant回调中通过接收want参数获取,以下示例通过router.getParam接收参数。
      @State url: string = (router.getParams() as Record<string, string>)['uri'];
      Image(this.url).width(200).height(300).objectFit(ImageFit.Contain)
    • 若应用有诉求实现“可能想发送的功能”这种效果,可以在目标应用中使用RecentPhoto组件。页面加载时对组件的配置项进行初始化,而后通过onRecentPhotoClick回调获取图片文件uri进行消息发送。
      // popup构造器定义弹框内容     
      @Builder
      popupBuilder() {
        Row({ space: 2 }) {
          RecentPhotoComponent({
            recentPhotoOptions: this.recentPhotoOptions,
            onRecentPhotoCheckResult: this.recentPhotoCheckResultCallback,
            onRecentPhotoClick: this.recentPhotoClickCallback,
            onRecentPhotoCheckInfo: this.recentPhotoCheckInfoCallback,
          }).height(170).width(100)
        }.width(120).height(180).padding(5)
      }
      onPageShow() {
        // 设置数据类型, IMAGE_VIDEO_TYPE:图片和视频(默认值)  IMAGE_TYPE:图片   VIDEO_TYPE:视频  MOVING_PHOTO_IMAGE_TYPE 动态图片
        this.recentPhotoOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
        // 设置最近图片的时间范围,单位(秒), 0表示所有时间。
        this.recentPhotoOptions.period = 30;
        // 设置资源的来源 ALL:所有 CAMERA:相机  SCREENSHOT:截图
        this.recentPhotoOptions.photoSource = PhotoSource.SCREENSHOT;
      }
      private onRecentPhotoClick(recentPhotoInfo: BaseItemInfo): boolean { 
        // 照片或视频返回         
        if (recentPhotoInfo) {
          console.info('The photo uri is ' + recentPhotoInfo.uri);
          this.url = recentPhotoInfo.uri
          this.customPopup = false
          return true;
        }
        return true;
      }

效果展示:

  • 点击应用内截图按钮,截取组件内所有内容(包括隐藏在可见区外的)

通过组件截图componentSnapshot.createFromBuilder获取离屏组件绘制区域信息,将其存入缓存,使用全局自定义弹窗展示缓存数据。基于webview实现长截图参考:Web组件网页长截图

核心代码

使用componentSnapshot.createFromBuilder获取组件绘制区,存入缓存并弹出模态框,注意可以通过配置ImageFit.Contain使得图片完全显示在显示边界内:

build() {
  Column() {
    Column() {
      this.RandomBuilder()
    }
    .height(50)
    .margin(10)
    Button("截取超出屏幕范围的组件截图").onClick(() => {
      if (this.dialogController != null) {
        // 建议使用this.getUIContext().getComponentSnapshot().createFromBuilder()          
        componentSnapshot.createFromBuilder(() => {
          this.RandomBuilder()
        }, (error: Error, pixmap: image.PixelMap) => {
          if (error) {
            console.log("error: " + JSON.stringify(error))
            return;
          }
          AppStorage.setOrCreate("pixmap", pixmap)
          this.dialogController?.open()
          let info = this.getUIContext().getComponentUtils().getRectangleById("builder")
          console.log(info.size.width + ' ' + info.size.height + ' ' + info.localOffset.x + ' ' + info.localOffset.y +
            ' ' + info.windowOffset.x + ' ' + info.windowOffset.y)
        }, 320, true, { scale: 3, waitUntilRenderFinished: true })
      }
    })
  }
  .width('100%')
  .margin({ left: 10, top: 5, bottom: 5 })
  .height(300)
}

构建组件截图Builder,并进行组件标识:

@Builder
RandomBuilder() {
  Scroll(this.scroller) {
    Column() {
      ForEach(this.arr, (item: string) => {
        Column() {
          Text(item)
        }
      })
    }
  }.id("builder")
}

截图弹窗展示缓存数据:

@CustomDialog
struct CustomDialogExample {
  @State pixmap: image.PixelMap | undefined = AppStorage.get("pixmap")
  controller?: CustomDialogController
  cancel: () => void = () => {
  }
  confirm: () => void = () => {
  }

  build() {
    Column() {
      Image(this.pixmap)
        .margin(10)
        .height(200)
        .width(200)
        .border({ color: Color.Black, width: 2 })
        .objectFit(ImageFit.Contain)
      Button('点我关闭弹窗').onClick(() => {
        if (this.controller != undefined) {
          this.controller.close()
        }
      }).margin(20)
    }
  }
}

效果展示:

  • 拼接截图,并将其保存至相册
  1. 截图页面与二维码QRCode拼接为一个新的组件,设置组件标志。
  2. 通过componentSnapshot.get()获取已加载的组件的截图,传入组件的组件标识,通过回调将图片压缩打包,将PixelMap转为arraybuffer(如以下示例),也可以将图片写入沙箱,转换为uri。
  3. 设置安全控件属性saveButton,这里需要遵循安全控件的规范,设置不当(建议宽高不要设置100%,容易出现组件失效)SaveButtonOnClickResult将会返回FAILED。
  4. 使用addResourcePhotoAccessHelper.applyChanges添加资源内容,提交媒体变更请求。

核心代码

Column() {
  Image(this.uri).width(120).height(150).objectFit(ImageFit.Auto)
  Row() {
    Text("二维码区域")
    QRCode(this.value).width(30).height(30)
  }.width(120).height(50)
}.margin(5).backgroundColor(Color.White).id("root1")
// 安全控件按钮属性
saveButtonOptions: SaveButtonOptions = { icon: SaveIconStyle.FULL_FILLED, text: SaveDescription.SAVE_IMAGE, buttonType: ButtonType.Capsule }
SaveButton(this.saveButtonOptions).onClick(async (event, result: SaveButtonOnClickResult) => {
  if (result == SaveButtonOnClickResult.SUCCESS) {
    try {
      //获取组件截图信息               
      componentSnapshot.get("root1", (error: Error, pixmap: image.PixelMap) => {
        if (error) {
          console.log("error: " + JSON.stringify(error))
          return;
        }
        // 创建ImagePacker 实例                 
        const imagePackerApi: image.ImagePacker = image.createImagePacker();
        let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 }
        // 重新压缩图片                 
        imagePackerApi.packing(this.pixmap, packOpts).then(async (buffer: ArrayBuffer) => {
          try {
            const context = getContext(this) // 获取相册管理实例                    
            let helper = photoAccessHelper.getPhotoAccessHelper(context)
            try {
              // 配置待创建的文件类型和扩展名     
              // 类型  
              let photoType: photoAccessHelper.PhotoType = photoAccessHelper.PhotoType.IMAGE;
              //扩展名                    
              let extension: string = 'jpg';
              // 创建资产变更请求                       
              let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest =
                photoAccessHelper.MediaAssetChangeRequest.createAssetRequest(context, photoType, extension);
              assetChangeRequest.addResource(photoAccessHelper.ResourceType.IMAGE_RESOURCE,
                buffer);
              // 提交媒体变更请求               
              await helper.applyChanges(assetChangeRequest);
              console.info('addResourceByArrayBuffer successfully');
            } catch (err) {
              console.error(`addResourceByArrayBufferDemo failed with error: ${err.code}, ${err.message}`);
            }
            promptAction.showToast({ message: `截图保存成功` })
          } catch (error) {
            console.error("error is " + JSON.stringify(error))
          }
        })
      }, { scale: 2, waitUntilRenderFinished: true })
    } catch (err) {
      console.error(`create asset failed with error: ${err.code}, ${err.message}`);
    }
  } else {
    console.error('SaveButtonOnClickResult create asset failed');
  }
})

 

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐