一、简介

组件截图是将应用内一个组件节点树的渲染结果生成位图(PixelMap)的能力,支持两种方式:一种是对已挂树显示的组件进行截图,另一种是对通过Builder或ComponentContent实现的离线组件进行截图。

说明
组件截图依赖UI上下文,需要在具备明确上下文的环境中调用,因此请优先使用UIContext的getComponentSnapshot接口返回的ComponentSnapshot对象的接口,不建议直接使用从@kit.ArkUI导入的componentSnapshot接口。

1.1 对挂树组件截图

对已明确挂树的组件进行截图,可通过get或getSync实现,传入组件标识(需提前通过.id通用属性配置)以指定组件根节点。系统在通过指定的ID查找待截图组件时,仅遍历已挂树的组件,不对cache或离屏组件进行查找。系统以首个查找到的结果为准,故应用需确保组件标识ID的唯一性。


从API version 15开始,在已知组件的getUniqueId的情况下,也可以使用getWithUniqueId或getSyncWithUniqueId接口来实现截图,这可以省去查找组件的过程。


截图仅能获取最近一帧的绘制内容。若在组件触发更新的同时调用截图,更新的渲染内容不会被截取,截图将返回前一帧的绘制内容。

说明
尽量避免在使用截图时触发待截图组件的刷新,防止对截图内容的干扰。

1.2 对离线组件截图

离线组件是指通过Builder或ComponentContent封装的、尚未挂载到树上的组件,可以使用createFromBuilder来实现。从API version 18开始,也可以使用createFromComponent实现离线组件截图。


这些组件不参与真实渲染,因此对其截图需要更长的时间,因为系统必须先进行离线构建、布局及资源加载等操作,在这些操作完成前执行的截图所获位图不符合预期。因此,通常需要通过设置delay参数指定足够的时间,确保系统能够完成这些操作。对于图片资源的加载,建议将图片组件的syncLoad属性设为 true,以强制同步加载,确保离线组件构建时图片已加载、下载及解码完成,从而确保截图过程中能够正确呈现图片像素。

二、保存截图

2.1 使用安全控件SaveButton实现截图保存到相册
// src/main/ets/pages/TestComponentSnapshot.ets
SaveButton({
  icon: SaveIconStyle.FULL_FILLED,
  text: SaveDescription.SAVE_IMAGE,
  buttonType: ButtonType.Capsule
})
  // ···
  .onClick((event, result) => {
    this.saveSnapshot(result);
  })
async saveSnapshot(result: SaveButtonOnClickResult): Promise<void> {
    if (!this.snapshotPixelMap) {
      console.log("没有图片可保存")
      return
    }
    try {
      if (result === SaveButtonOnClickResult.SUCCESS) {
        const helper = photoAccessHelper.getPhotoAccessHelper(this.context.getHostContext());
        const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png');
        const file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
        try {
          const imagePackerApi: image.ImagePacker = image.createImagePacker();
          const packOpts: image.PackingOption = {
            format: 'image/png',
            quality: 100,
          };
          const data = await imagePackerApi.packToData(this.snapshotPixelMap, packOpts);
          fileIo.writeSync(file.fd, data);
          console.info(`图片保存成功`);
          this.getUIContext().getPromptAction().showToast({
            message: '图片保存成功',
            duration: 1800
          })
        } catch (error) {
          let businessError = error as BusinessError;
          console.error(
            `图片保存失败. Error code is ${businessError.code}, message is ${businessError.message}`);
        } finally {
          try {
            fileIo.closeSync(file.fd);
          } catch (err) {
            let error = err as BusinessError;
            console.error(`io关闭失败. Error code is ${error.code}, message is ${error.message}`);
          }
          this.snapshotPixelMap?.release().then(() => {
            this.snapshotPixelMap = undefined
          })
        }
      }
    } catch (err) {
      let error = err as BusinessError;
      console.error(`图片保存失败 异常信息 err, errCode: ${error.code}, error message: ${error.message}`);
    }
  }
