第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 = '已采集画作,可以直接生成动画';
}
这种设计的好处:
- 统一状态管理 :无论图片来源如何,
hasPhoto、activeStep、generationProgress等状态均为一致 - 区分来源 :
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.Cover或ImageFit.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 中实现完整的绘画体验。