HarmonyOS:使用组件截图(ComponentSnapshot)并保存到相册
一、简介
组件截图是将应用内一个组件节点树的渲染结果生成位图(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%')
}
}
更多推荐


所有评论(0)