引言

在产品需求中有这样一种场景:用户填写了一张精美的卡片(包含文字、颜色、布局),然后希望把它保存为图片分享到社交平台。传统做法需要后端渲染或者 Canvas 重绘——但 HarmonyOS 提供了更直接的方式:ComponentSnapshot API 可以直接将任意 ArkUI 组件截图,导出为 PixelMap 位图数据。拿到 PixelMap 后,你可以在 Image 中显示、保存到本地文件、复制到剪切板,甚至上传到服务器。

这种"所见即所得"的截图能力非常实用:从卡片生成海报、从表单生成凭证、从仪表盘导出报表缩略图——只要 UI 能画出来,就能截图导出。

本文将通过构建一个"组件截图工坊",深入讲解 @ohos.arkui.componentSnapshot 的核心用法:get()(异步 Promise)、getSync()(同步阻塞)、SnapshotOptions(缩放/等待渲染)、以及 PixelMap 的显示与处理。

读完本文你将能够:

  • 使用 .id() 为组件分配标识符,作为截图的定位依据
  • 使用 componentSnapshot.get(id, options) 异步获取组件截图
  • 使用 componentSnapshot.getSync(id, options) 同步获取组件截图
  • 理解 SnapshotOptionsscalewaitUntilRenderFinished 参数
  • 将 PixelMap 传递给 Image 组件进行预览显示
  • 了解异步 vs 同步模式的性能差异和适用场景

ComponentSnapshot 概述

什么是 ComponentSnapshot

ComponentSnapshot 是 ArkUI 提供的组件快照 API,位于 @ohos.arkui.componentSnapshot 模块(属于 @kit.ArkUI 套件)。它的能力直截了当:根据组件 ID 找到组件 → 将组件渲染内容生成像素位图 → 返回 PixelMap。

import { componentSnapshot } from '@kit.ArkUI';

// 异步模式 — 推荐
componentSnapshot.get('my_component_id', {
  scale: 2.0,
  waitUntilRenderFinished: true
}).then((pixelMap: image.PixelMap) => {
  // pixelMap 即截图结果
  this.resultImage = pixelMap;
});

// 同步模式 — 阻塞主线程,最多等待 3 秒
let pixelMap: image.PixelMap = componentSnapshot.getSync('my_component_id', {
  scale: 1.0
});

它的工作原理是:在 get()getSync() 被调用时,ArkUI 框架找到对应 ID 的组件节点,通知渲染引擎将该组件及其子组件绘制到一个离屏 Surface 上,然后将 Surface 的内容编码为 PixelMap 返回。

与其它截图方案的区别

在 ComponentSnapshot 出现之前,开发者如果想在 ArkUI 中"截图",通常有两种途径:

  1. Canvas 重绘:手动在 Canvas 上用绘图 API 重新绘制一遍 UI——工作量大,且无法保证与原始 UI 完全一致。
  2. XComponent + 原生:通过 XComponent 调用 Native 层的窗口截图能力——开发复杂度高,涉及 NDK 和平台代码。

ComponentSnapshot 的优势在于零成本映射——UI 是怎么渲染的,截图就是什么样的。你不关心布局细节、字体渲染、抗锯齿等问题,框架替你处理了一切。

API 版本与废弃说明

注意:在 API 18 中,componentSnapshot.get() 的异步回调版本被标记为 deprecated,推荐使用 UIContext.ComponentSnapshot#get。但在 API 24 中,Promise 版本的 get() 和同步版本 getSync() 仍然可用。本文 Demo 使用 Promise 版本的 get()——因为它是目前最常用的模式,且与 UIContext 版本的 API 形态基本一致。

核心 API 解析

组件 ID 与 .id()

ComponentSnapshot 通过组件的 .id() 属性来定位目标组件。这是整个截图流程的起点:

Column() {
  Text('Hello World')
    .fontSize(28)
    .fontColor('#FFFFFF')
}
.id('target_card')  // 分配 ID,ComponentSnapshot 通过这个 ID 找到组件

几点注意事项:

  • ID 必须全局唯一:如果页面中有两个组件使用相同的 ID,ComponentSnapshot 会优先选择第一个匹配的组件。
  • ID 是运行时属性.id() 不是组件的标识符(与 @Entry 或组件名无关),它只是给 DOM 节点打了一个运行时标签。
  • ID 需要已挂载:截图时目标组件必须已经渲染到屏幕上。如果在组件挂载前调用 get(),会抛出 100001 — Invalid ID 错误。

get(id, options?): Promise<PixelMap>

异步截图方法。调用后立即返回一个 Promise,截图操作在后台进行,不阻塞 UI 线程。

