熟悉我们购物比价应用的朋友一定知道,商城App里的分享功能有多重要。用户看到心仪的商品,想发给朋友砍一刀;抢到了限时优惠券,想晒到朋友圈炫耀一下;甚至想把整页的促销攻略截成长图分享到群里。这些场景都离不开系统分享能力。

但问题来了:我们辛辛苦苦实现了分享功能,用户一点击“分享”,弹出了应用选择列表,选了个微信或者备忘录,结果弹出一个提示——“您选择的文件不支持分享”。用户一脸懵,我们也一脸懵。明明代码里传了uri和content,怎么就“不支持”了呢?

我们之前就在这个坑里爬了好久。后来仔细研究了华为官方文档和社区的一些分享实践,终于搞清楚了原因,并且找到了一个更优雅的解决方案——结合快照分享技术,让商城应用的分享功能既稳定又好用。这篇文章完整记录一下实现过程和踩坑经验。

功能设计

先说说预期效果。

用户在商城应用的不同页面,点击“分享”按钮,能够将当前内容(商品详情、订单截图、优惠券等)通过系统分享发送给好友或保存到其他应用。具体要求:

  1. 文本分享:分享商品链接或文案,能正常发送到微信、备忘录等。

  2. 图片分享:分享商品图片或截图,不能提示“文件不支持”。

  3. 长图分享:分享整个商品详情页或攻略列表的长截图,能保存到相册或发送给好友。

  4. 兼容性:不同系统版本、不同目标应用都能正常工作。

核心目标:

  1. 搞清楚系统分享的content和uri参数到底该怎么用。

  2. 解决在线图片不能直接分享的问题。

  3. 实现一键生成商品详情长图并分享的能力。

核心API

API/组件

说明

systemShare.ShareController

系统分享控制器,发起分享操作

systemShare.SharedRecord

分享数据记录,包含content和uri

componentSnapshot.get()

组件截图,生成PixelMap

image.PixelMap

像素图,可保存为本地文件

photoAccessHelper

保存到相册

SaveButton

安全控件,保存图片到相册的必要组件

实现过程

分享数据构造的正确姿势

首先,我们得搞清楚为什么会出现“文件不支持分享”。官方文档说了几个关键点:

  1. content和uri至少有一个不为空。如果两个都为空,系统不知道你要分享什么。

  2. uri指向的文件必须存在且有权限访问。如果文件路径不对或者权限不足,就会报错。

  3. 在线图片不能直接分享。系统分享不支持直接分享网络图片,必须先下载到本地。

  4. 缩略图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滚动截图+拼接+保存本地+分享

错误排查

检查文件存在性、权限、数据量

改完之后,我们的分享功能终于稳定了。用户再也没遇到过“文件不支持分享”的提示,而且还能一键生成商品详情长图分享给朋友。如果你也在做购物比价类应用,不妨试试这套方案,少走一些弯路。

Logo

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

更多推荐