应用内截图的实现方案
·
场景描述:
众多应用期望在系统截屏后实现应用内截图功能。在实现应用内截图时,由于系统截屏后不能直接返回截图信息,且应用无法直接读取相册的截图数据,从而使开发者在实现应用内截图时遇到一系列问题,主要包括以下几个场景:
- 触发系统截图时,应用内同步截图并进行二次处理(提示反馈、分析等):
- 用户截屏后,app 内展示截屏内容浮窗,作为问题反馈的入口,点击后可跳转对应页面,并且把这个图片显示在反馈页面上。
- 将应用内某页面的截图内容分享给微信。
- 社交类应用,识别到系统截屏后,在聊天页面弹出"可能想发送的图片",展示相册最新截图。
- 地图类应用,点击应用内截图按钮,截取组件内所有内容(包括隐藏在可见区外的)。
- 购物类应用,商品详情页截图后,生成商品详情+商品二维码的页面展示,可以保存至系统相册,以便分享至其他应用。
方案:
- 触发系统截图时,应用内同步截图并进行二次处理(提示反馈、分析等)。
在系统截图后,应用无法通过监听系统截图回调获取截图信息,应用可以重新获取当前窗口的屏幕截图,将其传递完成分享。
核心代码
- 通过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}`); }) } }
- 获取当前屏幕截图,进行数据处理:
在截图页面加载时获取当前屏幕截图信息,这里需要对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(); }); }
- 点击分享按钮,进行数据传递:
对于应用内分享可以直接通过Router或者Navigation传递数据,对于应用之间的分享可参考应用间跳转通过want信息进行传递。
Button("问题反馈").onClick(() => { PromptActionClass.closeDialog() router.pushUrl({ url: "pages/fankui", params: { uri: this.uri } }) })
- 接收方的处理:
- 接收方通过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; }
- 接收方通过router.getParam或者this.pageStack.getParamByName("页面名称")接收图片信息,并进行页面展示,对于应用间的分享,可以在onnewant回调中通过接收want参数获取,以下示例通过router.getParam接收参数。
效果展示:
- 点击应用内截图按钮,截取组件内所有内容(包括隐藏在可见区外的)
通过组件截图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)
}
}
}
效果展示:
- 拼接截图,并将其保存至相册
- 截图页面与二维码QRCode拼接为一个新的组件,设置组件标志。
- 通过componentSnapshot.get()获取已加载的组件的截图,传入组件的组件标识,通过回调将图片压缩打包,将PixelMap转为arraybuffer(如以下示例),也可以将图片写入沙箱,转换为uri。
- 设置安全控件属性saveButton,这里需要遵循安全控件的规范,设置不当(建议宽高不要设置100%,容易出现组件失效)SaveButtonOnClickResult将会返回FAILED。
- 使用addResource和PhotoAccessHelper.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');
}
})
更多推荐
所有评论(0)