2.2 使用弹窗授权保存媒体库资源
/* 保存媒体库资源
 *
 */
  static async saveIamgeToPicture(
    srcFileUri: string,
    context: common.UIAbilityContext
  ): Promise<string> {
    console.log("调用了 saveIamgeToPicture")
    let phAccessHelper: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
    let desFile: fileIo.File | null = null;
    let srcFile: fileIo.File | null = null;
    try {
      // 指定要保存到应用程序沙盒目录中的图片的URI。
      let srcFileUris: string[] = [
        srcFileUri
      ];
      const timeStamp = Date.now();
      const fileName = "IMG_" + timeStamp
      // 设置要保存的图片的参数:文件扩展名、图片类型、标题和子类型(后两者为可选)
      let photoCreationConfigs: photoAccessHelper.PhotoCreationConfig[] = [
        {
          title: fileName, // This parameter is optional.
          fileNameExtension: 'png',
          photoType: photoAccessHelper.PhotoType.IMAGE,
          subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
        }
      ];

      console.info('沙箱 URI: ' + srcFileUri);
      // 基于弹窗授权获取媒体库中的目标URI
      let desFileUris: string[] =
        await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);
      console.info('获取媒体库中的目标 URIs: ' + JSON.stringify(desFileUris));
      // 将图片从沙盒目录写入媒体库中的目标URI。
      desFile = await fileIo.open(desFileUris[0], fileIo.OpenMode.WRITE_ONLY);
      srcFile = await fileIo.open(srcFileUri, fileIo.OpenMode.READ_ONLY);
      await fileIo.copyFile(srcFile.fd, desFile.fd);
      console.info('保存图片成功');
      return '保存图片成功';
    } catch (err) {
      console.error(`保存图片失败 errCode is: ${err.code}, ${err.message}`);
      return `保存图片失败 errCode is: ${err.code}, ${err.message}`;
    } finally {
      if (srcFile) {
        fileIo.closeSync(srcFile);
        FileUtil.deleteFile(srcFileUri)
      }
      if (desFile) {
        fileIo.closeSync(desFile);
      }
    }
  }
/**
   * pixelMap 生成图片 保存到应用沙箱里
   * @param uiContext
   * @param pixelMap
   * @param fileName
   */
  static async savaPixelMap(uiContext: common.UIAbilityContext, pixelMap: image.PixelMap,
    fileName?: string): Promise<string | null> {
    const packer = image.createImagePacker();
    const packOpts: image.PackingOption = { format: 'image/png', quality: 100 };
    try {
      const arrayBuffer = await packer.packToData(pixelMap, packOpts);

      if (!fileName) {
        const timeStamp = Date.now();
        // const random = Math.random()
        fileName = "IMG_" + timeStamp+".png"
      }

      let filePath = `${uiContext.tempDir}/${fileName}`;
      const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
      fileIo.writeSync(file.fd, arrayBuffer);
      fileIo.closeSync(file);
      console.log("pixelMap 生成图片 沙箱路径:", filePath)
      return filePath
    } catch (error) {
      console.error(`pixelMap 生成图片 保存到应用沙箱里 失败, error: ${JSON.stringify(error)}`);
    }
    return null
  }

三、实践

示例代码

FileUtil.ets

import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class FileUtil {
  /**
   *
   * @param fileUri 文件的应用沙箱路径
   */
  static deleteFile(fileUri?: string) {
    if (!fileUri) {
      console.log(`沙箱路径 fileUri 参数值 ${fileUri} 不正确`)
      return
    }
    fileIo.access(fileUri, fileIo.AccessModeType.EXIST).then((accessRes: boolean) => {
      if (accessRes) {
        fileIo.unlink(fileUri, (err: BusinessError) => {
          if (err) {
            console.error(`文件:${fileUri} , 删除 失败. Code: ${err.code}, message: ${err.message}`);
          } else {
            console.info(`文件:${fileUri} , 删除 成功`);
          }
        });
      } else {
        console.log(`文件 ${fileUri} 不存在`)
      }
    }).catch((err: BusinessError) => {
      console.error(
        `deleteFile 删除文件 Failed to access. Code: ${err.code}, message: ${err.message} , fileUri: ${fileUri}`);
    });
  }
}

