本文将详细介绍如何在鸿蒙应用中实现将UI组件保存为图片并存储到相册的功能,通过componentSnapshot和photoAccessHelper等核心API,为用户提供便捷的分享体验。

功能概述

在现代移动应用中,分享功能是提升用户活跃度和传播性的重要特性。我们为"往来记"应用实现了分享卡片保存为图片功能,用户可以将精美的年度报告和记录详情保存到手机相册,方便分享到社交媒体或留作纪念。

技术架构

核心API介绍

1. componentSnapshot - 组件截图

// 获取组件截图
const pixelMap = await componentSnapshot.get(componentId);

2. image.ImagePacker - 图片打包

// 创建图片打包器
const imagePackerApi = image.createImagePacker();
// 打包为PNG格式
const imageData = await imagePackerApi.packing(pixelMap, packOpts);

3. photoAccessHelper - 相册访问

// 获取相册助手
const helper = photoAccessHelper.getPhotoAccessHelper(context);
// 创建相册资源
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png', options);

权限配置

module.json5中配置必要的相册访问权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "需要保存分享图片到相册",
        "usedScene": {
          "when": "inuse",
          "abilities": ["EntryAbility"]
        }
      },
      {
        "name": "ohos.permission.READ_IMAGEVIDEO", 
        "reason": "需要读取图片以支持分享功能",
        "usedScene": {
          "when": "inuse",
          "abilities": ["EntryAbility"]
        }
      }
    ]
  }
}

实现步骤

步骤1:组件标识设置

首先为需要截图的分享卡片组件设置唯一ID:

@Component
struct ShareCard {
  build() {
    Column() {
      // 分享卡片内容
      this.buildCardContent()
    }
    .width('90%')
    .backgroundColor(Color.White)
    .borderRadius(16)
    .padding(20)
    .id('annualReportShareCard') // 设置组件ID
  }
}

步骤2:实现截图保存功能

创建通用的图片保存服务:

// ShareCardService.ets
export class ShareCardService {
  /**
   * 保存组件截图到相册
   * @param componentId 组件ID
   * @param fileName 文件名称
   */
  async saveComponentToAlbum(componentId: string, fileName: string): Promise<boolean> {
    try {
      // 1. 等待UI渲染完成
      await this.delay(500);
      
      // 2. 获取组件截图
      const pixelMap = await componentSnapshot.get(componentId);
      if (!pixelMap) {
        promptAction.showToast({ message: '截图失败,请重试' });
        return false;
      }
      
      // 3. 打包为PNG图片
      const imagePackerApi = image.createImagePacker();
      const packOpts: image.PackingOption = {
        format: 'image/png',
        quality: 100
      };
      const imageData = await imagePackerApi.packing(pixelMap, packOpts);
      
      // 4. 保存到相册
      const result = await this.saveToPhotoAlbum(imageData, fileName);
      
      if (result) {
        promptAction.showToast({ message: '图片已保存到相册' });
      }
      
      return result;
    } catch (error) {
      logger.error('保存图片失败: ' + JSON.stringify(error));
      promptAction.showToast({ message: '保存失败,请检查相册权限' });
      return false;
    }
  }
  
