HarmonyOS 6商城开发学习:商品图文详情长截图分享——componentSnapshot滚屏裁剪与SaveButton存图实战
在HarmonyOS 6购物比价或电商类应用中,"商品图文详情快照分享"是高频运营需求——用户点分享按钮,自动将超出一屏的商品图文介绍(标题+参数+WebView图文详情)截取成长图,预览后可保存相册或直接发给好友。直接用componentSnapshot.get()只截到当前可视区、长图拼接重复、WebView未开全页绘制、WRITE_MEDIA权限被拒是常见坑。
本文将参考官方行业实践与51CTO社区快照分享案例,用 componentSnapshot.get()+ Scroller分段滚屏 + PixelMap.crop/writePixelsSync拼接 + photoAccessHelper+SaveButton落盘 完整实现商品详情页长截图分享功能。
一、需求拆解与页面结构
1. 典型商品详情截图区
Column
├── Header(商品名/价格/标签)— 固定高
├── Tabs(图文详情 | 参数 | 评价)
│ └── Scroll/Web(图文详情区,可变高)
└── BottomBar(加入购物车/立即购买)← 不截
截图目标 = Header + Tabs下图文详情区整体内容(不含底部操作栏),生成长图。
2. 核心API
|
API |
用途 |
|---|---|
|
|
按组件ID截取当前渲染像素→ |
|
|
驱动Scroll分段滚屏 |
|
|
裁剪每屏新增部分(避免重复拼接) |
|
|
创建空白长图并按区写入 |
|
|
安全写入相册(无需动态申明存储权限) |
|
|
Web图文详情必须调,否则只截可视区 |
二、图片处理工具类(裁剪与合并)
// utils/SnapshotUtil.ets
import { image } from '@kit.ImageKit';
import { UIContext } from '@kit.ArkUI';
export class SnapshotUtil {
/**
* 裁剪截图中的新增滚动部分(避免拼接重复)
* @param uiCtx UI上下文(vp2px转换)
* @param pm 当前屏 componentSnapshot 得到的 PixelMap
* @param offsets 历次滚屏Y偏移记录 [0, h1, h2...]
* @param vpW 截图组件宽(vp)
* @param vpH 截图组件可视高(vp)
*/
static async getCropArea(
uiCtx: UIContext,
pm: image.PixelMap,
offsets: number[],
vpW: number,
vpH: number
): Promise<image.PositionArea> {
const stride = pm.getBytesNumberPerRow();
const buf = new ArrayBuffer(pm.getPixelBytesNumber());
const area: image.PositionArea = {
pixels: buf, offset: 0, stride,
region: { x: 0, y: 0, size: { width: 0, height: 0 } }
};
const len = offsets.length;
if (len >= 2) {
// 非首屏:只保留本次新增滚动部分
const prevY = offsets[len - 2];
const curY = offsets[len - 1];
const addH = curY - prevY;
const cropRgn = {
x: 0,
y: uiCtx.vp2px(vpH - addH),
size: { width: uiCtx.vp2px(vpW), height: uiCtx.vp2px(addH) }
};
await pm.crop(cropRgn);
area.region = cropRgn;
} else {
// 首屏保留全部
area.region = {
x: 0, y: 0,
size: { width: uiCtx.vp2px(vpW), height: uiCtx.vp2px(vpH) }
};
}
pm.readPixelsSync(area);
return area;
}
/**
* 按序合并裁剪区域为一张长图 PixelMap
* @param uiCtx UI上下文
* @param areas 裁剪PositionArea数组(顺序=滚屏顺序)
* @param totalContentHVP 内容总高vp = 末次Y偏移 + 首屏可视高
* @param vpW 内容宽vp
*/
static async mergeToLong(
uiCtx: UIContext,
areas: image.PositionArea[],
totalContentHVP: number,
vpW: number
): Promise<image.PixelMap> {
const totalH = uiCtx.vp2px(totalContentHVP);
const w = uiCtx.vp2px(vpW);
const opts: image.InitializationOptions = {
editable: true,
pixelFormat: image.PixelFormat.RGBA_8888,
size: { width: w, height: totalH }
};
const longPm = image.createPixelMapSync(opts);
let offY = 0;
for (const a of areas) {
a.offset = offY;
longPm.writePixelsSync(a);
offY += a.region.size.height;
}
return longPm;
}
/** 简易延时 */
static sleep(ms: number): Promise<void> {
return new Promise(r => setTimeout(r, ms));
}
}
为什么要只保留新增部分?
每次滚屏后
componentSnapshot.get()截的是当前可视区全图,直接拼接上下相邻内容会重叠。只 crop 新增滚入区域可保证长图无缝。
三、商品详情页——滚屏截图+预览+保存
// pages/GoodsDetailSnapshotPage.ets
import { componentSnapshot } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { SnapshotUtil } from '../utils/SnapshotUtil';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct GoodsDetailSnapshotPage {
private ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
// ===== 详情内容Scroller =====
private detailScroller: Scroller = new Scroller();
@State curOffset: number = 0;
private scrollYHistory: number[] = [];
private cropAreas: image.PositionArea[] = [];
// ===== 快照结果 =====
@State mergedPic: image.PixelMap | undefined = undefined;
@State showPreview: boolean = false;
// 布局常量(实际可用getInspectorBounds动态取更准)
private DETAIL_ID = 'goods_detail_scroll';
private DETAIL_W_VP = 360; // ≈屏幕宽
private DETAIL_H_VP = 480; // 可视区高(含Header估算)
// ===== 开始截图 =====
async doSnapshot() {
// 记住原位
const bakY = this.detailScroller.currentOffset().yOffset;
// 滚回顶部 & 等渲染
this.detailScroller.scrollTo({ yOffset: 0, animation: false });
await SnapshotUtil.sleep(300);
this.scrollYHistory = [0];
this.cropAreas = [];
await this.captureLoop();
// 恢复原位置
this.detailScroller.scrollTo({ yOffset: bakY, animation: { duration: 200 } });
this.showPreview = true;
}
private async captureLoop(): Promise<void> {
const pm = await componentSnapshot.get(this.DETAIL_ID);
const area = await SnapshotUtil.getCropArea(
this.getUIContext(), pm, this.scrollYHistory,
this.DETAIL_W_VP, this.DETAIL_H_VP
);
this.cropAreas.push(area);
if (!this.detailScroller.isAtEnd()) {
const nextY = this.scrollYHistory[this.scrollYHistory.length - 1] + this.DETAIL_H_VP;
this.detailScroller.scrollTo({ yOffset: nextY, animation: { duration: 200 } });
await SnapshotUtil.sleep(350);
this.scrollYHistory.push(nextY);
return this.captureLoop();
}
// 到底 → 合并
const totalH = this.scrollYHistory[this.scrollYHistory.length - 1] + this.DETAIL_H_VP;
this.mergedPic = await SnapshotUtil.mergeToLong(
this.getUIContext(), this.cropAreas, totalH, this.DETAIL_W_VP
);
}
// ===== 保存到相册(配合SaveButton)=====
async saveToAlbum(pm: image.PixelMap) {
try {
const helper = photoAccessHelper.getPhotoAccessHelper(this.ctx);
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png');
const file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
const packer = image.createImagePacker();
const data = await packer.packToData(pm, { format: 'image/png', quality: 100 });
fileIo.writeSync(file.fd, data);
fileIo.closeSync(file.fd);
this.getUIContext().getPromptAction().showToast({ message: '已保存到相册' });
this.showPreview = false;
} catch (e) {
this.getUIContext().getPromptAction().showToast({ message: '保存失败' });
console.error(`save err: ${JSON.stringify(e)}`);
}
}
build() {
Stack() {
Column() {
// ---- 商品详情内容(待截图区域)----
Scroll(this.detailScroller) {
Column({ space: 12 }) {
// Header
Column() {
Text('HarmonyOS 6 智慧手表 Ultra')
.fontSize(20).fontWeight(FontWeight.Bold)
Text('¥1499 限时满减中')
.fontSize(16).fontColor('#FF5722').margin({ top: 4 })
}.padding(16)
// 图文详情区(示意多屏高度)
Column() {
ForEach([1,2,3,4,5], (i: number) => {
Image($r('app.media.prod_01'))
.width('100%').height(220)
.objectFit(ImageFit.Cover).margin({ bottom: 8 })
})
}.padding(16)
}
}
.id(this.DETAIL_ID)
.onScroll(() => {
this.curOffset = this.detailScroller.currentOffset().yOffset;
})
.layoutWeight(1)
// ---- 底部操作栏(不截图)----
Row() {
Button('分享商品快照')
.layoutWeight(1)
.height(44)
.backgroundColor('#FF5722')
.borderRadius(22)
.onClick(() => this.doSnapshot())
}
.padding(12)
.backgroundColor('#FFF')
}
.width('100%').height('100%').backgroundColor(Color.White)
// ---- 预览弹窗 ----
if (this.showPreview && this.mergedPic) {
this.buildPreviewDialog()
}
}
}
// 预览+保存弹窗
@Builder
buildPreviewDialog() {
Column() {
// 遮罩
Column().layoutWeight(1).backgroundColor('rgba(0,0,0,0.45)').onClick(()=>{
this.showPreview=false; this.mergedPic=undefined;
})
Column() {
Scroll() { Image(this.mergedPic!).width('100%').objectFit(ImageFit.Contain) }
.height('70%')
Row({ space: 20 }) {
Button('取消').onClick(()=>{
this.showPreview=false; this.mergedPic=undefined;
})
// ✅ 安全控件保存——无需 WRITE_MEDIA 权限
SaveButton({ icon: SaveIconStyle.FULL_FILLED, text:'保存', buttonType:ButtonType.NORMAL })
.onClick(async (_e, res) => {
if (res === SaveButtonOnClickResult.SUCCESS && this.mergedPic) {
await this.saveToAlbum(this.mergedPic);
}
})
}.padding(16)
}
.backgroundColor(Color.White)
.borderRadius({ topLeft:24, topRight:24 })
}
.width('100%').height('100%').position({x:0,y:0})
}
}
WebView图文详情补充:若图文详情用
Web({controller, src}),须先调webview.WebviewController.enableWholeWebPageDrawing(),在onPageEnd后开始滚屏截图,判断是否到底用webController.getPageHeight()而非isAtEnd()。
四、避坑指南
|
问题 |
原因 |
修复 |
|---|---|---|
|
截图只截到首屏/可视区 |
|
用 |
|
长图有重复段落 |
每次截全可视区直接拼接 |
|
|
保存时报权限拒绝 |
普通Button调MediaLibrary写文件 |
用 |
|
Web图文详情截不全 |
未启用全页绘制 |
调 |
|
滚屏截到动画残影 |
|
|
五、总结:商品详情长截图SOP
-
给截图区组件设唯一
id,包在Scroll内(Web需开enableWholeWebPageDrawing) -
滚回顶部 →
componentSnapshot.get(id)截首屏 →crop存首段 -
循环
scrollTo(y+可视高) → sleep → get → crop新增区 → push,直到isAtEnd()(Web用getPageHeight()) -
合并:
createPixelMapSync+ 按序writePixelsSync(area) -
预览 + SaveButton:
photoAccessHelper.createAsset+ImagePacker.packToData+fileIo.write
核心法则:HarmonyOS 6 中商品图文详情长截图分享 = "componentSnapshot分段截 + PixelMap.crop去重拼接 + SaveButton安全落盘",Web详情额外开 enableWholeWebPageDrawing()。
©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任。
更多推荐


所有评论(0)