componentSnapshot.get('target_card', {
  scale: 2.0,
  waitUntilRenderFinished: true
}).then((pixelMap: image.PixelMap) => {
  this.capturedImage = pixelMap;
  console.log('截图完成,尺寸:' + pixelMap.getImageInfoSync().size.width);
}).catch((err: Error) => {
  console.error('截图失败:' + err.message);
});

推荐使用异步模式,尤其是在以下场景:

  • 目标组件较大或包含复杂渲染(渐变、阴影、多层嵌套)
  • 使用了较大的 scale 值(如 3.0x),生成的 PixelMap 数据量大
  • 在用户交互中触发截图(如点击按钮),需要保持 UI 响应

getSync(id, options?): PixelMap

同步截图方法。直接返回 PixelMap,但会阻塞主线程。API 设定了最长 3 秒的等待时间,超时会抛出 160002 — Timeout 异常。

try {
  let pixelMap: image.PixelMap = componentSnapshot.getSync('target_card', {
    scale: 1.0,
    waitUntilRenderFinished: true
  });
  this.capturedImage = pixelMap;
} catch (e) {
  console.error('同步截图失败或超时');
}

同步模式适合的场景非常有限:

  • 目标组件很小(如单个按钮或图标)
  • scale 值较小(如 1.0x)
  • aboutToAppear() 或数据初始化等非交互阶段调用

在用户交互回调中使用 getSync() 会导致明显的卡顿——用户点击按钮后,整个界面会"冻住"直到截图完成。因此 Demo 中默认使用异步模式,并提供同步模式的开关让用户自己体验两者的差异。

SnapshotOptions

SnapshotOptions 接口定义了三个可选参数:

interface SnapshotOptions {
  scale?: number;               // 缩放比例,默认 1.0
  waitUntilRenderFinished?: boolean;  // 是否等待渲染完成,默认 false
  region?: SnapshotRegionType;  // 截图区域(API 15+)
}
scale

scale 是指定截图分辨率的缩放系数。注意它不是改变截图在屏幕上的显示大小——它改变的是 PixelMap 的像素密度。

  • scale: 1.0 → 以屏幕分辨率 1:1 截图。在标准密度屏幕上,一个 100×100vp 的组件生成约 100×100px 的 PixelMap。
  • scale: 2.0 → 以 2 倍分辨率截图。同样的组件生成约 200×200px 的 PixelMap,细节更清晰,文件也更大。
  • scale: 3.0 → 3 倍分辨率,适合需要高清输出的场景(如打印、生成海报)。

推荐策略:如果截图仅用于应用内预览,scale: 1.0 足够。如果需要保存为图片文件或分享到外部,scale: 2.0 更合适——它提供了更高的细节保留,而文件大小仍在可接受范围内。

waitUntilRenderFinished

当组件的渲染不是瞬时完成时(如包含 Image 组件的网络图片尚未加载完成、或包含异步渲染的 Canvas),waitUntilRenderFinished: false(默认值)会在当前渲染帧完成后立即截图——此时某些内容可能尚未渲染完毕,截图会不完整。

设为 true 后,API 会等待所有已知的渲染任务完成(包括 Image 解码、异步布局等)后再截图。代价是截图耗时会更长。

// 快速但可能不完整
componentSnapshot.get('card', { scale: 1.0 });

// 完整但可能更慢
componentSnapshot.get('card', { scale: 1.0, waitUntilRenderFinished: true });

推荐策略:如果目标组件包含网络图片、动态字体或 Canvas 内容,建议设为 true。如果是纯文字+纯色背景的简单组件,false 即可。
在这里插入图片描述
在这里插入图片描述

Demo 设计:组件截图工坊

本文 Demo 实现了一个完整的截图实验室,包含以下功能:

页面结构

Column(根容器)
├── Header(深色标题栏:"组件截图工坊" + ComponentSnapshot 标签)
├── Scroll
│   └── Column
│       ├── 截取目标区(带 .id('target_card') 的彩色预览卡片)
│       ├── 截取设置区
│       │   ├── 缩放比例 Slider(0.5x ~ 3.0x,步长 0.25)
│       │   ├── 等待渲染完成 Toggle
│       │   ├── 同步模式 Toggle
│       │   ├── 模式说明文字
│       │   └── "开始截取" Button
│       ├── 截取结果区(Image 组件显示 PixelMap + 元信息标签)
│       ├── 截取历史区(ForEach 列表显示最近 5 次记录)
│       └── API 参考区
└── 根容器结束

5 个交互点

  1. 更换截取目标:点击"换一个样式"切换预览卡片的颜色和文字,可以截取不同外观的卡片。
  2. 调节缩放比例:拖动 Slider 从 0.5x 到 3.0x,控制截图的 PixelMap 分辨率。
  3. 切换模式参数:两个 Toggle 分别控制 waitUntilRenderFinished 和同步/异步模式。
  4. 执行截图:点击"开始截取"按钮,调用 get()getSync() 获取截图,测量耗时并显示结果。
  5. 查看历史:最近 5 次截图显示在历史列表中,记录序号、模式、缩放比例。

