HarmonyOS APP《画伴梦工厂》开发第10篇:相册选择与 PhotoViewPicker——从相册导入图片

第2.2篇:相册选择与 PhotoViewPicker------从相册导入图片

难度 :⭐⭐ 进阶

前置知识 :第 2.1 篇 相机开发实战

涉及源文件products/default/src/main/ets/components/CreationComponents.ets


概述

第 2.1 篇我们学习了如何调用系统相机拍照。本篇将介绍另一种图片采集方式------通过 PhotoViewPicker 从相册中选择图片。与相机模式相比,相册选择不需要繁重的 CAMERA 权限,甚至 不需要 READ_MEDIA 权限 ,这得益于 HarmonyOS 的 Scope(作用域)访问模型。两者最终共用同一套 capturePhoto 处理流程,保持了代码的高度复用。


一、相机 vs 相册:场景对比

对比维度 相机拍照 相册选择
权限需求 ohos.permission.CAMERA 无需额外权限(Scope 访问)
用户操作 打开相机 → 按快门 → 确认 打开相册 → 点选图片 → 确认
返回数据 AbilityResult 需多层解析 PhotoSelectResult.photoUris 直接获取
适用场景 用户手中有纸质画作 用户之前拍好 / 截好的图片
交互链路 较长,涉及系统相机界面 较短,Picker 浮层直接选择
数据安全性 完整文件路径 Scope URI,应用只能访问选中文件

二、PhotoViewPicker 的基本使用

2.1 权限检查

与相机不同,相册选择不需要 READ_MEDIA 权限。PermissionGuard.requestAlbum 的实现非常简洁------直接返回授权成功:

typescript 复制代码
// PermissionGuard.ets
static async requestAlbum(context: common.UIAbilityContext): Promise<PermissionResult> {
  // PhotoViewPicker grants scoped access to the selected media item.
  // Do not declare broad media-read permissions here; some devices reject them at install time.
  return {
    granted: true,
    message: ''
  };
}

设计理念PhotoViewPicker 采用 Scope 访问模式。用户通过 Picker 选中的图片,应用仅获得该特定文件的访问权限,而不是整个媒体库的读取权限。这既保护了用户隐私,又简化了开发者的权限管理工作。

2.2 创建并配置 PhotoSelectOptions

typescript 复制代码
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
photoSelectOptions.isPhotoTakingSupported = false;

配置参数说明:

参数 含义
MIMEType PhotoViewMIMETypes.IMAGE_TYPE 只显示图片文件,过滤视频和其他类型
maxSelectNumber 1 单选模式,一次只选一张图
isPhotoTakingSupported false 不显示内置的拍照入口,保持交互简洁

2.3 调用 select 方法

typescript 复制代码
const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
photoViewPicker.select(photoSelectOptions).then((result: photoAccessHelper.PhotoSelectResult) => {
  if (result.photoUris.length === 0) {
    this.noticeText = '未选择图片,请重新从相册导入';
    return;
  }
  this.capturePhoto(result.photoUris[0], '相册图片');
  this.noticeText = '相册导入完成,已用选中图片覆盖当前预览';
}).catch(() => {
  this.noticeText = '相册选择失败,请重新从相册导入';
});
返回结果处理

PhotoSelectResult 的结构:

typescript 复制代码
interface PhotoSelectResult {
  photoUris: string[];   // 选中图片的 URI 数组
  isOriginalPhoto: boolean; // 是否为原图(可选字段)
}
  • result.photoUris[0] :因为配置了 maxSelectNumber = 1,数组至多一个元素,直接取第一个。
  • 空数组检查:用户可能取消选择或从选择界面返回,需要处理这种情况。
  • 错误处理catch 分支处理系统 Picker 异常(如相册无法打开)。

三、openAlbum 完整方法

将以上步骤组合,得到 openAlbum 的完整实现:

typescript 复制代码
private async openAlbum() {
  const context = getContext(this) as common.UIAbilityContext;
  const permissionResult: PermissionResult = await PermissionGuard.requestAlbum(context);
  if (!permissionResult.granted) {
    this.noticeText = permissionResult.message;
    return;
  }
  const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
  photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
  photoSelectOptions.maxSelectNumber = 1;
  photoSelectOptions.isPhotoTakingSupported = false;

  const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
  photoViewPicker.select(photoSelectOptions).then((result: photoAccessHelper.PhotoSelectResult) => {
    if (result.photoUris.length === 0) {
      this.noticeText = '未选择图片,请重新从相册导入';
      return;
    }
    this.capturePhoto(result.photoUris[0], '相册图片');
    this.noticeText = '相册导入完成,已用选中图片覆盖当前预览';
  }).catch(() => {
    this.noticeText = '相册选择失败,请重新从相册导入';
  });
}