  /**
   * 保存图片数据到相册
   */
  private async saveToPhotoAlbum(imageData: ArrayBuffer, fileName: string): Promise<boolean> {
    const context = getContext(this);
    
    // 1. 先写入应用缓存目录
    const tempFilePath = context.cacheDir + `/${fileName}_${Date.now()}.png`;
    const file = await fileIo.open(tempFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
    await fileIo.write(file.fd, imageData);
    await fileIo.close(file);
    
    // 2. 保存到系统相册
    try {
      const helper = photoAccessHelper.getPhotoAccessHelper(context);
      const options = photoAccessHelper.createOptions();
      options.title = fileName;
      
      const uri = await helper.createAsset(
        photoAccessHelper.PhotoType.IMAGE,
        'png',
        options
      );
      
      // 3. 复制文件到相册
      const sourceFile = await fileIo.open(tempFilePath, fileIo.OpenMode.READ_ONLY);
      const destFile = await fileIo.open(uri, fileIo.OpenMode.WRITE_ONLY);
      
      await fileIo.copyFile(sourceFile.fd, destFile.fd);
      
      await fileIo.close(sourceFile);
      await fileIo.close(destFile);
      
      // 4. 清理临时文件
      await fileIo.unlink(tempFilePath);
      
      return true;
    } catch (error) {
      logger.error('保存到相册失败: ' + JSON.stringify(error));
      return false;
    }
  }
  
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

步骤3:在页面中使用

年度报告页面示例:

// AnnualReportPage.ets
@Entry
@Component
struct AnnualReportPage {
  private shareCardService: ShareCardService = new ShareCardService();
  
  build() {
    Column() {
      // 页面内容...
      
      // 分享卡片弹窗
      if (this.showShareDialog) {
        this.buildShareCardDialog()
      }
    }
  }
  
  @Builder
  buildShareCardDialog() {
    Column() {
      // 分享卡片内容
      ShareCard()
      
      Row() {
        Button('保存图片')
          .onClick(() => {
            this.saveShareImage();
          })
          
        Button('复制文本')
          .onClick(() => {
            this.copyShareText();
          })
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width('100%')
      .margin({ top: 20 })
    }
  }
  
  /**
   * 保存分享图片
   */
  private async saveShareImage() {
    promptAction.showToast({ message: '正在生成图片...' });
    
    const success = await this.shareCardService.saveComponentToAlbum(
      'annualReportShareCard',
      `annual_report_${new Date().getFullYear()}`
    );
    
    if (success) {
      // 保存成功后的处理
      this.showShareDialog = false;
    }
  }
}

添加记录页面示例:

// AddRecordPage.ets
@Entry  
@Component
struct AddRecordPage {
  private shareCardService: ShareCardService = new ShareCardService();
  
  build() {
    Column() {
      // 表单内容...
      
      // 成功提示弹窗
      if (this.showSuccessDialog) {
        this.buildSuccessDialog()
      }
    }
  }
  
  @Builder
  buildSuccessDialog() {
    Column() {
      // 成功图标和文字
      this.buildSuccessHeader()
      
      // 记录详情卡片
      RecordDetailCard()
        .id('recordShareCard')
      
      // 操作按钮
      Row() {
        Button('保存图片')
          .onClick(() => {
            this.saveRecordImage();
          })
          
        Button('分享文本')  
          .onClick(() => {
            this.shareRecordText();
          })
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width('100%')
      .margin({ top: 16 })
      
      Button('完成')
        .onClick(() => {
          this.showSuccessDialog = false;
          router.back();
        })
        .margin({ top: 12 })
    }
  }
  
  /**
   * 保存记录图片
   */
  private async saveRecordImage() {
    promptAction.showToast({ message: '正在生成图片...' });
    
    const success = await this.shareCardService.saveComponentToAlbum(
      'recordShareCard',
      `record_${Date.now()}`
    );
    
    if (success) {
      this.showSuccessDialog = false;
      router.back();
    }
  }
}

步骤4:权限请求处理

在EntryAbility中处理权限请求:

// EntryAbility.ets
export default class EntryAbility extends UIAbility {
  async onWindowStageCreate(windowStage: window.WindowStage) {
    // 请求相册权限
    await this.requestPhotoPermissions();
    
    // 其他初始化代码...
  }
  
  /**
   * 请求相册权限
   */
  private async requestPhotoPermissions() {
    try {
      const atManager = abilityAccessCtrl.createAtManager();
      const permissions = [
        'ohos.permission.WRITE_IMAGEVIDEO',
        'ohos.permission.READ_IMAGEVIDEO'
      ];
      
      const result = await atManager.requestPermissionsFromUser(
        this.context, 
        permissions
      );
      
      logger.info('权限请求结果: ' + JSON.stringify(result));
    } catch (error) {
      logger.error('权限请求失败: ' + JSON.stringify(error));
    }
  }
}

用户体验优化

1. 加载状态提示

在图片生成过程中显示加载提示:

private async saveShareImage() {
  // 显示加载提示
  promptAction.showToast({ message: '正在生成图片...', duration: 1000 });
  
  try {
    const success = await this.shareCardService.saveComponentToAlbum(
      'annualReportShareCard',
      `annual_report_${new Date().getFullYear()}`
    );
    
    if (success) {
      promptAction.showToast({ message: '图片已保存到相册' });
    }
  } catch (error) {
    promptAction.showToast({ message: '保存失败,请重试' });
  }
}

2. 错误处理机制

完善的错误处理确保用户体验:

private async saveComponentToAlbum(componentId: string, fileName: string): Promise<boolean> {
  try {
    // 主要逻辑...
  } catch (error) {
    logger.error(`保存图片失败: ${JSON.stringify(error)}`);
    
    // 根据错误类型给出具体提示
    if (error.code === 201) {
      promptAction.showToast({ message: '权限不足,请在设置中开启相册权限' });
    } else if (error.code === 13900001) {
      promptAction.showToast({ message: '存储空间不足,请清理后重试' });
    } else {
      promptAction.showToast({ message: '保存失败,请重试' });
    }
    
    return false;
  }
}

性能优化建议

1. 图片质量与大小平衡

// 根据需求调整图片质量
const packOpts: image.PackingOption = {
  format: 'image/png',
  quality: 100  // 高质量,文件较大
};

// 或者选择JPEG格式减小文件大小
const packOpts: image.PackingOption = {
  format: 'image/jpeg', 
  quality: 85  // 良好质量,文件较小
};

2. 内存管理

及时释放资源避免内存泄漏:

private async saveComponentToAlbum(componentId: string, fileName: string): Promise<boolean> {
  let pixelMap: image.PixelMap | null = null;
  let sourceFile: fileIo.File | null = null;
  let destFile: fileIo.File | null = null;
  
  try {
    pixelMap = await componentSnapshot.get(componentId);
    // ...其他逻辑
    
    return true;
  } catch (error) {
    // 错误处理...
    return false;
  } finally {
    // 释放资源
    if (pixelMap) {
      pixelMap.release();
    }
    if (sourceFile) {
      await fileIo.close(sourceFile);
    }
    if (destFile) {
      await fileIo.close(destFile);
    }
  }
}

兼容性考虑

设备适配

确保功能在不同设备上正常工作:

private getComponentId(): string {
  // 根据设备类型调整组件ID
  const deviceType = deviceInfo.deviceType;
  
  if (deviceType === 'tablet' || deviceType === '2in1') {
    return 'annualReportShareCard_tablet';
  } else {
    return 'annualReportShareCard_phone';
  }
}

实际效果展示

年度报告分享卡片

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

添加记录分享卡片

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结

通过鸿蒙的componentSnapshot和photoAccessHelper API,我们成功实现了将UI组件保存为图片的功能。这个功能具有以下优势:

  1. 原生性能 - 使用系统API,截图和保存速度快
  2. 高质量输出 - 支持PNG无损格式,图片清晰
  3. 权限安全 - 遵循鸿蒙权限管理规范
  4. 用户体验好 - 操作简单,提示清晰
  5. 代码复用性高 - 封装成服务,多处可用

这个实现不仅适用于分享功能,还可以扩展到生成二维码、创建电子凭证等多种场景,为鸿蒙应用开发提供了有价值的参考。

扩展思考

未来可以进一步优化功能:

  1. 图片编辑 - 在保存前允许用户添加水印或文字
  2. 多图合成 - 将多个组件合并为一张长图
  3. 视频生成 - 将动态效果录制成视频分享
  4. 云端同步 - 将生成的图片自动备份到云端

希望本文对您的鸿蒙开发之旅有所帮助!

附:鸿蒙学习资源直达链接

https://developer.huawei.com/consumer/cn/training/classDetail/cfbdfcd7c53f430b9cdb92545f4ca010?type=1?ha_source=hmosclass&ha_sourceId=89000248

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