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

发布时间:2026/9/14 20:48:28

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/9/9 17:58:33

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

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

2026/9/14 0:36:39

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

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

2026/9/13 3:49:38

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

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

2026/9/15 3:06:29

Skills协议:可验证、可复用的能力建模与评分体系

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

2026/9/15 3:06:29

WorkBuddy AI工作流实战:从零搭建到自动化部署全指南

WorkBuddy 这套 36 集教程最近在各个平台被刷屏,标题动辄“吊打付费”“全 B 站最全”,很多人把它当成了 AI 工作流的入门救星。作为一个从 n8n、Coze 一路折腾到 WorkBuddy 的老玩家,我想用自己的真实体验聊聊这个工具到底值不值得学、安装过…

2026/9/15 3:06:29

野猪目标检测数据集:YOLOv12专用林区小目标训练集

简介:本资源是面向计算机视觉开发者与农业/生态领域AI应用工程师的野猪目标检测专用数据集,解决野生动物智能监测、农田入侵预警及种群行为研究中的高质量标注数据缺失问题。压缩包共1730个文件,含864张JPG图像(航拍与林间地面多视…

2026/9/15 3:06:29

Spark+Scala电商用户画像引擎:RFM建模与HBase实时服务闭环

简介:本资源是一套基于Spark的电商用户画像数据挖掘项目源码,面向大数据开发工程师、推荐系统从业者及高校相关专业学习者,聚焦解决电商平台海量用户行为数据的建模、标签化与个性化应用问题。压缩包共462个文件,总大小13.45MB&am…

2026/9/15 3:06:29

awesome-llm-apps:大模型应用开发精选清单与实战避坑指南

这两年只要打开 GitHub 热门榜,十次有八次都能看到“awesome”开头的仓库。从 awesome-frontend 到 awesome-selfhosted,这种“什么火就整理什么”的清单文化,某种程度上就是开发者社区的晴雨表。而“awesome-llm-apps”这个名字,…

2026/9/15 3:01:28

从环境变量到MCP:Java面试三轮考点全拆解

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

2026/9/14 2:17:50

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/15 0:01:16

AI英语单词APP开发:自适应学习算法与移动端优化实践

1. 项目概述 作为一名在移动应用开发领域摸爬滚打多年的老手,我最近完成了一个AI英语单词APP的开发项目。这个项目将传统单词记忆方法与现代AI技术相结合,打造了一款能够智能适应不同用户学习习惯的英语学习工具。 市面上大多数单词APP都存在一个通病&a…

2026/9/15 0:01:16

Flutter与OpenHarmony结合开发手语学习APP实战

1. 项目背景与核心价值作为一名同时接触过Flutter和OpenHarmony的开发者,最近我完成了一个基于Flutter for OpenHarmony的手语学习APP实战项目。这个项目最大的特点在于实现了跨平台框架与国产操作系统深度结合的创新实践——用Flutter开发的应用能完美运行在OpenHa…

2026/9/15 0:01:16

六个月成为机器人工程师:从ROS2到SLAM的实战路径

1. 六个月的紧迫感从哪来:先搞清楚你要成为哪种机器人工程师说实话,六个月的期限并不是一个宽松的时间线。市面上任何一本正经的机器人学教材都超过五百页,ROS2的官方文档可以翻到你怀疑人生,再加上ABB、KUKA这些工业机器人厂家动…

2026/9/14 11:59:31

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/14 13:53:59

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/14 11:22:57

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

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

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

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