PhotoViewPickerUtil.ets

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { dataSharePredicates } from '@kit.ArkData';
import util from '@ohos.util';
import { image } from '@kit.ImageKit';
import { UpLoadFileData } from '../model/UpLoadFileData';
import { FileUtil } from './FileUtil';


export class PhotoViewPickerUtil {
	/**
   * pixelMap 生成图片 保存到应用沙箱里
   * @param uiContext
   * @param pixelMap
   * @param fileName
   */
  static async savaPixelMap(uiContext: common.UIAbilityContext, pixelMap: image.PixelMap,
    fileName?: string): Promise<string | null> {
    const packer = image.createImagePacker();
    const packOpts: image.PackingOption = { format: 'image/png', quality: 100 };
    try {
      const arrayBuffer = await packer.packToData(pixelMap, packOpts);

      if (!fileName) {
        const timeStamp = Date.now();
        // const random = Math.random()
        fileName = "IMG_" + timeStamp+".png"
      }

      let filePath = `${uiContext.tempDir}/${fileName}`;
      const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
      fileIo.writeSync(file.fd, arrayBuffer);
      fileIo.closeSync(file);
      console.log("pixelMap 生成图片 沙箱路径:", filePath)
      return filePath
    } catch (error) {
      console.error(`pixelMap 生成图片 保存到应用沙箱里 失败, error: ${JSON.stringify(error)}`);
    }
    return null
  }

 
  static async saveIamgeToPicture(
    srcFileUri: string,
    context: common.UIAbilityContext
  ): Promise<string> {
    console.log("调用了 saveIamgeToPicture")
    let phAccessHelper: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
    let desFile: fileIo.File | null = null;
    let srcFile: fileIo.File | null = null;
    try {
      // 指定要保存到应用程序沙盒目录中的图片的URI。
      let srcFileUris: string[] = [
        srcFileUri
      ];
      const timeStamp = Date.now();
      const fileName = "IMG_" + timeStamp
      // 设置要保存的图片的参数:文件扩展名、图片类型、标题和子类型(后两者为可选)
      let photoCreationConfigs: photoAccessHelper.PhotoCreationConfig[] = [
        {
          title: fileName, // This parameter is optional.
          fileNameExtension: 'png',
          photoType: photoAccessHelper.PhotoType.IMAGE,
          subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
        }
      ];

      console.info('沙箱 URI: ' + srcFileUri);
      // 基于弹窗授权获取媒体库中的目标URI
      let desFileUris: string[] =
        await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);
      console.info('获取媒体库中的目标 URIs: ' + JSON.stringify(desFileUris));
      // 将图片从沙盒目录写入媒体库中的目标URI。
      desFile = await fileIo.open(desFileUris[0], fileIo.OpenMode.WRITE_ONLY);
      srcFile = await fileIo.open(srcFileUri, fileIo.OpenMode.READ_ONLY);
      await fileIo.copyFile(srcFile.fd, desFile.fd);
      console.info('保存图片成功');
      return '保存图片成功';
    } catch (err) {
      console.error(`保存图片失败 errCode is: ${err.code}, ${err.message}`);
      return `保存图片失败 errCode is: ${err.code}, ${err.message}`;
    } finally {
      if (srcFile) {
        fileIo.closeSync(srcFile);
        FileUtil.deleteFile(srcFileUri)
      }
      if (desFile) {
        fileIo.closeSync(desFile);
      }
    }
  }
}

TestComponentSnapshot.ets

import { image } from '@kit.ImageKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';
import fileIo from '@ohos.file.fs';
import { PhotoViewPickerUtil } from '../../common/utils/PhotoViewPickerUtil';

const PAGE_ID = "TestComponentSnapshot"

@Entry
@ComponentV2
struct TestComponentSnapshot {
  @Local message: string = '使用组件截图(ComponentSnapshot)';
  @Local snapshotPixelMap: image.PixelMap | undefined = undefined;
  @Local context: UIContext = this.getUIContext();
  private uIAbilityContext = this.context.getHostContext() as common.UIAbilityContext