核心实现

截图目标组件

Column() {
  Text(this.previewTitle)
    .fontSize(28)
    .fontColor('#FFFFFF')
    .fontWeight(FontWeight.Bold)
    .margin({ bottom: 8 })

  Text('这是一段用于演示组件截图的示例文字...')
    .fontSize(12)
    .fontColor('#FFFFFFCC')
    .lineHeight(18)
    .maxLines(3)

  // 显示当前缩放比例标签
  Row() {
    Text('scale: ' + this.captureScale.toFixed(2) + 'x')
      .fontSize(10)
      .fontColor('#FFFFFF88')
      .padding({ top: 4, bottom: 4, left: 8, right: 8 })
      .borderRadius(4)
      .backgroundColor('#FFFFFF20')
  }
  .margin({ top: 10 })
  .width('100%')
}
.width('100%')
.padding(20)
.borderRadius(12)
.backgroundColor(this.previewColor)
.id('target_card')   // ← 截图定位标识

关键点:.id('target_card') 是整个截图的锚点。ComponentSnapshot 通过这个字符串找到组件节点。id 参数在 get()getSync() 中作为第一个参数传入。

异步截图实现

captureSnapshot(): void {
  let startTime: number = Date.now();
  this.isCapturing = true;

  let options: componentSnapshot.SnapshotOptions = {
    scale: this.captureScale,
    waitUntilRenderFinished: this.waitRender
  };

  if (this.useSyncMode) {
    // 同步模式 — 直接返回 PixelMap
    try {
      let pixelMap: image.PixelMap = componentSnapshot.getSync('target_card', options);
      this.onCaptureSuccess(pixelMap, startTime, '同步');
    } catch (e) {
      this.isCapturing = false;
    }
  } else {
    // 异步模式 — Promise 返回 PixelMap
    componentSnapshot.get('target_card', options).then((pixelMap: image.PixelMap) => {
      this.onCaptureSuccess(pixelMap, startTime, '异步');
    }).catch((e: Error) => {
      this.isCapturing = false;
    });
  }
}

onCaptureSuccess(pixelMap: image.PixelMap, startTime: number, method: string): void {
  this.snapshotData = pixelMap;
  this.hasSnapshot = true;
  this.isCapturing = false;
  this.captureTime = Date.now() - startTime;
  this.lastMethod = method;
  this.captureCount = this.captureCount + 1;

  // 更新历史记录
  let newHistory: SnapshotRecord[] = this.history.slice();
  newHistory.unshift(new SnapshotRecord(this.captureCount, this.captureScale, method));
  if (newHistory.length > 5) {
    newHistory.pop();
  }
  this.history = newHistory;
}

实现要点:

  • 使用 Date.now() 测量截图耗时,反映 scalewaitUntilRenderFinished 对性能的实际影响。
  • isCapturing 状态锁防止重复点击(Button 的 enabled 属性绑定到 !this.isCapturing)。
  • 历史记录通过不可变更新模式(slice() + unshift())维护,限制最近 5 条。

PixelMap 显示

将 PixelMap 显示在 Image 组件中非常简单——直接将 PixelMap 对象作为 Image 的数据源:

if (this.hasSnapshot && this.snapshotData !== null) {
  Column() {
    Text('截取结果')
      .fontSize(13)
      .fontColor('#1a1a2e')
      .fontWeight(FontWeight.Medium)

    Column() {
      Image(this.snapshotData)
        .width('100%')
        .objectFit(ImageFit.Contain)
        .borderRadius(8)
    }
    .width('100%')
    .padding(4)
    .borderRadius(10)
    .border({ width: 1, color: '#F0F0F0' })
    .backgroundColor('#FAFAFA')
  }
}

ImageFit.Contain 确保截图等比缩放适配显示区域宽度,不裁剪、不拉伸。

注意 PixelMap 存储在私有变量 snapshotData 而非 @State 中,原因是在 ArkTS 严格模式下,复杂的 SDK 对象类型(如 image.PixelMap)在 @State 中的行为不完全可控。Demo 通过一个独立的 @State hasSnapshot: boolean 来控制条件渲染——当 hasSnapshot 从 false 变为 true 时,ArkUI 重新执行 build() 并进入 if 分支,此时 Image(this.snapshotData) 被创建并读取最新的 PixelMap 数据。

状态更新模式

PixelMap 数据存储采用了一个特殊模式——私有变量 + @State 触发标志:

// 截图结果 — 不通过 @State 存储(避免复杂 SDK 对象在状态管理中的潜在问题)
private snapshotData: image.PixelMap | null = null;
@State hasSnapshot: boolean = false;  // 用作 UI 刷新触发器

