HarmonyOS 6商城开发学习:AI商品推荐富媒体卡片快照分享——componentSnapshot

HarmonyOS 6商城开发学习:AI商品推荐富媒体卡片快照分享——componentSnapshot 在HarmonyOS 6购物比价或电商类应用中AI推荐模块可生成富媒体商品卡片图文混排、价格标签、评分星级用户希望一键分享给好友——自动滚动截取完整推荐卡片生成长图预览后可保存相册或直接调系统分享。常见坑是直接用componentSnapshot.get()只截到可视区、长图拼接重复、Web图文详情未开全页绘制、WRITE_MEDIA权限动态申请被拒。本文将参考官方行业实践与51CTO社区快照分享案例用componentSnapshot.get()Scroller分段滚屏 PixelMap.crop/writePixelsSync拼接 photoAccessHelperSaveButton落盘​ 完整实现AI推荐卡片的长截图分享功能。一、需求拆解与页面结构1. 典型AI推荐卡片结构Column (AI推荐区包在 Scroll 内) ├── 推荐标题 为你推荐 ├── 商品图文块1 (Image 名称 ¥价格 ★评分) ├── 商品图文块2 ├── 商品图文块3 └── 底部说明 由HarmonyOS AI生成截图目标 上述Scroll内全部内容不含底部加入购物车操作栏。2. 交互流程用户点分享推荐 → 禁交互 → 滚回顶部 → 循环【滚一段截一屏crop新增区】→ 合并为长PixelMap → 恢复原位 → 弹预览弹窗 → SaveButton保存 / 系统分享二、图片处理工具类裁剪与合并// 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 ): Promiseimage.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 } } }; if (offsets.length 2) { // 非首屏只保留本次新增滚动部分 const prevY offsets[offsets.length - 2]; const curY offsets[offsets.length - 1]; const addH curY - prevY; // 新增像素高 const cropRgn { x: 0, y: uiCtx.vp2px(vpH - addH), // pm底部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 ): Promiseimage.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): Promisevoid { return new Promise(r setTimeout(r, ms)); } }为什么只保留新增部分​每次滚屏后componentSnapshot.get()截的是当前可视区全图直接拼接上下相邻内容会重叠。只 crop 新增滚入区域可保证长图无缝。三、AI推荐卡片页——滚屏截图预览保存// pages/AIRecCardPage.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; // 模拟推荐商品 interface RecItem { id: string; name: string; price: string; score: number; img: Resource; } const REC_ITEMS: RecItem[] [ { id:R01, name:HarmonyOS 6 智慧耳机 Pro, price:¥899, score:4.8, img:$r(app.media.ic_headphone) }, { id:R02, name:氮化镓快充套装 120W, price:¥149, score:4.6, img:$r(app.media.ic_charger) }, { id:R03, name:AI翻译词典笔 2代, price:¥259, score:4.9, img:$r(app.media.ic_dict) }, ]; Entry Component struct AIRecCardPage { private ctx this.getUIContext().getHostContext() as common.UIAbilityContext; private scroller: Scroller new Scroller(); private REC_AREA_ID ai_rec_area; State snapshotPm: image.PixelMap | undefined undefined; State showPreview: boolean false; // 滚屏历史 裁剪区 private offsets: number[] []; private areas: image.PositionArea[] []; // 布局常量可用getInspectorBounds动态取更准 private AREA_W_VP 360; private AREA_H_VP 420; // 执行快照 async doSnapshot() { // 记住原位 const bakY this.scroller.currentOffset().yOffset; // 滚回顶部等渲染 this.scroller.scrollTo({ yOffset: 0, animation: false }); await SnapshotUtil.sleep(300); this.offsets [0]; this.areas []; await this.captureLoop(); // 恢复原位置 this.scroller.scrollTo({ yOffset: bakY, animation: { duration: 200 } }); this.showPreview true; } private async captureLoop(): Promisevoid { const pm await componentSnapshot.get(this.REC_AREA_ID); const area await SnapshotUtil.getCropArea( this.getUIContext(), pm, this.offsets, this.AREA_W_VP, this.AREA_H_VP ); this.areas.push(area); if (!this.scroller.isAtEnd()) { const nextY this.offsets[this.offsets.length - 1] this.AREA_H_VP; this.scroller.scrollTo({ yOffset: nextY, animation: { duration: 200 } }); await SnapshotUtil.sleep(350); this.offsets.push(nextY); return this.captureLoop(); } // 合并 const totalH this.offsets[this.offsets.length - 1] this.AREA_H_VP; this.snapshotPm await SnapshotUtil.mergeToLong( this.getUIContext(), this.areas, totalH, this.AREA_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() { // ---- AI推荐区待截图---- Scroll(this.scroller) { Column({ space: 16 }) { Text(为你推荐 · AI精选) .fontSize(18) .fontWeight(FontWeight.Bold) .padding({ top: 12, bottom: 4 }) ForEach(REC_ITEMS, (it: RecItem) { Row({ space: 12 }) { Image(it.img).width(80).height(80).borderRadius(8).objectFit(ImageFit.Cover) Column() { Text(it.name).fontSize(15).fontColor(#333).maxLines(1) Text(it.price).fontSize(14).fontColor(#FF5722).margin({ top: 4 }) Row({ space: 4 }).margin({ top: 4 }) { ForEach(new Array(Math.round(it.score)).fill(0), (_i,i){ Image($r(sys.media.ohos_ic_public_star_filled)) .width(14).height(14).fillColor(#FF9800) }) } }.layoutWeight(1).alignItems(HorizontalAlign.Start) } .padding(12) .backgroundColor(Color.White) .borderRadius(12) .shadow({ radius: 3, color:rgba(0,0,0,0.05), offsetX:0, offsetY:1 }) }) Text(由 HarmonyOS AI 生成 · 仅供参考) .fontSize(11) .fontColor(#BBB) .margin({ top: 8, bottom: 16 }) } .padding(16) } .id(this.REC_AREA_ID) .layoutWeight(1) // ---- 底部操作栏 ---- Row() { Button(分享推荐快照) .layoutWeight(1) .height(44) .backgroundColor(#FF5722) .borderRadius(22) .onClick(() this.doSnapshot()) } .padding(12) .backgroundColor(#FFF) } .width(100%) .height(100%) .backgroundColor(#F5F6F8) // ---- 预览弹窗 ---- if (this.showPreview this.snapshotPm) { this.buildPreview() } } } Builder buildPreview() { Column() { // 遮罩 Column().layoutWeight(1).backgroundColor(rgba(0,0,0,0.45)).onClick((){ this.showPreviewfalse; this.snapshotPmundefined; }) Column() { Scroll() { Image(this.snapshotPm!).width(100%).objectFit(ImageFit.Contain) }.height(70%) Row({ space: 20 }) { Button(取消).onClick((){ this.showPreviewfalse; this.snapshotPmundefined; }) // ✅ 安全控件保存——无需 WRITE_MEDIA 权限 SaveButton({ icon: SaveIconStyle.FULL_FILLED, text:保存, buttonType:ButtonType.NORMAL }) .onClick(async (_e, res) { if (res SaveButtonOnClickResult.SUCCESS this.snapshotPm) { await this.saveToAlbum(this.snapshotPm); } }) }.padding(16) } .backgroundColor(Color.White) .borderRadius({ topLeft:24, topRight:24 }) } .width(100%) .height(100%) .position({ x:0,y:0 }) } }四、避坑指南问题原因修复截图只截到首屏componentSnapshot.get()只截组件当前渲染像素用Scroller分段滚 多次get()长图有重复段落每次截全可视区直接拼接PixelMap.crop()只保留新增滚动部分参考getCropArea保存时报权限拒绝普通 Button 调 MediaLibrary 写文件用SaveButton安全控件系统弹授权框Web图文详情截不全未启用全页绘制调webview.WebviewController.enableWholeWebPageDrawing() 等onPageEnd后截图滚屏截到动画残影scrollTo动画未完成即截图await sleep(300~500ms)等渲染完再componentSnapshot.get()五、总结AI卡片快照分享SOP给截图区组件设唯一id包在Scroll内滚回顶部​ →componentSnapshot.get(id)截首屏 →crop存首段循环​scrollTo(y可视高) → sleep → get → crop新增区 → push直到isAtEnd()合并createPixelMapSync 按序writePixelsSync(area)预览 SaveButtonphotoAccessHelper.createAssetImagePacker.packToDatafileIo.write核心法则HarmonyOS 6 中富媒体卡片长截图分享 componentSnapshot分段截 PixelMap.crop去重拼接 SaveButton安全落盘Web详情额外开enableWholeWebPageDrawing()。©著作权归作者所有如需转载请注明出处否则将追究法律责任。