1# 使用组件截图(ComponentSnapshot) 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @jiangtao92--> 5<!--Designer: @piggyguy--> 6<!--Tester: @songyanhong--> 7<!--Adviser: @HelloCrease--> 8## 能力介绍 9组件截图是将应用内一个组件节点树的渲染结果生成位图([PixelMap](../reference/apis-image-kit/arkts-apis-image-PixelMap.md))的能力,支持两种方式:一种是对已挂树显示的组件进行截图,另一种是对通过Builder或ComponentContent实现的离线组件进行截图。 10 11> **说明:** 12> 13> 组件截图依赖UI上下文,需要在具备明确上下文的环境中调用,因此请优先使用UIContext的getComponentSnapshot接口返回的[ComponentSnapshot](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md)对象的接口,不建议直接使用从`@kit.ArkUI`导入的`componentSnapshot`接口。 14 15 16### 对挂树组件截图 17对已明确挂树的组件进行截图,可通过[get](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#get12-1)或[getSync](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#getsync12)实现,传入组件标识(需提前通过.id通用属性配置)以指定组件根节点。系统在通过指定的ID查找待截图组件时,仅遍历已挂树的组件,不对cache或离屏组件进行查找。系统以首个查找到的结果为准,故应用需**确保组件标识ID的唯一性**。 18 19在已知组件的[getUniqueId](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuniqueid12)的情况下,也可以使用[getWithUniqueId](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#getwithuniqueid15)或[getSyncWithUniqueId](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#getsyncwithuniqueid15)接口来实现截图,这可以省去查找组件的过程。 20 21截图仅能获取最近一帧的绘制内容。若在组件触发更新的同时调用截图,更新的渲染内容不会被截取,截图将返回前一帧的绘制内容。 22 23> **说明:** 24> 25> 尽量避免在使用截图时触发待截图组件的刷新,防止对截图内容的干扰。 26 27 28### 对离线组件截图 29离线组件是指通过Builder或ComponentContent封装的、尚未挂载到树上的组件,可以使用[createFromBuilder](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#createfrombuilder12-1)和[createFromComponent](../reference/apis-arkui/arkts-apis-uicontext-componentsnapshot.md#createfromcomponent18)来实现。 30 31这些组件不参与真实渲染,因此对其截图需要更长的时间,因为系统必须先进行离线构建、布局及资源加载等操作,在这些操作完成前执行的截图所获位图不符合预期。因此,通常需要通过设置delay参数指定足够的时间,确保系统能够完成这些操作。对于图片资源的加载,建议将图片组件的[syncLoad](../reference/apis-arkui/arkui-ts/ts-basic-components-image.md#syncload8)属性设为 true,以强制同步加载,确保离线组件构建时图片已加载、下载及解码完成,从而确保截图过程中能够正确呈现图片像素。 32 33## 典型使用场景 34以下通过几个典型场景来说明组件截图能力的常见使用方式。 35 36### 截取长内容(滚动截图) 37较长内容通常使用滚动类容器组件实现。截图时,仅能捕获容器内可见内容,超出边界部分无法截取。若使用LazyForEach或Repeat,超出显示范围内容亦不会被系统构建及截取。 38 39可利用滚动类容器接口,模拟用户滑动逐页截图,之后按偏移量拼接各页PixelMap位图,以生成完整长图。关键点在于模拟滑动、维护位移与位图关系及实现PixelMap位图读写。 40 41**步骤1:添加滚动控制器及事件监听** 42 43为了能够模拟滚动,以及监听组件滚动的具体offset,需要为List(此处以列表为例)组件添加滚动控制器以及滚动监听。 44 45```ts 46// src/main/ets/view/ScrollSnapshot.ets 47@Component 48export struct ScrollSnapshot { 49 private scroller: Scroller = new Scroller(); 50 private listComponentWidth: number = 0; 51 private listComponentHeight: number = 0; 52 // list组件的当前偏移量 53 private curYOffset: number = 0; 54 // 每次滚动距离 55 private scrollHeight: number = 0; 56 57 58 // ... 59 build() { 60 // ... 61 Stack() { 62 // ... 63 // 1.1 绑定滚动控制器,并通过`.id`配置组件唯一标识。 64 List({ 65 scroller: this.scroller 66 })// ... 67 .id(LIST_ID) 68 // 1.2 通过回调获取滚动偏移量。 69 .onDidScroll(() => { 70 this.curYOffset = this.scroller.currentOffset().yOffset; 71 }) 72 .onAreaChange((oldValue, newValue) => { 73 // 1.3 获取组件的宽高。 74 this.listComponentWidth = newValue.width as number; 75 this.listComponentHeight = newValue.height as number; 76 }) 77 } 78 } 79} 80``` 81 82**步骤2:循环滚动截图并缓存** 83 84通过实现一个递归方法滚动循环截图,并在滚动过程配合一些动效实现。 85 86```ts 87 /** 88 * 递归滚动截图,直到滚动到底,最后合并所有截图 89 */ 90 async scrollSnapAndMerge() { 91 // 记录滚动偏移 92 this.scrollYOffsets.push(this.curYOffset - this.yOffsetBefore); 93 // 调用组件截图接口,获取list组件的截图 94 const pixelMap = await this.getUIContext().getComponentSnapshot().get(LIST_ID); 95 // 获取位图像素字节,并保存在数组中 96 let area: image.PositionArea = 97 await this.getSnapshotArea(pixelMap, this.scrollYOffsets, this.listComponentWidth, this.listComponentHeight) 98 this.areaArray.push(area); 99 100 // 判断是否滚动到底以及用户是否已经强制停止 101 if (!this.scroller.isAtEnd() && !this.isClickStop) { 102 // 如果没有到底或被停止,则播放一个滚动动效,延迟一段时间后,继续递归截图 103 CommonUtils.scrollAnimation(this.scroller, 1000, this.scrollHeight); 104 await CommonUtils.sleep(1500); 105 await this.scrollSnapAndMerge(); 106 } else { 107 // 当滚动到底时,调用`mergeImage`将所有保存的位图数据进行拼接,返回长截图位图对象 108 this.mergedImage = 109 await this.mergeImage(this.areaArray, this.scrollYOffsets[this.scrollYOffsets.length - 1], 110 this.listComponentWidth, this.listComponentHeight); 111 } 112 } 113 114// src/main/ets/common/CommonUtils.ets 115static scrollAnimation(scroller: Scroller, duration: number, scrollHeight: number): void { 116 scroller.scrollTo({ 117 xOffset: 0, 118 yOffset: (scroller.currentOffset().yOffset + scrollHeight), 119 animation: { 120 duration: duration, 121 curve: Curve.Smooth, 122 canOverScroll: false 123 } 124 }); 125} 126``` 127 128**步骤3:拼接长截图** 129 130使用image.createPixelMapSync()方法创建长截图longPixelMap,并遍历之前保存的图像片段数据(this.areaArray),构建image.PositionArea对象area,然后调用longPixelMap.writePixelsSync(area)方法将这些片段逐个写入到正确的位置,从而拼接成一个完整的长截图。 131 132```ts 133async mergeImage(areaArray: image.PositionArea[], lastOffsetY: number, listWidth: number, 134 listHeight: number): Promise<PixelMap> { 135 // 创建一个长截图位图对象 136 let opts: image.InitializationOptions = { 137 editable: true, 138 pixelFormat: 4, 139 size: { 140 width: this.getUIContext().vp2px(listWidth), 141 height: this.getUIContext().vp2px(lastOffsetY + listHeight) 142 } 143 }; 144 let longPixelMap = image.createPixelMapSync(opts); 145 let imgPosition: number = 0; 146 147 148 for (let i = 0; i < areaArray.length; i++) { 149 let readArea = areaArray[i]; 150 let area: image.PositionArea = { 151 pixels: readArea.pixels, 152 offset: 0, 153 stride: readArea.stride, 154 region: { 155 size: { 156 width: readArea.region.size.width, 157 height: readArea.region.size.height 158 }, 159 x: 0, 160 y: imgPosition 161 } 162 } 163 imgPosition += readArea.region.size.height; 164 longPixelMap.writePixelsSync(area); 165 } 166 return longPixelMap; 167} 168``` 169 170**步骤4:保存截图** 171 172使用安全控件SaveButton实现截图保存到相册。 173 174```ts 175// src/main/ets/view/SnapshotPreview.ets 176SaveButton({ 177 icon: SaveIconStyle.FULL_FILLED, 178 text: SaveDescription.SAVE_IMAGE, 179 buttonType: ButtonType.Capsule 180}) 181 .onClick((event, result) => { 182 this.saveSnapshot(result); 183 }) 184 185async saveSnapshot(result: SaveButtonOnClickResult): Promise<void> { 186 if (result === SaveButtonOnClickResult.SUCCESS) { 187 const helper = photoAccessHelper.getPhotoAccessHelper(this.context); 188 const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png'); 189 const file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); 190 const imagePackerApi: image.ImagePacker = image.createImagePacker(); 191 const packOpts: image.PackingOption = { 192 format: 'image/png', 193 quality: 100, 194 }; 195 imagePackerApi.packing(this.mergedImage, packOpts).then((data) => { 196 fileIo.writeSync(file.fd, data); 197 fileIo.closeSync(file.fd); 198 Logger.info(TAG, `Succeeded in packToFile`); 199 promptAction.showToast({ 200 message: $r('app.string.save_album_success'), 201 duration: 1800 202 }) 203 }).catch((error: BusinessError) => { 204 Logger.error(TAG, `Failed to packToFile. Error code is ${error.code}, message is ${error.message}`); 205 }); 206 } 207 // ... 208} 209``` 210 211**步骤5:保存完成后释放位图** 212 213当位图对象不再使用时,应及时将其赋值为空,例如:`this.mergedImage = undefined;`。 214 215```ts 216 closeSnapPopup(): void { 217 // 关闭弹窗 218 this.isShowPreview = false; 219 // 释放位图对象 220 this.mergedImage = undefined; 221 // 重置相关参数 222 this.snapPopupWidth = 100; 223 this.snapPopupHeight = 200; 224 this.snapPopupPosition = 225 PopupUtils.calcPopupCenter(this.screenWidth, this.screenHeight, this.snapPopupWidth, this.snapPopupHeight); 226 this.isLargePreview = false; 227 } 228``` 229 230### 封装全局截图接口 231如前文所述,截图接口必须在UI上下文明确的位置使用。然而,应用有时希望对不同模块封装统一的全局截图方法。例如,在下述示例中,awardBuilder构建的组件是固定结构的。GlobalStaticSnapshot提供了一个getAwardSnapshot全局方法,能够满足不同模块的需求,对同一固定模式的组件进行截图,从而实现全局截图接口的封装。 232 233```ts 234import { image } from '@kit.ImageKit'; 235import { ComponentContent } from '@kit.ArkUI'; 236 237export class Params { 238 text: string | undefined | null = ""; 239 240 constructor(text: string | undefined | null) { 241 this.text = text; 242 } 243} 244 245@Builder 246function awardBuilder(params: Params) { 247 Column() { 248 Text(params.text) 249 .fontSize(90) 250 .fontWeight(FontWeight.Bold) 251 .margin({ bottom: 36 }) 252 .width('100%') 253 .height('100%') 254 }.backgroundColor('#FFF0F0F0') 255} 256 257export class GlobalStaticSnapshot { 258 /** 259 * 一个可以获取固定对象截图的静态方法 260 */ 261 static getAwardSnapshot(uiContext: UIContext, textParam: Params): image.PixelMap | undefined { 262 let resultPixmap: image.PixelMap | undefined = undefined 263 let contentNode = new ComponentContent(uiContext, wrapBuilder(awardBuilder), textParam); 264 uiContext.getComponentSnapshot() 265 .createFromComponent(contentNode, 320, true, { scale: 1, waitUntilRenderFinished: true }) 266 .then((pixmap: image.PixelMap) => { 267 resultPixmap = pixmap 268 }) 269 .catch((err: Error) => { 270 console.error("error: " + err) 271 }) 272 return resultPixmap; 273 } 274} 275``` 276 277**完整示例:** 278 279完整示例请参考[长截图](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-long-snapshot-practice#section1566681910427)。 280 281## 组件截图最佳实践 282### 合理控制截图时机 283在实现截图功能时,需注意组件的渲染过程非一次性完成。系统在构建与显示组件时,将经过测量、布局、提交指令等多个复杂步骤,最终在一次硬件刷新时呈现于屏幕上。因此,在特定情况下,若在组件刷新后立即调用截图,可能无法获取预期内容或出现截图失败报错。 284 285为了确保截图结果准确,建议在组件完全渲染后再执行截图操作。 286 287**了解组件的绘制状态** 288 289为了确保截图内容符合预期,应该了解代码对界面状态的修改时机,并注意给系统预留处理时间,这通常可以通过增加一定延时来实现。 290 291尽管可以通过inspector上的[ComponentObserver](../reference/apis-arkui/js-apis-arkui-inspector.md#componentobserver)感知应用组件绘制(draw)送显通知,但需要注意的是,ComponentObserver的组件绘制通知并不意味着系统已经真正将绘制指令执行,这取决于图形系统服务的负载情况。 292 293**明确等待绘制完成** 294 295影响截图预期的主要因素是截图时机与系统服务执行绘制指令的时间差。在发起截图调用时,应用侧之前提交的所有绘制指令可能尚未被图形服务真正执行。为此,可以通过指定[SnapshotOptions](../reference/apis-arkui/js-apis-arkui-componentSnapshot.md#snapshotoptions12)参数中的waitUntilRenderFinished为true,来确保系统在执行截图请求时等待所有之前的绘制指令均执行完毕,从而截取到更完整的内容。 296 297> **说明:** 298> 299> 建议始终开启`waitUntilRenderFinished`参数。 300 301**了解资源加载对截图的影响** 302 303影响截图预期的另一个常见原因,是图片资源的加载。图片组件支持在线资源链接,也可指定本地资源,且绝大多数图片资源为PNG、JPEG等压缩格式。这些资源需要系统解码为可提交绘制的位图格式,此过程默认在异步IO线程上进行,因此可能由于该过程耗时的不确定性而导致截图不符合预期。 304 305应用可通过以下几种方式进行优化: 3061. 自行提前解析图片为PixelMap格式,将PixelMap配置给图片组件;建议优先以此方法进行优化。 3072. 配置所使用的图片组件的syncload属性为true来强制同步加载,这样组件被构建时,即可确保资源可以直接被提交; 3083. 通过指定延迟时长以及checkImageStatus设置为true,尝试截图,当返回160001错误后,重新加大时长进行截图; 309 310 311### 及时保存和释放位图对象 312为了及时释放资源,当截图接口返回的PixelMap对象不再使用时,应将其赋值为空。 313 314### 合理控制采样精度 315请不要截取过大尺寸的图片,截图不建议超过屏幕尺寸大小。当要截取的图片目标长宽超过底层限制时,截图会返回失败,不同设备的底层限制并不相同。可以通过控制SnapshotOptions中的scale参数,减小采样精度,这可以在很大程度上节省内存,并大幅度提高截图的效率。 316 317### 使用其他能力对自渲染场景实现截图 318尽管截图只需传入一个组件根节点即可实现对其下所有组件进行截图,但当子组件中存在[Video](../reference/apis-arkui/arkui-ts/ts-media-components-video.md)、[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)或[Web](../reference/apis-arkweb/arkts-basic-components-web.md)组件时,这并不是推荐的截图方式。建议直接使用[image.createPixelMapFromSurface](../reference/apis-image-kit/arkts-apis-image-f.md#imagecreatepixelmapfromsurface11)接口来实现。 319