OpenHarmony 相册图片选择 + 压缩工具 ImagePickerUtil 完整实战
前言
原生file.picker相册选择 API 使用繁琐,每次调用都要重复写权限判断、图片读取、尺寸压缩、沙盒缓存写入逻辑,大量冗余代码。本文封装一套完整 ImagePickerUtil 工具,集成权限校验、单选 / 多选、图片自动压缩、图片保存到沙盒、释放 PixelMap 内存,搭配前文 PermissionUtil 权限工具,一行代码唤起相册,适配 HSP、entry、所有 HAR 页面直接调用。
一、工具完整源码 har_base/utils/image_picker_util.ets
ets
import picker from '@ohos.file.picker';
import image from '@ohos.multimedia.image';
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
import PermissionUtil, { PermType } from './permission_util';
import FileUtil from './file_util';
import LogUtil from './log_util';
// 图片压缩配置
export interface ImageCompressOption {
maxWidth: number;
maxHeight: number;
quality: number; // 0-100
}
// 相册返回图片实体
export interface MediaImage {
uri: string;
sandBoxPath: string;
pixelMap?: image.PixelMap;
}
class ImagePickerUtil {
private ctx: common.UIAbilityContext | null = null;
// 默认压缩规则
private readonly defaultOption: ImageCompressOption = {
maxWidth: 1080,
maxHeight: 1920,
quality: 70
};
setContext(ctx: common.UIAbilityContext) {
this.ctx = ctx;
}
// 释放图片内存,防止内存泄漏
private releasePixelMap(pixelMap: image.PixelMap | undefined) {
if (pixelMap) {
pixelMap.release();
LogUtil.debug("ImagePicker", "PixelMap资源释放完成");
}
}
// 图片压缩并写入沙盒缓存目录
private async compressAndSave(pixelMap: image.PixelMap, opt: ImageCompressOption): Promise<string | null> {
try {
// 缩放图片尺寸
const info = pixelMap.getImageInfo();
let targetW = info.width;
let targetH = info.height;
if (targetW > opt.maxWidth || targetH > opt.maxHeight) {
const scale = Math.min(opt.maxWidth / targetW, opt.maxHeight / targetH);
targetW = Math.floor(targetW * scale);
targetH = Math.floor(targetH * scale);
}
const resizer = image.createImageResizer(pixelMap);
const resizeMap = resizer.resizeSync(targetW, targetH);
resizer.release();
// 生成缓存文件路径
const cacheDir = FileUtil.getCacheDir();
const fileName = `img_${new Date().getTime()}.jpg`;
const savePath = `${cacheDir}/${fileName}`;
await FileUtil.mkdir(cacheDir);
// 写入文件
const packer = image.createImagePacker();
const packOpt: image.PackingOption = {
format: image.ImageFormat.JPEG,
quality: opt.quality
};
const fileFd = await fs.open(savePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
await packer.packToFile(resizeMap, fileFd.fd, packOpt);
packer.release();
resizeMap.release();
fs.closeSync(fileFd);
return savePath;
} catch (e) {
LogUtil.error("ImagePicker", "图片压缩失败", e);
return null;
}
}
// 单选相册图片
async pickSingle(compressOpt?: ImageCompressOption): Promise<MediaImage | null> {
// 先校验媒体读取权限
const hasPerm = await PermissionUtil.requestPermission(PermType.IMAGE);
if (!hasPerm) return null;
if (!this.ctx) {
LogUtil.error("ImagePicker", "上下文未初始化");
return null;
}
try {
// 创建相册选择器
const photoPicker = new picker.PhotoViewPicker();
const res = await photoPicker.select({
MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE,
maxSelectNumber: 1
});
if (res.photoUris.length === 0) return null;
const uri = res.photoUris[0];
// 读取图片PixelMap
const imgSource = image.createImageSource(uri);
const pixelMap = imgSource.createPixelMapSync();
imgSource.release();
// 压缩保存
const opt = compressOpt ?? this.defaultOption;
const sandBoxPath = await this.compressAndSave(pixelMap, opt);
if (!sandBoxPath) {
this.releasePixelMap(pixelMap);
return null;
}
const result: MediaImage = {
uri,
sandBoxPath,
pixelMap
};
return result;
} catch (err) {
LogUtil.error("ImagePicker", "单选图片异常", err);
return null;
}
}
// 多选相册图片
async pickMulti(maxCount: number, compressOpt?: ImageCompressOption): Promise<MediaImage[]> {
const hasPerm = await PermissionUtil.requestPermission(PermType.IMAGE);
if (!hasPerm) return [];
if (!this.ctx) return [];
const list: MediaImage[] = [];
try {
const photoPicker = new picker.PhotoViewPicker();
const res = await photoPicker.select({
MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE,
maxSelectNumber: maxCount
});
for (const uri of res.photoUris) {
const imgSource = image.createImageSource(uri);
const pixelMap = imgSource.createPixelMapSync();
imgSource.release();
const opt = compressOpt ?? this.defaultOption;
const sandBoxPath = await this.compressAndSave(pixelMap, opt);
if (sandBoxPath) {
list.push({ uri, sandBoxPath, pixelMap });
} else {
this.releasePixelMap(pixelMap);
}
}
return list;
} catch (err) {
LogUtil.error("ImagePicker", "多选图片异常", err);
return [];
}
}
// 清理页面图片资源
clearImageResource(images: MediaImage[]) {
images.forEach(item => this.releasePixelMap(item.pixelMap));
}
}
export default new ImagePickerUtil();
二、工具初始化配置
1. EntryAbility 注入上下文(和前文统一)
ets
// EntryAbility onCreate
ImagePickerUtil.setContext(this.context);
三、页面调用示例
示例 1:编辑器页面单选图片(HSP 分包 EditorPage)
ets
import ImagePickerUtil, { MediaImage } from '@ohos:har_base/utils/image_picker_util'
import DialogUtil from '@ohos:har_base/utils/dialog_util'
import ThemeUtil from '@ohos:har_base/utils/theme'
@Entry
@Component
struct EditorPage {
@State imgList: MediaImage[] = []
// 唤起相册选图
async selectImage() {
const img = await ImagePickerUtil.pickSingle({
maxWidth: 720,
maxHeight: 1280,
quality: 60
});
if (img) {
this.imgList.push(img);
DialogUtil.alert({ content: "图片选择成功,已自动压缩" });
}
}
// 页面销毁释放图片内存
aboutToDisappear() {
ImagePickerUtil.clearImageResource(this.imgList);
}
build() {
const color = ThemeUtil.getColor();
const size = ThemeUtil.getSize();
Column({ space: size.gapMd })
.width("100%")
.height("100%")
.padding(size.gapMd)
.backgroundColor(color.pageBg)
{
Button("选择相册图片")
.width("100%")
.height(size.btnNormal)
.backgroundColor(color.primary)
.onClick(() => this.selectImage())
// 图片预览
List({ space: size.gapSm }) {
ForEach(this.imgList, (item: MediaImage) => {
ListItem() {
Image(item.sandBoxPath)
.width("100%")
.height(200)
.objectFit(ImageFit.Contain)
.backgroundColor("#00000010")
}
})
}
.layoutWeight(1)
}
}
}
示例 2:个人中心头像上传(entry/mine/Mine.ets)
ets
import ImagePickerUtil, { MediaImage } from '@ohos:har_base/utils/image_picker_util'
import ThemeUtil from '@ohos:har_base/utils/theme'
@Entry
@Component
struct MinePage {
@State avatarPath: string = ""
async changeAvatar() {
// 头像专用压缩尺寸
const img = await ImagePickerUtil.pickSingle({
maxWidth: 300,
maxHeight: 300,
quality: 80
});
if (img) {
this.avatarPath = img.sandBoxPath;
}
}
build() {
const color = ThemeUtil.getColor();
const size = ThemeUtil.getSize();
Column()
.width("100%")
.padding(size.gapLg)
.backgroundColor(color.pageBg)
{
Image(this.avatarPath || $r("sys.media.ohos_ic_public_avatar"))
.width(120)
.height(120)
.borderRadius(60)
.margin({ bottom: size.gapLg })
.onClick(() => this.changeAvatar())
Text("点击头像更换相册图片")
.fontColor(color.textSecondary)
}
}
}
四、工具核心能力说明
-
权限全自动管理 内部自动调用 PermissionUtil 校验媒体相册权限,未授权自动弹窗申请,永久拒绝则跳转系统设置,业务页面无需写任何权限判断代码。
-
自动压缩逻辑 支持自定义宽高上限、画质质量,大图自动等比例缩放,减少图片占用存储与内存,上传接口时降低流量消耗。
-
沙盒持久缓存 选择后的图片自动写入应用私有 cache 缓存目录,统一管理,可通过 FileUtil.clearCache () 一键全部清理。
-
内存自动管控 封装 releasePixelMap 统一释放图像资源,页面销毁调用 clearImageResource 彻底回收,避免反复选图造成内存持续上涨。
-
单选 / 多选双接口 单选用于头像、单图上传;多选用于笔记、朋友圈多图场景,限制最大选择数量。
五、常见踩坑解决方案
-
图片选择后页面内存持续升高 问题:未释放 PixelMap 图像资源。 解决:页面 aboutToDisappear 调用 ImagePickerUtil.clearImageResource 清空图片数组。
-
相册唤起直接失败无弹窗 问题:未提前注入上下文,或者权限未授予。 解决:确认 EntryAbility 执行 ImagePickerUtil.setContext,工具内部会自动申请媒体权限。
-
压缩后图片模糊 解决:调高 quality 参数(80-90),适当增大 maxWidth/maxHeight 尺寸。
-
HSP 分包调用图片工具失效 解决:工具存放在 har_base,HSP 已依赖 har_base,导入路径
@ohos:har_base/utils/image_picker_util即可正常使用。 -
缓存图片太多占用存储空间 解决:设置页面调用 FileUtil.clearCache () 一键清空所有压缩图片缓存。
六、搭配前文架构规范总结
- 所有图片相关操作统一使用 ImagePickerUtil,禁止直接调用原生 picker/image API;
- 页面退出必须释放 PixelMap 资源,遵守统一生命周期规范;
- 业务区分头像、笔记附件两套压缩配置,按需调整尺寸画质;
- 缓存图片统一存放在 cacheDir,不占用永久 files 目录空间。
更多推荐
所有评论(0)