onCaptureSuccess(pixelMap: image.PixelMap, ...): void {
  this.snapshotData = pixelMap;   // 更新数据
  this.hasSnapshot = true;        // 触发 UI 刷新
}

这种模式适合存储复杂对象(PixelMap、回调函数、外部资源引用等),它们不需要细粒度的属性级别变化追踪——只需要在"数据已更新"时整体重渲染。

性能考量

异步 vs 同步的实际差异

在 Demo 中,同一个卡片(带有文字、圆角、背景色的 Column),使用不同的 scale 和模式,实测耗时差异明显:

scale 模式 waitUntilRenderFinished 大致耗时
1.0x 异步 false < 10ms
1.0x 同步 false < 10ms
2.0x 异步 true 20-40ms
3.0x 异步 true 50-100ms
3.0x 同步 true 50-100ms + UI 卡顿

在低 scale 值下,同步和异步的耗时差异不大。但在 3.0x 高分辨率截图中,同步模式会带来明显的 UI 卡顿——因为主线程被阻塞在截图渲染上,用户在此期间无法进行任何操作。

scale 与内存的关系

PixelMap 占用的内存量与 scale 的平方成正比:scale: 2.0 时内存占用是 scale: 1.0 的 4 倍,scale: 3.0 时是 9 倍。

对于 360×200vp 的卡片:

  • 1.0x → PixelMap 约 360×200=72,000 像素,RGBA 4 字节,约 288KB
  • 2.0x → 约 720×400=288,000 像素,约 1.15MB
  • 3.0x → 约 1080×600=648,000 像素,约 2.6MB

在多次截图场景中,记得释放不再使用的 PixelMap(调用 pixelMap.release()),避免内存泄漏。

截图与状态更新

一个重要但容易被忽略的行为:当 waitUntilRenderFinished: false 时,ComponentSnapshot 在当前渲染帧完成后立即截图。如果在截图前刚刚更新了 @State 变量(触发了重渲染),这个渲染可能尚未完成——此时截图会得到旧状态的 UI。

// 反例:立即截图可能截到旧状态
this.previewColor = '#FF0000';  // 触发重渲染
let pixelMap = componentSnapshot.getSync('target_card');  // 可能截到旧颜色

// 正例:等待渲染完成
this.previewColor = '#FF0000';
let pixelMap = componentSnapshot.getSync('target_card', {
  waitUntilRenderFinished: true
});

Demo 中的 waitUntilRenderFinished Toggle 正是为了让用户直观地理解这个差异。

实际应用场景

场景一:卡片分享/导出

这是 ComponentSnapshot 最典型的应用场景。用户在应用中设计了一张卡片(名片、邀请函、成就徽章等),点击"导出"按钮,ComponentSnapshot 将卡片组件截图,然后使用 @ohos.file.fs 将 PixelMap 编码为 JPEG/PNG 保存到本地或通过分享 Intent 发送。

场景二:列表项缩略图

对于内容型应用(笔记、文档、图表),使用 ComponentSnapshot 为每个用户的文档生成缩略图,在文档列表中展示——比每次都重新渲染完整文档高效得多。

场景三:错误/状态快照

在 Beta 测试或调试场景中,当应用发生异常时,使用 ComponentSnapshot 自动截取当前界面状态,上报给后端用于问题排查。这种方式比单纯的日志更直观。

场景四:主题预览

在换肤/主题定制功能中,使用 ComponentSnapshot 将不同主题下的界面渲染效果生成为预览图,用户在选择主题时可以看到真实效果而非文字描述。

总结

本文通过构建一个"组件截图工坊",深入讲解了 HarmonyOS ArkUI 中 @ohos.arkui.componentSnapshot 的核心用法:

  1. .id() 与组件定位:为组件分配运行时 ID,作为 ComponentSnapshot 的截图锚点。
  2. 异步截图 get():通过 Promise 返回 PixelMap,不阻塞主线程,适合大部分场景。
  3. 同步截图 getSync():直接返回 PixelMap,阻塞主线程最多 3 秒,仅适合简单快速截图。
  4. SnapshotOptionsscale 控制输出分辨率(与内存平方成正比),waitUntilRenderFinished 确保截图完整性。
  5. PixelMap 显示:直接将 PixelMap 作为 Image 组件的数据源,配合 ImageFit.Contain 等比显示。

ComponentSnapshot 的价值在于它的所见即所得——UI 是什么样,截图就是什么样。你不需要用 Canvas 重绘 DOM,不需要学习新的渲染 API,只需要给目标组件打上一个 .id(),然后调用一个方法。这种简洁性让它成为"UI 到图片"场景的最优解。

从产品角度看,ComponentSnapshot 让"截图分享"、“卡片导出”、"状态快照"等功能得以用极低的开发成本实现——这不仅是技术上的便利,更是产品可能性空间的扩展。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