系列第 5 篇。本文讲一个很容易被低估的工具能力:不是只把文本复制出去,而是把费用明细做成图片,方便发到群里确认。

一、真实问题背景

羽毛球活动结束后,经常要把场地费、球费、男女折扣、人均费用发到群里。纯文本能用,但信息层级弱;直接截图整个屏幕又容易带上多余导航。更好的方式是:只截取费用结果卡片,生成一张干净图片,再走系统分享。

项目里的费用结果页同时支持复制文本和分享截图。复制用于快速粘贴,截图用于群内确认和留档。

费用输入

费用结果

二、目标与边界

本文要实现的是一个完整分享闭环:

1. 用 componentSnapshot 截取指定组件。2. 用 ImageKit 把 PixelMap 编码成 PNG。3. 写入应用缓存目录。4. 用 fileUri 生成可分享 URI。5. 用 ShareKit 拉起系统分享面板。

边界是:本文不讲图片美化模板,也不讲跨设备传输。后续如果要做“平板展示 + 手机分享”或“跨设备数据流转”,可以复用这里的截图和文件输出能力。

三、结果页结构

核心页面是 features/src/main/ets/fee/FeeResultPage.ets。页面先从 FeeCalcService 读取最近一次计算结果,再把结果区域包在一个带 id 的组件里。

const CAPTURE_ID: string = 'fee_result_capture';

aboutToAppear(): void {
  this.result = FeeCalcService.loadLastResult();
}

Column() {
  // 费用结果卡片和计算明细
}
.id(CAPTURE_ID)
.width('100%')
.backgroundColor($r('app.color.bg_page'));

给组件设置稳定 id 是截图的前提。不要截整个页面,否则会把顶部导航、按钮和说明文字都带进去。

这篇文章涉及的源码对象比较集中:

- features/src/main/ets/fee/FeeResultPage.ets:结果页 UI、复制、截图和分享入口。- common/src/main/ets/service/FeeCalcService.ets:费用输入、结果计算和最近一次结果缓存。- common/src/main/ets/feedback/Feedback.ets:Toast 反馈封装。- entry/src/main/ets/pages/FeeResult.ets:路由壳页面。- common/src/main/ets/router/Routes.ets:费用页与结果页的路由常量。

把这些文件列清楚,有一个实际好处:以后如果分享失败,可以快速判断问题在计算服务、页面渲染、截图组件、文件写入还是系统分享调用,而不是把所有问题都归到“ShareKit 不稳定”。

四、复制文本

分享图片之前,页面先提供了更轻量的复制能力。它使用 @kit.BasicServicesKit 的 pasteboard。

const pasteboardData = pasteboard.createData(
  pasteboard.MIMETYPE_TEXT_PLAIN,
  this.result.detailText
);

pasteboard.getSystemPasteboard().setData(pasteboardData).then(() => {
  Feedback.toast('费用结果已复制');
});

复制文本是低成本兜底。即使截图或分享失败,用户仍然能把费用结果发出去。

五、截图、编码与写入缓存

图片分享的核心逻辑在 shareResult()。先截取组件,得到 PixelMap,再用 image.createImagePacker() 编成 PNG。

const pixelMap: image.PixelMap = await componentSnapshot.get(CAPTURE_ID);
const packer: image.ImagePacker = image.createImagePacker();
const opts: image.PackingOption = { format: 'image/png', quality: 100 };
const buffer: ArrayBuffer = await packer.packing(pixelMap, opts);

接下来把二进制写入缓存目录,并转换成文件 URI。

const ctx: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
const filePath: string = `${ctx.cacheDir}/fee_result_${Date.now()}.png`;
const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.writeSync(file.fd, buffer);
fs.closeSync(file);
const uri: string = fileUri.getUriFromPath(filePath);

缓存目录适合这种临时分享文件。它不是用户长期相册,也不是业务数据持久化目录。

六、调用 ShareKit

有了 URI 后,就可以构造 systemShare.SharedData,再通过 ShareController 展示分享面板。

const shared: systemShare.SharedData = new systemShare.SharedData({
  utd: utd.UniformDataType.PNG,
  uri
});

const controller: systemShare.ShareController = new systemShare.ShareController(shared);
await controller.show(ctx, {
  previewMode: systemShare.SharePreviewMode.DETAIL,
  selectionMode: systemShare.SelectionMode.SINGLE
});

这里选择 PNG 类型,是因为费用结果是文字和卡片组合,清晰度比有损格式更重要。

七、取舍与风险

截图分享有几个风险点。第一,组件必须已经渲染完成,否则 componentSnapshot.get 可能失败。第二,截图区域不能太大,否则图片体积和编码耗时都会上升。第三,分享文件必须用系统可识别的 URI,不能直接把内部路径丢给其他应用。

因此项目把截图范围控制在结果卡片区域,失败时用 Toast 提示,并保留文本复制能力。这是一种工具 App 比较稳的设计:主流程可用,增强能力失败不造成数据丢失。

还有一个容易忽略的边界:分享图只是一种输出格式,不能反过来成为业务数据源。真正的费用结果仍然来自 FeeCalcService 中的结构化结果,截图只是当前结果的表达。这样后续如果要增加“复制 Markdown 明细”“生成群公告文案”“跨设备发送费用结果”,都不需要从图片里反解析数据。

八、验证命令

构建验证:

& 'D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\hvigor\bin\hvigorw.bat' assembleHap --mode module -p product=default --no-daemon

验证时间:2026-06-28。当前构建结果为 BUILD SUCCESSFUL。手工验收建议覆盖:无结果时提示、正常复制、正常分享、取消分享、结果页返回再进入、深色模式下截图可读。

九、官方参考

截图、文件和分享涉及多个 Kit,正式开发应分别核对 ArkUI、ImageKit、CoreFileKit 和 ShareKit 文档。可从 HarmonyOS 应用开发文档 进入对应 API 参考。

十、工程验收清单

- 被截图区域有稳定 id。- 截图范围不包含无关按钮和导航。- PNG 写入缓存目录,分享后不污染业务数据。- fileUri 由真实文件路径生成。- 分享失败有提示,复制文本仍可用。- 截图内容与当前费用结果一致。

十一、小结

分享能力的价值不在于“能调起系统面板”,而在于把用户要表达的信息整理好。费用结果页先形成结构化明细,再截图为图片,最后走系统分享,这才是完整闭环。后续做跨设备展示、手表端计分或俱乐部群分享时,也可以沿用这种“数据清楚、输出稳定、失败兜底”的思路。

十二、下一篇衔接

接下来可以继续写 App Linking:从聚合链接直达费用页、对阵页或计分页,为后续跨设备入口和活动分享做准备。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