  async saveSnapshot(result: SaveButtonOnClickResult): Promise<void> {
    if (!this.snapshotPixelMap) {
      console.log("没有图片可保存")
      return
    }
    try {
      if (result === SaveButtonOnClickResult.SUCCESS) {
        const helper = photoAccessHelper.getPhotoAccessHelper(this.context.getHostContext());
         // createAsset需要 ohos.permission.WRITE_IMAGEVIDEO
        //在未申请相册管理模块权限'ohos.permission.WRITE_IMAGEVIDEO'时,可以使用安全控件或授权弹窗的方式创建媒体资源,详情请参考保存媒体库资源。
        const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png');
        const file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
        try {
          const imagePackerApi: image.ImagePacker = image.createImagePacker();
          const packOpts: image.PackingOption = {
            format: 'image/png',
            quality: 100,
          };
          const data = await imagePackerApi.packToData(this.snapshotPixelMap, packOpts);
          fileIo.writeSync(file.fd, data);
          console.info(`图片保存成功`);
          this.getUIContext().getPromptAction().showToast({
            message: '图片保存成功',
            duration: 1800
          })
        } catch (error) {
          let businessError = error as BusinessError;
          console.error(
            `图片保存失败. Error code is ${businessError.code}, message is ${businessError.message}`);
        } finally {
          try {
            fileIo.closeSync(file.fd);
          } catch (err) {
            let error = err as BusinessError;
            console.error(`io关闭失败. Error code is ${error.code}, message is ${error.message}`);
          }
          //保存完成后释放位图,当位图对象不再使用时,应及时将其赋值为空,
          this.snapshotPixelMap?.release().then(() => {
            this.snapshotPixelMap = undefined
          })
        }
      }
    } catch (err) {
      let error = err as BusinessError;
      console.error(`图片保存失败 异常信息 err, errCode: ${error.code}, error message: ${error.message}`);
    }
  }

  build() {
    Column({ space: 20 }) {
      Text(this.message)
        .id('TestComponentSnapshotHelloWorld')
        .fontSize($r('app.float.text_size_20fp'))
        .margin({ top: 40 })

      Button("截图").onClick(() => {
        if (this.snapshotPixelMap) {
          //截图前先释放之前的截图
          this.snapshotPixelMap.release().then(() => {
            this.snapshotPixelMap = undefined
          })
        }

        this.getUIContext().getComponentSnapshot().get(PAGE_ID).then((pixelMap: image.PixelMap) =>{
          this.snapshotPixelMap = pixelMap
          console.log("截图成功")
        })
      })

      Button("使用弹窗授权保存媒体库资源").onClick(() => {
        if (this.snapshotPixelMap) {
          PhotoViewPickerUtil.savaPixelMap(this.uIAbilityContext, this.snapshotPixelMap).then((fileUri) => {
            if (fileUri) {
              PhotoViewPickerUtil.saveIamgeToPicture(fileUri, this.uIAbilityContext).then((res) => {
                console.log("使用使用弹窗授权保存媒体库资源 结果:", res)
                //截图前先释放之前的截图
                this.snapshotPixelMap!.release().then(() => {
                  this.snapshotPixelMap = undefined
                })
              })
            }
          })

        }
      })

      //使用安全控件SaveButton实现截图保存到相册。
      SaveButton({
        icon: SaveIconStyle.FULL_FILLED,
        text: SaveDescription.SAVE_IMAGE,
        buttonType: ButtonType.Capsule
      })
        .onClick((event, result) => {
          this.saveSnapshot(result);
        })

      if (this.snapshotPixelMap) {
        Text("下面是截图")
        Image(this.snapshotPixelMap)
          .width("80%")
          .height(300)
          .objectFit(ImageFit.Contain)
      }
    }
    .id(PAGE_ID)
    .height('100%')
    .width('100%')
  }
}
Logo

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

更多推荐