上个月才接触鸿蒙,这个月开始写功能,中间遇到了很多问题,但是通过搜索,查阅官方API一般都能解决。
但是上周在写图片上传功能时遇到了很大的坑,先记录下过程,然后慢慢梳理解决方案吧。 对于中间的研究过程不做细节分析,毕竟有结果Ctrl C+V多爽
tips:本次的分享式基于Bate5,Build Version:5.0.3.700的DevEco Studio,因为版本迭代较快,所以后期可能有所差别。
- 需要选择图片并回显出来,这块在接收到图片选择的结果后,再次打开后传入之前接收的图片值,会发现无法回显。
2.图片选择后,大图片进行压缩处理,但是官网虽然有直接现成的压缩方法,但是直接现用时就………
3.图片上传时,有几个三方库,demo也很完善,但是这块需要的是带加密和传参的文件上传,又是查无可查……
1. 相册图片选择
参考的官方链接是
如何读取相册中的图片
@ohos.file.photoAccessHelper (相册管理模块)
/**
* 拉起相册并选择图片
*/
getPictureFromAlbum() {
let selectResult = new Array<string>() //预选择图片的uri数据
// 拉起相册,选择图片
this.imageList.forEach(element => {
if (element !== this.addImage) {
selectResult.push(element)
}
});
let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 4;
PhotoSelectOptions.preselectedUris = selectResult;
let photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
if (PhotoSelectResult.photoUris.length > 0) {
// 拿到结果
this.imageList = PhotoSelectResult.photoUris;
// 如果不够4张,添加AddImage
if (this.imageList.length < 4) {
this.imageList.push(this.addImage)
}
}
}).catch((err: BusinessError) => {
})
}
相册图片选择需要注意选择图片后,再次拉起相册后,想要之前已经选择的照片直接选择时需要传值,此时不能直接传之前的@State接收变量的值,相册中不接受动态变量。
2.图片压缩
参考的官方链接是
如何将PixelMap压缩到指定大小以下
图片压缩API的质量参数quality与图片原始大小、压缩后大小的关系
说遇到的问题
1.本地选择图片后,需要把文件转换为PixelMap类型
2.这块我是批量选取的,最大4张,如果选择完后如何批量压缩呢?(for循环是不支持await的,在鸿蒙中写这个时真懵逼了,完全忘了for循环中不能进行耗时操作,哎,苦逼的试了好半天)
3.在写这个功能时,官方文档给的最后文件名是写死的,这块需要自己改
/**
* 拉起相册并选择图片
*/
getPictureFromAlbum() {
let selectResult = new Array<string>() //预选择图片的uri数据
// 拉起相册,选择图片
this.imageList.forEach(element => {
if (element !== this.addImage) {
selectResult.push(element)
}
});
let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 4;
PhotoSelectOptions.preselectedUris = selectResult;
let photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
if (PhotoSelectResult.photoUris.length > 0) {
// 初始化压缩列表
this.compressedList = []
// 异步进行压缩
PhotoSelectResult.photoUris.forEach(async (element: string) => {
let upImage = await compressedImage(element, 200)
upImage.originalPath = element
this.compressedList.push(upImage)
})
// 拿到结果
this.imageList = PhotoSelectResult.photoUris;
// 如果不够4张,添加AddImage
if (this.imageList.length < 4) {
this.imageList.push(this.addImage)
}
}
}).catch((err: BusinessError) => {
})
}
最后试出最好用的办法就是在选择万图片后直接进行 异步压缩,然后在最后上传时处理删除图片的操作
下面是压缩的文件方法类,也可以看作一个工具类
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
export class CompressedImageInfo {
imageUri: string = ""; // 压缩后图片保存位置的uri
name: string = ""; // 压缩后图片名称
imageByteLength: number = 0; // 压缩后图片字节长度
originalPath: string = ""; // 原路径
}
export async function imageToPixelMap(uri: string): Promise<image.PixelMap> {
return new Promise((resolve, reject) => {
try {
// 解码成PixelMap
const imageSource = image.createImageSource(uri);
const decodingOptions: image.DecodingOptions = {
editable: true,
desiredPixelFormat: 3,
}
imageSource.createPixelMap(decodingOptions).then((pixelMap: image.PixelMap) => {
resolve(pixelMap)
})
} catch (error) {
reject(error);
}
});
}
/**
* 图片拷贝,保存
* @param uri 原始图片路径
* @returns 拷贝后的沙箱路径
*/
export async function imageCopy(uri: string): Promise<CompressedImageInfo> {
const saveDir = getContext().cacheDir //存储的文件目录
const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY)
const name = util.generateRandomUUID() + ".jpg"
const fileDir = saveDir + "/" + name
fileIo.copyFileSync(file.fd, fileDir)
const photoSize = fileIo.statSync(file.fd).size; // 获取图片大小 单位:字节
fileIo.closeSync(file.fd)
let compressedImageInfo: CompressedImageInfo = new CompressedImageInfo();
compressedImageInfo.imageUri = fileDir;
compressedImageInfo.name = name;
compressedImageInfo.imageByteLength = photoSize;
return compressedImageInfo;
}
/**
* 图片压缩,保存
* @param sourcePixelMap:原始待压缩图片的PixelMap对象
* @param maxCompressedImageSize:指定图片的压缩目标大小,单位kb
* @returns compressedImageInfo:返回最终压缩后的图片信息
*/
export async function compressedImage(filePath: string,
maxCompressedImageSize: number): Promise<CompressedImageInfo> {
let compress = await imageCopy(filePath)
let sourcePixelMap = await imageToPixelMap(compress.imageUri)
// 创建图像编码ImagePacker对象
const imagePackerApi = image.createImagePacker();
const IMAGE_QUALITY = 0;
const packOpts: image.PackingOption = { format: "image/jpeg", quality: IMAGE_QUALITY };
// 通过PixelMap进行编码。compressedImageData为打包获取到的图片文件流。
let compressedImageData: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts);
// 压缩目标图像字节长度
const maxCompressedImageByte = maxCompressedImageSize * 1024;
// 图片压缩。先判断设置图片质量参数quality为0时,packing能压缩到的图片最小字节大小是否满足指定的图片压缩大小。如果满足,则使用packing方式二分查找最接近指定图片压缩目标大小的quality来压缩图片。如果不满足,则使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据。
if (maxCompressedImageByte > compressedImageData.byteLength) {
// 使用packing二分压缩获取图片文件流
compressedImageData =
await packingImage(compressedImageData, sourcePixelMap, IMAGE_QUALITY, maxCompressedImageByte);
} else {
// 使用scale对图片先进行缩放,采用while循环每次递减0.4倍缩放图片,再用packing(图片质量参数quality设置0)获取压缩图片大小,最终查找到最接近指定图片压缩目标大小的缩放倍数的图片压缩数据
let imageScale = 1;
const REDUCE_SCALE = 0.4;
// 判断压缩后的图片大小是否大于指定图片的压缩目标大小,如果大于,继续降低缩放倍数压缩。
while (compressedImageData.byteLength > maxCompressedImageByte) {
if (imageScale > 0) {
// 性能知识点: 由于scale会直接修改图片PixelMap数据,所以不适用二分查找scale缩放倍数。这里采用循环递减0.4倍缩放图片,来查找确定最适合的缩放倍数。如果对图片压缩质量要求不高,建议调高每次递减的缩放倍数reduceScale,减少循环,提升scale压缩性能。
imageScale = imageScale - REDUCE_SCALE;
await sourcePixelMap.scale(imageScale, imageScale);
compressedImageData = await packing(sourcePixelMap, IMAGE_QUALITY);
} else {
// imageScale缩放小于等于0时,没有意义,结束压缩。这里不考虑图片缩放倍数小于reduceScale的情况。
break;
}
}
}
// 保存图片,返回压缩后的图片信息。
const compressedImageInfo: CompressedImageInfo = await saveImage(compressedImageData, compress);
console.info('compressedImageInfo: ' + JSON.stringify(compressedImageInfo))
return compressedImageInfo;
}
/**
* packing压缩
* @param sourcePixelMap:原始待压缩图片的PixelMap
* @param imageQuality:图片质量参数
* @returns data:返回压缩后的图片数据
*/
async function packing(sourcePixelMap: image.PixelMap, imageQuality: number): Promise<ArrayBuffer> {
const imagePackerApi = image.createImagePacker();
const packOpts: image.PackingOption = { format: "image/jpeg", quality: imageQuality };
const data: ArrayBuffer = await imagePackerApi.packing(sourcePixelMap, packOpts);
return data;
}
/**
* packing二分方式循环压缩
* @param compressedImageData:图片压缩的ArrayBuffer
* @param sourcePixelMap:原始待压缩图片的PixelMap
* @param imageQuality:图片质量参数
* @param maxCompressedImageByte:压缩目标图像字节长度
* @returns compressedImageData:返回二分packing压缩后的图片数据
*/
async function packingImage(compressedImageData: ArrayBuffer, sourcePixelMap: image.PixelMap, imageQuality: number,
maxCompressedImageByte: number): Promise<ArrayBuffer> {
// 图片质量参数范围为0-100,这里以10为最小二分单位创建用于packing二分图片质量参数的数组。
const packingArray: number[] = [];
const DICHOTOMY_ACCURACY = 10;
// 性能知识点: 如果对图片压缩质量要求不高,建议调高最小二分单位dichotomyAccuracy,减少循环,提升packing压缩性能。
for (let i = 0; i <= 100; i += DICHOTOMY_ACCURACY) {
packingArray.push(i);
}
let left = 0;
let right = packingArray.length - 1;
// 二分压缩图片
while (left <= right) {
const mid = Math.floor((left + right) / 2);
imageQuality = packingArray[mid];
// 根据传入的图片质量参数进行packing压缩,返回压缩后的图片文件流数据。
compressedImageData = await packing(sourcePixelMap, imageQuality);
// 判断查找一个尽可能接近但不超过压缩目标的压缩大小
if (compressedImageData.byteLength <= maxCompressedImageByte) {
left = mid + 1;
if (mid === packingArray.length - 1) {
break;
}
// 获取下一次二分的图片质量参数(mid+1)压缩的图片文件流数据
compressedImageData = await packing(sourcePixelMap, packingArray[mid + 1]);
// 判断用下一次图片质量参数(mid+1)压缩的图片大小是否大于指定图片的压缩目标大小。如果大于,说明当前图片质量参数(mid)压缩出来的图片大小最接近指定图片的压缩目标大小。传入当前图片质量参数mid,得到最终目标图片压缩数据。
if (compressedImageData.byteLength > maxCompressedImageByte) {
compressedImageData = await packing(sourcePixelMap, packingArray[mid]);
break;
}
} else {
// 目标值不在当前范围的右半部分,将搜索范围的右边界向左移动,以缩小搜索范围并继续在下一次迭代中查找左半部分。
right = mid - 1;
}
}
return compressedImageData;
}
/**
* 图片保存
* @param compressedImageData:压缩后的图片数据
* @returns compressedImageInfo:返回压缩后的图片信息
*/
async function saveImage(compressedImageData: ArrayBuffer, compress: CompressedImageInfo): Promise<CompressedImageInfo> {
const context: Context = getContext();
// 定义要保存的压缩图片uri。afterCompressiona.jpeg表示压缩后的图片。
// const afterCompression = util.generateRandomUUID() + ".jpg"
// const compressedImageUri: string = context.filesDir + '/' + afterCompression;
const compressedImageUri: string = compress.imageUri
try {
const res = fileIo.accessSync(compressedImageUri);
if (res) {
// 如果图片afterCompressiona.jpeg已存在,则删除
fileIo.unlinkSync(compressedImageUri);
}
} catch (err) {
console.error(`AccessSync failed with error message: ${err.message}, error code: ${err.code}`);
}
// 知识点:保存图片。获取最终图片压缩数据compressedImageData,保存图片。
// 压缩图片数据写入文件
const file: fileIo.File = fileIo.openSync(compressedImageUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
fileIo.writeSync(file.fd, compressedImageData);
fileIo.closeSync(file);
// 获取压缩图片信息
let compressedImageInfo: CompressedImageInfo = new CompressedImageInfo();
compressedImageInfo.imageUri = compressedImageUri;
compressedImageInfo.name = compress.name;
compressedImageInfo.imageByteLength = compressedImageData.byteLength;
return compressedImageInfo;
}
里面经过处理的几个特别注意的点
- 图片转PixelMap时,需要把文件Copy到app缓存中,应该就是说的沙箱路径
2.图片是循环压缩的,倍数0.4,需要自己按需更改,不然会模糊的厉害
3.压缩后的路径需要和压缩前的路径保持完全一样,不然虽然现实压缩成功了,但是在上传时,上传压缩后的文件路径会一直显示文件损坏
3. 文件上传
@ohos.request (上传下载)
这块官方文档demo比较全,但是介绍看的云里雾里的,比如
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM)//表单类型
.addFormDataPart("userId", userId);
RequestBody imageBody = RequestBody.create(MediaType.parse("multipart/form-data"), files.get(i));
builder.addFormDataPart("fileupload", files.get(i).getName(), imageBody);
在Android中一个builder都给区分开了,但是在鸿蒙中,就需要分的比较清晰了,需要注意几个点:
1.有固定的文件上传格式,特别是internal://cache/... 这个路径,一定要写对。
2.multipart/form-data属于header,而别的传参则属于key-value格式的data
3.上传完成后,‘complete’回调,并不会有任何请求结果返回,只是一个完成的状态回调,而上传成功后的结果,需要自己在‘headerReceive’中接收处理,这块特别注意的一点是,每个图片上传成功后,都会回调一次‘headerReceive’,而‘complete’方法是所有文件上传完成后才会执行。
static async uploadImageFile(fileList: CompressedImageInfo[]): Promise<string> {
return new Promise(async (resolve, reject) => {
//返回的结果 string
let resp: string[] = []
const fileParams: request.File[] = []
fileList.forEach((element) => {
fileParams.push({
filename: element.name,
name: "fileupload",
type: "jpg",
uri: `internal://cache/${element.name}`
})
})
let uploadConfig: request.UploadConfig = {
method: http.RequestMethod.POST,
url: requestUrl,
header: {
'Content-Type': 'multipart/form-data',
'app-platform': 'HarmonyOS',
'client-type': 'HUAWEI',
'User-Agent': `harmony/1.0 (${deviceTypeInfo}; ${osFullName}; Scale/3.00)`,
},
files: fileParams,
data: [
{ name: 'userId', value: userId },
]
}
// 输出请求信息
console.info("uploadConfig:" + JSON.stringify(uploadConfig));
// 请求进度回调
let upProgressCallback = (uploadedSize: number, totalSize: number) => {
console.info("upload totalSize:" + totalSize + " uploadedSize:" + uploadedSize);
};
// 请求头/结果回调
let headerCallback = (headers: object) => {
console.info("upOnHeader headers:" + JSON.stringify(headers));
if (headers["body"]) {
let result = JSON.parse(headers["body"]) as BasicResponse
if (result.success) {
let imageBean = result.entity as ImageBean[]
resp.push(imageBean[0].imageUrl)
}
}
};
// 请求结束回调
let upCompleteCallback = (taskStates: Array<request.TaskState>) => {
console.info("upOnComplete taskState:" + JSON.stringify(resp));
resolve(resp.join())
};
// 请求失败回调
let upFailCallback = (taskStates: Array<request.TaskState>) => {
reject('上传失败')
};
let uploadTask = await request.uploadFile(getContext(), uploadConfig)
uploadTask.on('progress', upProgressCallback);
uploadTask.on('headerReceive', headerCallback);
uploadTask.on('complete', upCompleteCallback);
uploadTask.on('fail', upFailCallback);
} catch (err) {
console.info('uploadFile err:' + err);
reject('上传失败')
}
})
}
总结
从图片选择,压缩,上传,再到相互结合处理,前后花费了差不多一周。也许是刚刚接触HarmonyOs,比较难以上手,又或者是自己能力不到家,反正大大小小的坑基本都走全了。 在这个过程中,不管是百度,官网,还有论坛,所有的资料都零零散散,感觉很杂乱,故此整理一下做成可用的功能来分享下。
时间赶,代码乱,哈哈……不喜欢啥功能都靠三方库依赖来解决,直接加进来按需修改多么简单粗暴,反正稍微修改下直接用没问题,有啥问题多多海涵,亦望指教。