HarmonyOS 6商城开发学习:分享功能的“文件不支持”陷阱与快照分享实战
熟悉我们购物比价应用的朋友一定知道,商城App里的分享功能有多重要。用户看到心仪的商品,想发给朋友砍一刀;抢到了限时优惠券,想晒到朋友圈炫耀一下;甚至想把整页的促销攻略截成长图分享到群里。这些场景都离不开系统分享能力。
但问题来了:我们辛辛苦苦实现了分享功能,用户一点击“分享”,弹出了应用选择列表,选了个微信或者备忘录,结果弹出一个提示——“您选择的文件不支持分享”。用户一脸懵,我们也一脸懵。明明代码里传了uri和content,怎么就“不支持”了呢?
我们之前就在这个坑里爬了好久。后来仔细研究了华为官方文档和社区的一些分享实践,终于搞清楚了原因,并且找到了一个更优雅的解决方案——结合快照分享技术,让商城应用的分享功能既稳定又好用。这篇文章完整记录一下实现过程和踩坑经验。
功能设计
先说说预期效果。
用户在商城应用的不同页面,点击“分享”按钮,能够将当前内容(商品详情、订单截图、优惠券等)通过系统分享发送给好友或保存到其他应用。具体要求:
-
文本分享:分享商品链接或文案,能正常发送到微信、备忘录等。
-
图片分享:分享商品图片或截图,不能提示“文件不支持”。
-
长图分享:分享整个商品详情页或攻略列表的长截图,能保存到相册或发送给好友。
-
兼容性:不同系统版本、不同目标应用都能正常工作。
核心目标:
-
搞清楚系统分享的content和uri参数到底该怎么用。
-
解决在线图片不能直接分享的问题。
-
实现一键生成商品详情长图并分享的能力。
核心API
|
API/组件 |
说明 |
|---|---|
|
|
系统分享控制器,发起分享操作 |
|
|
分享数据记录,包含content和uri |
|
|
组件截图,生成PixelMap |
|
|
像素图,可保存为本地文件 |
|
|
保存到相册 |
|
|
安全控件,保存图片到相册的必要组件 |
实现过程
分享数据构造的正确姿势
首先,我们得搞清楚为什么会出现“文件不支持分享”。官方文档说了几个关键点:
-
content和uri至少有一个不为空。如果两个都为空,系统不知道你要分享什么。
-
uri指向的文件必须存在且有权限访问。如果文件路径不对或者权限不足,就会报错。
-
在线图片不能直接分享。系统分享不支持直接分享网络图片,必须先下载到本地。
-
缩略图thumbnail限制32KB。太大的缩略图会导致want数据超限,无法拉起分享。
我们先封装一个安全的分享工具类:
// utils/SafeShareUtil.ets
import { systemShare } from '@kit.ArkShare';
import { fileIo } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@ohos.base';
export class SafeShareUtil {
/**
* 分享文本内容
*/
static async shareText(title: string, content: string, link?: string) {
const sharedData: systemShare.SharedData = {
title: title,
contentType: systemShare.ContentType.TEXT,
content: link ? `${content}\n${link}` : content
};
try {
const controller = await systemShare.createShareController();
await controller.share(sharedData);
} catch (error) {
console.error(`分享文本失败: ${JSON.stringify(error)}`);
}
}
/**
* 分享本地图片文件
* @param localUri 本地文件uri,必须是已存在的文件
*/
static async shareImage(localUri: string, title?: string) {
// 确认文件存在
try {
fileIo.statSync(localUri);
} catch {
console.error(`文件不存在: ${localUri}`);
return;
}
const sharedData: systemShare.SharedData = {
title: title || '分享图片',
contentType: systemShare.ContentType.IMAGE,
uri: localUri, // 关键:图片必须用uri,不能用content
thumbnail: undefined // 缩略图先不传,避免超限
};
try {
const controller = await systemShare.createShareController();
await controller.share(sharedData);
} catch (error) {
console.error(`分享图片失败: ${JSON.stringify(error)}`);
}
}
/**
* 分享在线图片(先下载到本地再分享)
*/
static async shareOnlineImage(imageUrl: string, title?: string) {
// 1. 下载到本地缓存目录
const localPath = await this.downloadImageToCache(imageUrl);
if (!localPath) {
console.error('下载图片失败');
return;
}
// 2. 分享本地文件
await this.shareImage(localPath, title);
}
/**
* 下载图片到缓存目录
*/
private static async downloadImageToCache(url: string): Promise<string | null> {
try {
const context = getContext() as Context;
const cacheDir = context.cacheDir;
const fileName = `share_${Date.now()}.jpg`;
const filePath = `${cacheDir}/${fileName}`;
// 使用http下载(简化写法,实际需要完整请求)
// 这里省略具体下载代码,假设下载成功
// 下载完成后返回filePath
return filePath;
} catch (error) {
console.error(`下载图片失败: ${JSON.stringify(error)}`);
return null;
}
}
}
解决“文件不支持分享”的常见问题
根据官方文档的FAQ,我们总结了以下几个排查步骤:
// 排查函数
function diagnoseShareFailure(error: BusinessError) {
// 错误码1003703001:数据记录数量超过限制
if (error.code === 1003703001) {
console.warn('数据记录超过限制,建议不使用getSharedData,直接从want.uri解析');
return '数据量过大,请简化分享内容';
}
// 其他错误,检查文件和权限
console.error(`分享失败: code=${error.code}, message=${error.message}`);
return '分享失败,请检查文件是否存在或联系客服';
}
官方文档提到几个关键点,我们整理成表格方便自查:
|
问题 |
原因 |
解决方案 |
|---|---|---|
|
提示“文件不支持分享” |
content和uri都为空 |
确保至少一个不为空 |
|
分享在线图片失败 |
系统不支持直接分享网络图片 |
先下载到本地,再用uri分享 |
|
缩略图设置无效 |
图片大小超过32KB |
使用ImagePacker压缩到32KB以下 |
|
报错1003703001 |
数据记录数量超限 |
不从getSharedData获取,直接从want.uri解析 |
|
文件存在但仍提示不支持 |
文件路径或权限问题 |
确认文件可读,使用正确的沙箱路径 |
快照分享:一键生成商品详情长图
单纯的图片分享还不够,用户经常想把整个商品详情页分享给朋友。传统做法是截屏,但商品详情页很长,一张屏幕截不全。我们就想到了快照分享技术——自动滚动截图,拼接成长图,然后分享。
这部分我们参考了社区的快照分享方案,结合商城场景做了适配。
// view/ProductDetailShare.ets
import { componentSnapshot } from '@ohos.componentSnapshot';
import { image } from '@kit.ImageKit';
import { SafeShareUtil } from '../utils/SafeShareUtil';
@Component
export struct ProductDetailShare {
private scroller: Scroller = new Scroller();
@State curYOffset: number = 0;
@State scrollYOffsets: number[] = [];
@State areaArray: image.PositionArea[] = [];
@State mergedImage: PixelMap | undefined;
@State isGenerating: boolean = false;
private listId: string = 'product_detail_scroll';
// 一键生成并分享长图
async generateAndShareLongImage() {
if (this.isGenerating) return;
this.isGenerating = true;
try {
// 1. 滚动到顶部
this.scroller.scrollTo({ yOffset: 0, animation: { duration: 200 } });
await this.sleep(200);
// 2. 开始滚动截图
this.scrollYOffsets = [];
this.areaArray = [];
await this.snapAndMerge();
// 3. 保存到临时文件
if (this.mergedImage) {
const localUri = await this.savePixelMapToCache(this.mergedImage);
if (localUri) {
// 4. 分享本地图片
await SafeShareUtil.shareImage(localUri, '商品详情');
}
}
} catch (error) {
console.error(`生成长图失败: ${JSON.stringify(error)}`);
} finally {
this.isGenerating = false;
}
}
async snapAndMerge() {
this.scrollYOffsets.push(this.curYOffset);
// 截图
const pixelMap = await this.getUIContext().getComponentSnapshot().get(this.listId);
// 裁剪出新增部分
let area = await this.getSnapshotArea(pixelMap);
this.areaArray.push(area);
// 判断是否到底
if (!this.scroller.isAtEnd()) {
// 继续滚动
this.scroller.scrollBy({ yOffset: 200 });
await this.sleep(200);
await this.snapAndMerge();
} else {
// 合并所有截图
this.mergedImage = await this.mergeImages();
}
}
// 裁剪新增区域(省略具体实现,参考社区文章)
async getSnapshotArea(pixelMap: PixelMap): Promise<image.PositionArea> {
// ...
return {} as image.PositionArea;
}
// 合并所有截图(省略具体实现)
async mergeImages(): Promise<PixelMap> {
// ...
return {} as PixelMap;
}
// 保存PixelMap到缓存目录
async savePixelMapToCache(pixelMap: PixelMap): Promise<string | null> {
try {
const context = getContext() as Context;
const cacheDir = context.cacheDir;
const fileName = `long_share_${Date.now()}.png`;
const filePath = `${cacheDir}/${fileName}`;
const imagePacker = image.createImagePacker();
const packOpts = { format: 'image/png', quality: 90 };
const data = await imagePacker.packToData(pixelMap, packOpts);
const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
fileIo.writeSync(file.fd, data);
fileIo.closeSync(file.fd);
return filePath;
} catch (error) {
console.error(`保存图片到缓存失败: ${JSON.stringify(error)}`);
return null;
}
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
build() {
Column() {
// 商品详情列表
List({ scroller: this.scroller }) {
ForEach(this.productSections, (section: ProductSection) => {
ListItem() {
// 商品详情各部分内容
this.buildSection(section);
}
})
}
.id(this.listId)
.onScrollIndex((start, end) => {
this.curYOffset = this.scroller.currentOffset().yOffset;
})
// 分享按钮
Button('分享商品')
.width('90%')
.height(48)
.backgroundColor('#FF5500')
.fontColor(Color.White)
.margin({ top: 16 })
.onClick(() => this.generateAndShareLongImage())
}
}
}
分享时的缩略图优化
官方文档说缩略图不能超过32KB,否则会导致分享失败。我们在分享图片时,如果携带缩略图,需要先压缩。
// 压缩缩略图
async compressThumbnail(pixelMap: PixelMap): Promise<ArrayBuffer> {
const imagePacker = image.createImagePacker();
const packOpts = {
format: 'image/jpeg',
quality: 50 // 降低质量以缩小体积
};
let data = await imagePacker.packToData(pixelMap, packOpts);
// 如果还是太大,继续降低质量
let quality = 50;
while (data.byteLength > 32 * 1024 && quality > 10) {
quality -= 10;
packOpts.quality = quality;
data = await imagePacker.packToData(pixelMap, packOpts);
}
return data;
}
遇到的问题与解决方案
问题1:分享在线图片一直提示“文件不支持”
一开始我们直接把网络图片的url传给uri,结果当然不行。后来才知道系统分享不支持在线图片,必须先下载到本地沙箱目录,再用本地uri分享。
问题2:分享成功后目标应用显示空白
这是因为我们传了content但没传uri,或者反过来。官方文档说得很清楚:如果是图片,必须用uri;如果是文本,用content。我们一开始混用了,导致微信收到了空内容。
问题3:长截图拼接后出现重复内容
滚动截图时,每次截取的图片有一部分和前一张重叠(因为滚动距离不够)。解决方案:只保留新增的滚动部分,用PixelMap.crop()裁剪掉重叠区域。社区文章里给出了具体的计算方法。
问题4:分享大图时拉起分享面板很慢
原因是图片太大,want数据超限。解决方案:压缩图片质量,或者在分享时不传缩略图(thumbnail设为undefined),让目标应用自己去加载。
总结
商城应用的分享功能看似简单,实则暗藏不少坑。核心要点总结如下:
|
要点 |
实现方式 |
|---|---|
|
分享文本 |
content传文本,uri可为空 |
|
分享本地图片 |
uri传文件路径,content可为空 |
|
分享在线图片 |
先下载到本地缓存,再用uri分享 |
|
缩略图限制 |
压缩到32KB以下,或干脆不传 |
|
长图分享 |
componentSnapshot滚动截图+拼接+保存本地+分享 |
|
错误排查 |
检查文件存在性、权限、数据量 |
改完之后,我们的分享功能终于稳定了。用户再也没遇到过“文件不支持分享”的提示,而且还能一键生成商品详情长图分享给朋友。如果你也在做购物比价类应用,不妨试试这套方案,少走一些弯路。
更多推荐
所有评论(0)