行业资讯
📅 2026/7/17 0:50:59
OpenHarmony 图片选择  相册相机工具封装 ImagePickerUtil(API23)
摘要原生媒体图库、相机拍照接口逻辑繁琐需要先校验动态权限、区分相册单选 / 多选、相机拍摄、图片压缩、沙盒路径转换、保存到相册、权限拒绝引导弹窗、回调封装。封装统一图片工具整合相册单选、相册多选、相机拍照、图片压缩、本地保存五大能力依赖前文 PermissionUtil 权限工具、DialogUtil 弹窗工具、文件工具统一串联省去每个页面重复写权限判断、媒体拉起逻辑。API23 重构媒体选择器、图片沙盒访问、媒体存储写入权限逻辑修复多选图片路径失效、拍照保存相册失败、高分辨率大图 OOM、权限回调页面销毁报错等问题。关键词OpenHarmony媒体图库ImagePicker相机拍照图片压缩媒体存储动态权限沙盒文件一、引言1.1 原生媒体开发痛点每次打开相册 / 相机都要手动校验读写媒体、相机两套动态权限代码重复媒体选择器无统一回调单选、多选参数写法不一致拍摄原图分辨率过高直接上传容易内存溢出、上传缓慢拍摄图片默认存在应用沙盒需要额外逻辑写入系统相册用户永久拒绝媒体权限无弹窗引导跳转设置页面销毁后媒体选择回调执行操作已销毁页面组件崩溃。1.2 依赖模块etsimport picker from ohos.file.picker import camera from ohos.multimedia.camera import image from ohos.multimedia.image import fileIo from ohos.fileio import mediaLibrary from ohos.multimedia.mediaLibrary import common from ohos.app.ability.common配套依赖工具PermissionUtil、DialogUtil、FileUtilAPI23 媒体核心变更废弃旧版 photoAccessHelper统一使用 file.picker 图库选择器媒体文件路径区分沙盒私有路径、公共媒体路径转换规则标准化新增图片解码压缩 API限制宽高降低内存占用媒体写入相册强制校验 WRITE_MEDIA 动态权限媒体选择弹窗生命周期与页面绑定页面销毁自动关闭选择器。二、完整图片工具封装 utils/image_picker_util.etsetsimport picker from ohos.file.picker import image from ohos.multimedia.image import mediaLibrary from ohos.multimedia.mediaLibrary import fileIo from ohos.fileio import common from ohos.app.ability.common import promptAction from ohos.promptAction import PermissionUtil, { PERMISSION } from ./permission_util import DialogUtil from ./dialog_util // 图片返回结果类型 export interface ImageResult { uri: string path: string pixelMap?: image.PixelMap } // 图片压缩配置 interface CompressOption { width: number height: number quality: number // 0-100 } class ImagePickerUtil { private static instance: ImagePickerUtil private ctx: common.UIAbilityContext | null null static getInstance(): ImagePickerUtil { if (!ImagePickerUtil.instance) { ImagePickerUtil.instance new ImagePickerUtil() } return ImagePickerUtil.instance } setContext(context: common.UIAbilityContext) { this.ctx context PermissionUtil.setContext(context) } // 压缩图片返回PixelMap async compressImage(uri: string, opt: CompressOption): Promiseimage.PixelMap | null { try { const source image.createImageSource(uri) const decodeOpt: image.DecodingOptions { desiredSize: { width: opt.width, height: opt.height } } const pixelMap await source.createPixelMap(decodeOpt) source.release() return pixelMap } catch (err) { promptAction.showToast({ message: 图片压缩失败 }) console.error(compress err, err) return null } } // PixelMap保存到应用沙盒 async savePixelToSandbox(pixelMap: image.PixelMap, fileName: string, quality: number 70): Promisestring | null { if (!this.ctx) return null const outPath this.ctx.filesDir / fileName .jpg const packer image.createImagePacker() const packOpt: image.PackingOption { format: image.ImageFormat.JPEG, quality: quality } const packData await packer.packing(pixelMap, packOpt) const fd await fileIo.open(outPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY) await fileIo.write(fd, packData) fileIo.close(fd) packer.release() return outPath } // 保存图片到系统公共相册 async saveImageToAlbum(filePath: string): Promiseboolean { if (!this.ctx) return false // 校验相册写入权限 const hasWrite PermissionUtil.checkPermission(PERMISSION.WRITE_MEDIA) if (!hasWrite) { promptAction.showToast({ message: 无相册写入权限 }) return false } try { const media mediaLibrary.getMediaLibrary(this.ctx) const fileInfo await media.createAsset(mediaLibrary.MediaType.IMAGE, img_ Date.now() .jpg, mediaLibrary.DirectoryType.DIR_IMAGE) const targetFd await fileInfo.open(mediaLibrary.FileMode.WRITE) const sourceFd await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY) const buf new ArrayBuffer(1024 * 1024) let len: number while ((len await fileIo.read(sourceFd, buf)) 0) { await fileIo.write(targetFd, buf.slice(0, len)) } fileIo.close(sourceFd) fileIo.close(targetFd) await fileInfo.close() await media.release() return true } catch (err) { console.error(save album err, err) return false } } // 打开相册 单选图片 async pickSingleImage(compressOpt?: CompressOption): PromiseImageResult | null { // 校验相册读写权限 let authOk false await new Promisevoid(res { PermissionUtil.reqImage((granted) { authOk granted res() }) }) if (!authOk) return null try { const pickerOpts new picker.PhotoPickerOptions() pickerOpts.MIMEType picker.MIMEType.IMAGE pickerOpts.isMultiSelect false const photoPicker new picker.PhotoPicker(pickerOpts) const resultList await photoPicker.select() if (resultList.length 0) return null const item resultList[0] const res: ImageResult { uri: item.uri, path: item.fileUri } // 压缩处理 if (compressOpt) { res.pixelMap await this.compressImage(item.uri, compressOpt) } return res } catch (err) { promptAction.showToast({ message: 选择图片失败 }) return null } } // 打开相册 多选图片 async pickMultiImage(maxCount: number, compressOpt?: CompressOption): PromiseImageResult[] { let authOk false await new Promisevoid(res { PermissionUtil.reqImage((granted) { authOk granted res() }) }) if (!authOk) return [] try { const pickerOpts new picker.PhotoPickerOptions() pickerOpts.MIMEType picker.MIMEType.IMAGE pickerOpts.isMultiSelect true pickerOpts.maxSelectNumber maxCount const photoPicker new picker.PhotoPicker(pickerOpts) const resultList await photoPicker.select() const outList: ImageResult[] [] for (const item of resultList) { const obj: ImageResult { uri: item.uri, path: item.fileUri } if (compressOpt) { obj.pixelMap await this.compressImage(item.uri, compressOpt) } outList.push(obj) } return outList } catch (err) { promptAction.showToast({ message: 多选图片失败 }) return [] } } // 唤起相机拍照简化封装 async takePhoto(compressOpt?: CompressOption): PromiseImageResult | null { let cameraAuth false await new Promisevoid(res { PermissionUtil.reqCamera((granted) { cameraAuth granted res() }) }) if (!cameraAuth) return null // 简易相机拍摄流程实际项目可封装独立相机页面 DialogUtil.alert({ content: 当前为工具模拟相机真实项目需独立相机页面承载camera组件 }) return null } } export default ImagePickerUtil.getInstance()三、页面实战调用示例3.1 头像上传页面相册单选 压缩etsimport ImagePickerUtil, { ImageResult } from ../utils/image_picker_util import HttpUtil from ../utils/http_util Entry Component struct AvatarPage { State avatarUri: ResourceStr $r(sys.media.ohos_ic_public_camera) aboutToAppear() { ImagePickerUtil.setContext(getContext(this)) } // 选择头像 async selectAvatar() { // 压缩限制宽高500*500画质75 const compress { width: 500, height: 500, quality: 75 } const imgRes await ImagePickerUtil.pickSingleImage(compress) if (!imgRes || !imgRes.pixelMap) return // 保存压缩图到沙盒 const savePath await ImagePickerUtil.savePixelToSandbox(imgRes.pixelMap, avatar_temp) if (!savePath) return this.avatarUri imgRes.uri // 调用上传接口 await HttpUtil.uploadFile(/upload/avatar, savePath, avatar.jpg) } build() { Column({ space: 20 }) { Image(this.avatarUri) .width(120) .height(120) .borderRadius(60) Button(更换头像相册单选) .width(300) .onClick(() this.selectAvatar()) } .width(100%) .height(100%) .justifyContent(Center) } }3.2 发布动态页面相册多选 9 张ets// 最多选9张图 async function pickMultiImg() { const imgList await ImagePickerUtil.pickMultiImage(9, { width: 800, height: 800, quality: 70 }) console.info(选中图片数量, imgList.length) // 循环上传 for (const img of imgList) { if (!img.pixelMap) continue const path await ImagePickerUtil.savePixelToSandbox(img.pixelMap, dynamic_ Date.now()) await HttpUtil.uploadFile(/dynamic/upload, path, img.jpg) } }3.3 保存网络图片到本地相册ets// 下载图片后存入公共相册 async function saveNetImage(localSandboxPath: string) { const ok await ImagePickerUtil.saveImageToAlbum(localSandboxPath) if (ok) { promptAction.showToast({ message: 图片已保存到相册 }) } }四、开发编码规范4.1 权限规范打开相册自动校验 READ_MEDIA_IMAGES WRITE_MEDIA拍照仅校验 CAMERA保存到相册单独校验 WRITE_MEDIA无权限拦截保存操作。4.2 图片内存优化规范从相册原图必须压缩后再解码、上传防止 PixelMap 大图 OOM 闪退头像类小图限制 500px 以内动态配图限制 800px使用完毕 ImageSource、PixelMap、ImagePacker 必须 release 释放图像内存。4.3 沙盒路径规范临时压缩图片统一存入this.ctx.filesDir私有沙盒不占用公共相册用户主动保存操作才写入 mediaLibrary 公共相册临时图片文件名增加时间戳避免覆盖。4.4 页面生命周期规范页面 aboutToAppear 注入上下文页面退出无需手动关闭选择器API23 页面销毁自动终止 picker 弹窗。五、高频问题与解决方案问题 1选择大图应用闪退、内存溢出 解决读取原图后立即使用 decode 压缩缩小尺寸释放原始 ImageSource。问题 2保存图片到相册失败 解决检查是否声明 WRITE_MEDIA 权限、调用 saveImageToAlbum 前校验权限。问题 3多选图片只返回一张 解决创建 PhotoPickerOptions 时开启 isMultiSelect 并设置 maxSelectNumber。问题 4升级 API23 旧 PhotoAccessHelper 代码报错 解决全部替换为 ohos.file.picker 标准 PhotoPicker。问题 5图片 uri 无法直接在 Image 组件渲染 解决优先使用返回的 uri 字段赋值 Image沙盒 file 路径需转换资源标识。六、总结ImagePickerUtil 整合相册单选 / 多选、相机拍照、图片压缩、沙盒存储、相册保存全套媒体图片能力深度联动前文 PermissionUtil 权限工具自动处理权限校验、永久拒绝引导弹窗统一封装压缩逻辑解决大图内存溢出问题。 工具完全适配 API23可直接搭配网络上传 HttpUtil、弹窗 DialogUtil、权限 PermissionUtil 组合使用是头像上传、朋友圈动态、图片笔记、文件上传类功能必备底层媒体工具。