• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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