37 相册读取与图片选择:PhotoViewPicker 实战

前言

在这里插入图片描述

图:37 相册读取与图片选择:PhotoViewPicker 实战 运行效果截图(HarmonyOS NEXT)

除了直接拍照,从相册选择已有图片也是常见的图片获取方式。鸿蒙提供了 PhotoViewPicker——一个系统级的图片选择器,让用户可以从相册中选择一张或多张图片。

本文以"鹿鹿·笔迹心理分析"项目中 [CapturePage.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/CapturePage.ets) 的 pickFromAlbum() 方法为例,深入解析 PhotoViewPicker 的使用方法和相册选择流程。

鸿蒙官方·PhotoViewPicker 文档:developer.huawei.com
项目源码仓库:harmony-app GitHub

PhotoViewPicker 相册选择流程

图:PhotoViewPicker 相册选择流程——创建选择器 → 配置选项 → 获取 URI

文件系统 系统相册 UI PhotoViewPicker CapturePage 文件系统 系统相册 UI PhotoViewPicker CapturePage new photoAccessHelper.PhotoViewPicker() select(PhotoSelectOptions) 打开系统相册界面 用户浏览并选择图片 返回选中图片 PhotoSelectResult { photoUris } 通过 URI 读取图片数据

一、PhotoViewPicker 基础

1.1 模块导入

import { photoAccessHelper } from '@kit.MediaLibraryKit'

1.2 基本使用

private async pickFromAlbum() {
  try {
    // ① 创建选择器
    const photoPicker = new photoAccessHelper.PhotoViewPicker()

    // ② 配置选项
    const pickerOptions: photoAccessHelper.PhotoSelectOptions = {
      maxSelectNumber: 1              // 最多选择 1 张
    }

    // ③ 打开选择器
    const result: photoAccessHelper.PhotoSelectResult =
      await photoPicker.select(pickerOptions)

    // ④ 处理结果
    if (result && result.photoUris && result.photoUris.length > 0) {
      this.previewPath = result.photoUris[0]
      this.hasPreview = true
      this.source = 'gallery'
    }
  } catch (error) {
    hilog.error(0x0000, TAG, '相册选择失败: %s', JSON.stringify(error))
  }
}

二、PhotoSelectOptions 配置

2.1 选项详解

interface PhotoSelectOptions {
  maxSelectNumber: number     // 最大选择数量
  MIME?: string[]            // 文件类型过滤
  isEdit?: boolean           // 是否允许编辑
}

2.2 实际配置

参数 本项目值 说明
maxSelectNumber 1 一次分析一张笔迹图片
MIME 未指定 默认所有图片类型
isEdit 未指定 选择后不编辑

2.3 扩展配置示例

// 多选 + MIME 过滤
const pickerOptions: photoAccessHelper.PhotoSelectOptions = {
  maxSelectNumber: 9,
  MIME: ['image/jpeg', 'image/png'],
}

三、PhotoSelectResult 结果处理

3.1 返回结构

interface PhotoSelectResult {
  photoUris: string[]   // 所选图片的 URI 列表
}

3.2 URI 的使用

result.photoUris[0]
// 返回类似:content://media/external/images/media/12345
// 或:/data/storage/el2/base/files/...

// 这个 URI 可以直接用于:
// 1. image.createImageSource(uri) — 解码图像
// 2. Image(uri) — ArkUI 图片组件展示
// 3. fs.openSync(uri) — 文件操作

四、拍照/相册双模式切换

4.1 模式切换策略

CapturePage 支持两种来源——拍照和相册,通过 source 参数控制:

aboutToAppear() {
  // 从路由参数读取来源
  const params = this.getUIContext().getRouter().getParams() as Record<string, Object>
  if (params && typeof params['source'] === 'string') {
    this.source = params['source'] as string
  }

  if (this.source === 'gallery') {
    this.pickFromAlbum()    // → 相册模式
  } else {
    this.initCamera()       // → 相机模式
  }
}

