在 HarmonyOS NEXT 中,保存图片或视频到相册主要有三种方式,它们的权限模型和适用场景各不相同。下面我将为你详细拆解,并配合代码示例,帮助你理解并选择最合适的方案。

核心方法对比

特性 SaveButton(保存控件) PhotoViewPicker photoAccessHelper + 权限
权限模型 零权限,用户点击控件时临时授权,授权窗口仅弹一次。 零权限,拉起系统“保存到”界面,由用户选择保存位置。 需申请权限,需在module.json5中配置ohos.permission.WRITE_IMAGEVIDEO等权限,并动态请求。
交互方式 应用内控件,点击即存,无需用户选择保存路径。 拉起系统FilePicker界面,用户需二次确认保存路径。 静默保存,无额外交互(权限授予后)。
授权时效 授权有时效(如10秒),需在回调中完成核心操作。 无时效限制,用户确认后获得目标URI的读写权限。 权限长期有效(除非用户手动在设置中关闭)。
适用场景 首选。UI中可放置按钮,用户点击保存图片/小文件。体验最佳,是官方推荐做法。 需要用户明确知道文件保存位置,或进行“另存为”操作时。 需要后台自动保存、或处理大文件(如>2GB视频)时,用此方案配合分段写入更稳定。

方案一:使用 SaveButton 安全控件(推荐)

这是HarmonyOS NEXT主推的方式,能提供最流畅的用户体验,因为它无需应用提前申请任何存储权限,完全由用户点击控件的动作触发临时授权。

1. 核心逻辑与代码

首先,在UI中放置一个SaveButton。当用户点击它且授权成功后,在onClick回调里获取临时授权,并执行保存操作。

typescript

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// 保存图片的函数,在SaveButton的onClick中调用
async function savePhotoToGallery(context: common.UIAbilityContext, imageBuffer: ArrayBuffer) {
  let helper = photoAccessHelper.getPhotoAccessHelper(context);
  try {
    // 1. 在相册中创建一个图片文件,指定类型和格式
    // 注意:此操作需在用户点击SaveButton后的10秒内完成[citation:7]
    let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
    
    // 2. 打开文件并写入数据
    let file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
    await fileIo.write(file.fd, imageBuffer);
    await fileIo.close(file.fd);
    
    promptAction.showToast({ message: '已保存至相册!' });
  } catch (error) {
    const err: BusinessError = error as BusinessError;
    console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`);
  }
}

@Entry
@Component
struct SaveButtonDemo {
  build() {
    Row() {
      Column() {
        // 假设这是你要保存的图片
        Image($r('app.media.demoImage')).width(300).height(300)
        
        // 保存控件:用户点击即触发保存逻辑,无需任何权限申请
        SaveButton()
          .padding({ top: 12, bottom: 12, left: 24, right: 24 })
          .onClick(async (event, result) => {
            if (result === SaveButtonOnClickResult.SUCCESS) {
              const context = getContext(this) as common.UIAbilityContext;
              // 这里需要将你的图片转换为ArrayBuffer
              // 例如从网络下载、从沙箱读取或从PixelMap编码
              // const imageBuffer: ArrayBuffer = await getImageBuffer();
              // await savePhotoToGallery(context, imageBuffer);
            } else {
              promptAction.showToast({ message: '授权失败' });
            }
          })
      }
    }
  }
}

方案二:使用 PhotoViewPicker

这种方案不涉及权限申请,但会拉起系统选择器,让用户自己决定保存位置,适合“另存为”的场景。

1. 核心逻辑与代码

typescript

import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';

async function saveWithPicker() {
  const photoSaveOptions = new picker.PhotoSaveOptions();
  // 可指定一个建议的文件名
  photoSaveOptions.newFileNames = ["MyImage.jpg"]; 
  const photoViewPicker = new picker.PhotoViewPicker();
  
  try {
    // 拉起系统保存界面,用户选择文件夹后返回保存的URI
    const photoSaveResult = await photoViewPicker.save(photoSaveOptions);
    const uri = photoSaveResult[0]; 
    console.info('Save URI is: ' + uri);

    // 拿到URI后,可以像操作普通文件一样写入数据
    let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
    // 这里写入你的图片数据
    // let writeLen = fs.writeSync(file.fd, 'your image data');
    fs.closeSync(file);
  } catch (err) {
    console.error(`Save failed, code is ${err.code}, message is ${err.message}`);
  }
}

方案三:申请权限 + photoAccessHelper

当需要应用自动、静默地保存文件时(例如自动保存聊天图片),就需要申请ohos.permission.WRITE_IMAGEVIDEO权限。这是一个系统级权限,需要用户在弹窗中授权。

1. 配置权限

module.json5中添加权限声明:

json

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:reason_for_saving_photos",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

2. 动态申请权限并保存

typescript

import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import fs from '@ohos.file.fs';

async function requestPermissionAndSave(context: common.UIAbilityContext, imageBuffer: ArrayBuffer) {
  const atManager = abilityAccessCtrl.createAtManager();
  const permissions: Array<string> = ['ohos.permission.WRITE_IMAGEVIDEO'];
  try {
    // 1. 动态申请权限
    const result = await atManager.requestPermissionsFromUser(context, permissions);
    if (result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      // 2. 权限已授权,执行保存
      let helper = photoAccessHelper.getPhotoAccessHelper(context);
      let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
      let file = fs.openSync(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      fs.writeSync(file.fd, imageBuffer);
      fs.closeSync(file);
      promptAction.showToast({ message: '保存成功' });
    }
  } catch (err) {
    console.error(`Request permission failed, error: ${err.code}`);
  }
}

视频保存与“大文件”注意事项

  • 方法通用:上述三种方法均适用于视频保存。只需将PhotoType.IMAGE改为PhotoType.VIDEO,文件后缀改为.mp4即可。

  • 大文件处理:对于体积较大的视频(如超过2GB),使用SaveButton可能会因授权窗口期(10秒)不够用而导致保存失败。

    • 解决方案:对于此类场景,推荐使用方案三(申请权限模式)。在获得长期权限后,可以通过fs.openSync等API进行分段写入,稳定地将大文件保存到相册。

总结与建议

  1. 首选 SaveButton:对于绝大多数保存图片和小视频的场景,这是最佳实践,提供了最友好的用户体验。

  2. 需要用户确认位置时选 PhotoViewPicker:当你需要用户明确知道文件存到了哪里,或者提供“另存为”功能时,使用Picker。

  3. 后台自动保存或处理超大文件选 photoAccessHelper + 权限:这种方式虽然需要申请权限,但提供了最灵活和持久的访问能力,适合复杂或后台任务。

Logo

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

更多推荐