OpenHarmony 图片选择 相册相机工具封装 ImagePickerUtil(API23)

发布时间:2026/11/5 16:56:30

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 组合使用是头像上传、朋友圈动态、图片笔记、文件上传类功能必备底层媒体工具。
延伸阅读

更多相关文章

2026/10/26 7:04:58

OpenHarmony 权限统一封装 PermissionUtil(动态权限申请全套方案 API23+)

摘要鸿蒙将权限分为安装时静态权限与运行时动态敏感权限,相机、相册、文件、通知、位置等都必须动态申请,原生权限 API 重复代码多、缺少永久拒绝跳转设置页逻辑、无统一弹窗提示、未做权限状态判断封装。本文基于ohos.accessToken封装全局权限工具类&am…

2026/10/31 6:15:14

Chat2DB 实战:从 ChatGPT 写 SQL 到专业 AI 数据库工具

关键词:Chat2DB、数据库管理工具、智能问数、AI SQL、Text2SQL 很多开发者第一次体验 AI 写 SQL,通常是从 ChatGPT 这类通用大模型开始的。输入一句自然语言需求,模型很快给出 SQL 示例,看起来很高效。 但在真实团队里&#xff0…

2026/10/23 15:42:56

开源阅读鸿蒙版:打造完全自定义的阅读体验终极指南

开源阅读鸿蒙版:打造完全自定义的阅读体验终极指南 【免费下载链接】legado-Harmony 开源阅读鸿蒙版仓库 项目地址: https://gitcode.com/gh_mirrors/le/legado-Harmony 你是否厌倦了广告满天飞的阅读应用?是否渴望一个真正自由、无限制的电子书阅…

2026/11/5 16:51:33

EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析

1. 为什么一个PE维护盘值得单独拿出来聊装系统、修引导、救数据、清病毒,这些活儿但凡干过几次的人都知道,真正决定效率的往往不是你手速多快,而是手边那套PE环境靠不靠谱。EasyU(圈内习惯叫优启通)就是在这个场景下被…

2026/11/5 16:26:13

Rust原来很简单二:所有权系统

Part 2 核心篇 —— 所有权系统 本篇覆盖 第 6 ~ 10 章:所有权、借用与引用、生命周期、字符串与切片、内存模型。 ⚠️ 这是全书最重要的一篇。 把它吃透,Rust 从此“很简单”;跳过它,你会处处碰壁。请慢下来,动手敲每一段代码。 第 6 章 所有权:Rust 的第一性原理 6…

2026/11/5 15:00:49

基于Hadoop+Spark的民宿智能推荐系统设计与实现

1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享…

2026/11/5 14:10:30

ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 14:10:30

UART接口24位ADC搭配RS485,解决远距离温度监控难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 14:10:30

UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 0:02:07

多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中…

2026/11/5 0:02:07

大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#…

2026/11/5 0:02:07

鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