4.2 触发来源

// 跳转到 CapturePage — 拍照模式
this.getUIContext().getRouter().pushUrl({
  url: 'pages/CapturePage',
  params: { source: 'camera' }
})

// 跳转到 CapturePage — 相册模式
this.getUIContext().getRouter().pushUrl({
  url: 'pages/CapturePage',
  params: { source: 'gallery' }
})

4.3 UI 中的相册入口

在拍照页面底部,用户也可以点击相册按钮随时切换到相册选择:

// 拍照模式底栏中的相册按钮
Text('🖼')
  .fontSize(22)
  .fontColor('rgba(255,255,255,0.8)')
  .onClick(() => this.pickFromAlbum())

五、MediaLibraryKit 的其他能力

5.1 获取相册列表

async function getAlbums(): Promise<photoAccessHelper.Album[]> {
  const helper = photoAccessHelper.getPhotoAccessHelper(getContext(this))
  const albums = await helper.getAlbums(
    photoAccessHelper.AlbumType.USER,
    photoAccessHelper.AlbumSubtype.USER_GENERIC
  )
  return albums
}

5.2 保存图片到相册

import { photoAccessHelper } from '@kit.MediaLibraryKit'

async function saveToAlbum(uri: string): Promise<void> {
  const helper = photoAccessHelper.getPhotoAccessHelper(getContext(this))
  // 将文件添加到系统相册
  await helper.addAsset(uri)
}

六、PhotoViewPicker vs 自定义相册

对比 PhotoViewPicker(项目使用) 自定义相册
实现复杂度 低(3 行代码) 高(权限 + 查询 + UI)
UI 一致性 系统原生相册 自定义 UI
多选支持 ✅ 内置 需要自行实现
权限要求 自动处理 需要 MANAGE_MEDIA
推荐场景 快速集成 需定制 UI 的场景

七、路径与 URI 的处理

7.1 路径统一

拍照和相册选择的图片路径格式不同,但后续处理方式一致:

// 拍照:文件系统路径
this.previewPath = `/data/storage/el2/base/cache/capture_${Date.now()}.jpg`

// 相册:content:// URI
this.previewPath = result.photoUris[0]  // content://media/...

// 两种路径传入 AnalyzingPage 后的使用
const imagePath = AppStorage.get<string>('capture_image_path') ?? ''
await HandwritingDao.create({
  image_path: imagePath,    // 原样保存路径
  // ...
})

7.2 路径格式统一化

private normalizePath(uri: string): string {
  if (uri.startsWith('content://')) {
    // content:// URI → 复制到应用缓存目录
    // 这需要读取文件内容并写入临时目录
    return uri  // 项目当前直接使用 URI
  }
  return uri  // 已经是文件路径
}

八、权限相关

8.1 相册读取权限

使用 PhotoViewPicker 不需要申请 ohos.permission.READ_MEDIA 权限——系统选择器自动处理了授权。但如果使用 photoAccessHelper 的其他 API(如遍历相册),则需要声明权限:

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

8.2 权限对比

场景 需要权限 权限声明
PhotoViewPicker.select() ❌ 不需要 系统选择器自带授权
photoAccessHelper.getAlbums() ✅ 需要 ohos.permission.READ_MEDIA
photoAccessHelper.addAsset() ✅ 需要 ohos.permission.WRITE_MEDIA

总结

本文从 CapturePage 的 pickFromAlbum() 方法出发,完整解析了鸿蒙 PhotoViewPicker 的使用:

  1. 基本使用new PhotoViewPicker()select(options)result.photoUris
  2. 选项配置maxSelectNumber 控制选择数量
  3. 双模式切换:通过 source 参数在拍照/相册间切换
  4. URI 处理:拍照返回文件路径,相册返回 content:// URI
  5. 权限优势:PhotoViewPicker 不需要额外权限声明