四、与相机共用同一套处理流程

无论图片来自相机还是相册,都通过 capturePhoto 方法统一处理:

typescript 复制代码
private capturePhoto(uri: string = '', sourceLabel: string = '拍照图片') {
  if (uri !== '') {
    this.capturedImageUri = uri;
    this.imageSourceLabel = sourceLabel;  // 标记来源:'拍照图片' 或 '相册图片'
  }
  this.hasPhoto = true;
  this.activeStep = 1;
  this.generationProgress = 35;
  this.noticeText = '已采集画作,可以直接生成动画';
}

这种设计的好处:

  • 统一状态管理 :无论图片来源如何,hasPhotoactiveStepgenerationProgress 等状态均为一致
  • 区分来源imageSourceLabel 记录了来源信息,UI 层可以展示"已使用拍照图片"或"已使用相册图片"
  • 降低复杂度 :后续的生成流程不需要关心图片从哪来,只需使用 capturedImageUri

五、UI 层触发

在界面上,用户通过"相册导入"按钮触发:

typescript 复制代码
Button('相册导入')
  .height(46)
  .fontSize(14)
  .fontWeight(FontWeight.Bold)
  .fontColor('#FFFFFF')
  .backgroundColor(this.mint)        // #42CDA3 绿色按钮
  .borderRadius(23)
  .layoutWeight(1)
  .margin({ left: 10 })
  .onClick(() => {
    if (!this.recognizing) {
      this.openAlbum();
    }
  })

"拍照采集"和"相册导入"两个按钮并排显示在画作预览区下方,用户可以根据手头情况自由选择:

复制代码
┌────────────────────────────────┐
│     [ 拍照采集 ]  [ 相册导入 ]    │
└────────────────────────────────┘

六、完整流程时序图

复制代码
用户点击"相册导入"
    │
    ▼
PermissionGuard.requestAlbum() → 直接返回 granted
    │
    ▼
new PhotoViewPicker().select(options)
    │
    ├── 用户取消 → noticeText 提示 → 结束
    │
    └── 用户选择图片
          │
          ▼
    result.photoUris[0] 获取 URI
          │
          ▼
    capturePhoto(uri, '相册图片')
          │
          ▼
    hasPhoto = true
    activeStep = 1
    generationProgress = 35
    imageSourceLabel = '相册图片'

七、常见问题排查

7.1 图片未显示

  • 确认 capturedImageUri 是否赋值正确
  • 检查 URI 格式,PhotoViewPicker 返回的 URI 可直接用于 Image 组件的 src 属性
  • 确认 Image 组件设置了合适的 objectFit(如 ImageFit.CoverImageFit.Contain

7.2 相册打不开

  • 在真机上测试(模拟器相册功能可能受限)
  • 检查 @kit.MediaLibraryKit 是否已在模块中正确引入

7.3 Scope URI 与文件路径

  • PhotoViewPicker 返回的 URI 是 Scope URI,不能 直接用于 fileIo 等文件操作
  • 如需文件操作,需通过 fileIo.fopen 或沙箱路径转换
  • 在本项目中,URI 直接传给 Image 组件和后续 RecognitionWaitingPage 展示,无需文件级操作

总结

本文介绍了使用 PhotoViewPicker 从相册选择图片的方案,与第 2.1 篇的相机拍照形成互补:

知识点 实现方式
Picker 配置 PhotoSelectOptions 设置 MIME、选择数、拍照按钮
调用方式 PhotoViewPicker.select() 返回 Promise
结果获取 PhotoSelectResult.photoUris 直接获取
权限模型 Scope 访问,无需 READ_MEDIA 权限
流程复用 与相机共用 capturePhoto 统一处理

两种图片采集方式覆盖了用户不同的使用场景------手中有纸质画作用相机拍,手机里已有图片从相册选。下一节我们将进入自由涂鸦模式,学习如何在 ArkUI 中实现完整的绘画体验。