下一篇文章将介绍 图片压缩与本地存储——在保存图片时的质量和大小优化。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


参考资源:


九、PhotoViewPicker 完整功能扩展

9.1 多选图片场景

async function pickMultipleImages(): Promise<string[]> {
  const photoPicker = new photoAccessHelper.PhotoViewPicker()
  const options: photoAccessHelper.PhotoSelectOptions = {
    maxSelectNumber: 9,          // 最多选 9 张
    MIME: ['image/jpeg', 'image/png', 'image/heif']  // 仅图片格式
  }
  const result = await photoPicker.select(options)
  return result.photoUris ?? []
}

9.2 DocumentViewPicker:选择文档

除了图片,鸿蒙还提供了 DocumentViewPicker 用于选择文档文件:

import { picker } from '@kit.CoreFileKit'

async function pickDocument(): Promise<string> {
  const documentPicker = new picker.DocumentViewPicker()
  const options: picker.DocumentSelectOptions = {
    maxSelectNumber: 1,
    fileSuffixFilters: ['.pdf', '.txt', '.docx']  // 只显示指定后缀的文件
  }
  const result = await documentPicker.select(options)
  return result[0] ?? ''
}

9.3 URI 转文件路径

相册 URI(content://)需要通过 fs.open 获取文件描述符才能读取:

import fs from '@ohos.file.fs'

async function readImageFromUri(uri: string): Promise<ArrayBuffer> {
  // 通过 URI 打开文件
  const file = fs.openSync(uri, fs.OpenMode.READ_ONLY)
  // 获取文件大小
  const stat = fs.statSync(file.fd)
  // 读取文件内容
  const buffer = new ArrayBuffer(stat.size)
  fs.readSync(file.fd, buffer)
  fs.closeSync(file)
  return buffer
}

9.4 相册访问能力对比

功能 PhotoViewPicker photoAccessHelper DocumentViewPicker
选择图片 ❌(需权限查询)
选择文档
遍历相册 ✅(需 READ_MEDIA)
保存到相册 ✅(需 WRITE_MEDIA)
权限要求 无需额外权限 需要 READ_MEDIA 无需额外权限
推荐场景 用户主动选择 程序化批量访问 用户选择文档

9.5 相册操作完整代码模板

// 完整的相册选择 + 图片处理模板
@Component
struct ImagePickerDemo {
  @State selectedImageUri: string = ''
  @State isLoading: boolean = false

  private async pickAndProcess(): Promise<void> {
    this.isLoading = true
    try {
      // ① 选择图片
      const picker = new photoAccessHelper.PhotoViewPicker()
      const result = await picker.select({ maxSelectNumber: 1 })
      if (!result.photoUris || result.photoUris.length === 0) return

      const uri = result.photoUris[0]

      // ② 解码 PixelMap
      const source = image.createImageSource(uri)
      const pixelMap = await source.createPixelMap({
        desiredSize: { width: 1024, height: 1024 }
      })

      // ③ 压缩保存到缓存
      const outputPath = `${getContext(this).cacheDir}/selected_${Date.now()}.jpg`
      const packer = image.createImagePacker()
      const buffer = await packer.packing(pixelMap, { format: 'image/jpeg', quality: 85 })
      const file = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY)
      fs.writeSync(file.fd, buffer)
      fs.closeSync(file)

      // ④ 释放内存
      pixelMap.release()

      this.selectedImageUri = outputPath
    } catch (err) {
      hilog.error(0x0000, 'ImagePicker', '选图失败: %s', JSON.stringify(err))
    } finally {
      this.isLoading = false
    }
  }

  build() {
    Column() {
      if (this.selectedImageUri) {
        Image(this.selectedImageUri).width(200).height(200).objectFit(ImageFit.Cover)
      }
      Button('从相册选择')
        .onClick(() => this.pickAndProcess())
    }
  }
}

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Logo

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

更多推荐