feat(ImageCropper): 增加图片裁剪和压缩功能- 在 ImageCropperView 中添加宽高比和最大压缩大小参数

- 实现图片压缩功能,支持指定压缩目标大小
- 在 UserDetailView 和 YWQMineView 中集成图片裁剪和压缩- 更新基础库,添加 ImageHelper 工具类
这个提交包含在:
徐勤民 2025-05-08 15:37:21 +08:00
父节点 606c006c2a
当前提交 13a7ca4e87
共有 4 个文件被更改,包括 176 次插入57 次删除

查看文件

@ -1,6 +1,7 @@
# [v1.0.10] 2025.xx.xx # [v1.0.10] 2025.xx.xx
> - `ToolsHelper.showConfirmDialog()`&`ToolsHelper.showAlertDialog()`添加自定义`UI`功能 > - `ToolsHelper.showConfirmDialog()`&`ToolsHelper.showAlertDialog()`添加自定义`UI`功能
> - 添加一个`ImageHelper`,处理图片相关
> >
# [v1.0.9] 2025.04.06 # [v1.0.9] 2025.04.06

查看文件

@ -8,6 +8,7 @@ export { Base64Helper } from './src/main/ets/utils/Base64Helper'
export { CharHelper } from './src/main/ets/utils/CharHelper' export { CharHelper } from './src/main/ets/utils/CharHelper'
export { CrashHelper } from './src/main/ets/utils/CrashHelper' export { CrashHelper } from './src/main/ets/utils/CrashHelper'
export { FileHelper } from './src/main/ets/utils/FileHelper' export { FileHelper } from './src/main/ets/utils/FileHelper'
export { ImageHelper } from './src/main/ets/utils/ImageHelper'
export { PickerHelper } from './src/main/ets/utils/PickerHelper' export { PickerHelper } from './src/main/ets/utils/PickerHelper'
export { StrHelper } from './src/main/ets/utils/StrHelper' export { StrHelper } from './src/main/ets/utils/StrHelper'
export { LogHelper } from './src/main/ets/utils/LogHelper' export { LogHelper } from './src/main/ets/utils/LogHelper'

126
README.md
查看文件

@ -12,17 +12,17 @@ ohpm install @szyx/sdk_base
// 初始化 // 初始化
import { WindowHelper } from '@szyx/sdk_base'; import { WindowHelper } from '@szyx/sdk_base';
export default class AppAbility extends UIAbility { export default class AppAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void { onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => { windowStage.loadContent('pages/Index', (err) => {
if (err.code) { if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return; return;
} }
}); });
// 这行代码 // 这行代码
GlobalContext.setContext(this.context) GlobalContext.setContext(this.context)
} }
} }
``` ```
@ -58,11 +58,11 @@ PreferencesHelper.put(StorageKeys.CLIENT_ID, value)
// 获取存储的数据 // 获取存储的数据
PreferencesHelper.get(StorageKeys.CLIENT_ID).then(res => { PreferencesHelper.get(StorageKeys.CLIENT_ID).then(res => {
console.log('>>>>>', res) console.log('>>>>>', res)
}) })
// 删除存储的数据 // 删除存储的数据
PreferencesHelper.delete(StorageKeys.CLIENT_ID).then(() => { PreferencesHelper.delete(StorageKeys.CLIENT_ID).then(() => {
console.log('>>>>>') console.log('>>>>>')
}) })
``` ```
@ -157,7 +157,7 @@ ToolsHelper.showAlertDialog({
msg: '提示信息', msg: '提示信息',
action: { action: {
onClick: () => { onClick: () => {
} }
} }
}) })
@ -174,8 +174,6 @@ ToolsHelper.showAlertDialog({
> >
> `注意,传入参数(options: AlertBean)是固定的` > `注意,传入参数(options: AlertBean)是固定的`
### 1.4.[ValidatorHelper](./src/main/ets/utils/ValidatorHelper.ets) ### 1.4.[ValidatorHelper](./src/main/ets/utils/ValidatorHelper.ets)
> 常用正则 > 常用正则
@ -219,8 +217,8 @@ import { XWebHelper } from '@szyx/sdk_base';
const XWebview = import('../pages/XWebview'); const XWebview = import('../pages/XWebview');
XWebHelper.openWeb({ XWebHelper.openWeb({
url: 'https://www.baidu.com', url: 'https://www.baidu.com',
title: '百度一下', title: '百度一下',
}) })
``` ```
@ -328,6 +326,20 @@ import { TimeHelper } from '@szyx/sdk_base'
TimeHelper.getMonthDays() TimeHelper.getMonthDays()
``` ```
### 1.8.[ImageHelper](./src/main/ets/utils/ImageHelper.ets)
#### 1.8.1 压缩图片到指定大小
```tsx
import { ImageHelper } from '@szyx/sdk_base'
ImageHelper.compressedImage(pixelMap, this.maxCompressedImageSize).then((data: ArrayBuffer) => {
// 压缩后到图片数据
}).catch((err: BusinessError) => {
ToolsHelper.showMessage(err.message)
})
```
## 2.[Dialog](./src/main/ets/dialog) ## 2.[Dialog](./src/main/ets/dialog)
### 2.1.弹出list选中弹窗 ### 2.1.弹出list选中弹窗
@ -338,50 +350,50 @@ import { XDialogList } from '../dialog/XDialogList';
@Component @Component
struct MyView{ struct MyView{
// 控制器,控制开关 // 控制器,控制开关
dialogController: XDialogController = {} as XDialogController dialogController: XDialogController = {} as XDialogController
build() build()
{
Column()
{ {
Column() Button
{ ({ buttonStyle: ButtonStyleMode.TEXTUAL })
Button {
({ buttonStyle: ButtonStyleMode.TEXTUAL }) Image($r('sys.media.ohos_ic_public_more'))
{ .width(26).height(26)
Image($r('sys.media.ohos_ic_public_more')) }
.width(26).height(26) .
} width(65)
. .onClick(() => {
width(65) if (this.dialogController != null) {
.onClick(() => { this.dialogController.open()
if (this.dialogController != null) {
this.dialogController.open()
}
})
XDialogList({
// 控制器
controller: this.dialogController,
// 标题(可选)
title: '选择您的操作',
// 选择内容列表
values: ['刷新', '浏览器打开', '分享', '复制地址'],
// 用户选择事件
onSelected: (index: number, value: string) => {
ToolsHelper.showMessage(`用户选择了第${index}个,内容为:${value}`)
},
// 用户取消事件
onCancel: () => {
ToolsHelper.showMessage('用户取消操作')
},
// 是否可取消(点击空白处,或者物理返回键)
autoCancel: true
})
} }
. })
width('100%').height('100%')
XDialogList({
// 控制器
controller: this.dialogController,
// 标题(可选)
title: '选择您的操作',
// 选择内容列表
values: ['刷新', '浏览器打开', '分享', '复制地址'],
// 用户选择事件
onSelected: (index: number, value: string) => {
ToolsHelper.showMessage(`用户选择了第${index}个,内容为:${value}`)
},
// 用户取消事件
onCancel: () => {
ToolsHelper.showMessage('用户取消操作')
},
// 是否可取消(点击空白处,或者物理返回键)
autoCancel: true
})
} }
.
width('100%').height('100%')
}
} }
``` ```

查看文件

@ -0,0 +1,105 @@
import { image } from '@kit.ImageKit';
export class ImageHelper {
private constructor() {
}
/**
* 图片压缩
* @param sourcePixelMap原始待压缩图片的PixelMap对象
* @param maxCompressedImageSize指定图片的压缩目标大小,单位kb
* @returns ArrayBuffer返回最终压缩后的图片信息
*/
static async compressedImage(sourcePixelMap: image.PixelMap, maxCompressedImageSize: number): Promise<ArrayBuffer> {
const imagePackerApi = image.createImagePacker();
const IMAGE_QUALITY = 0;
const packOpts: image.PackingOption = { format: "image/png", 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 ImageHelper.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 ImageHelper.packing(sourcePixelMap, IMAGE_QUALITY);
} else {
// imageScale缩放小于等于0时,没有意义,结束压缩。这里不考虑图片缩放倍数小于reduceScale的情况。
break;
}
}
}
return compressedImageData
}
/**
* packing压缩
* @param sourcePixelMap原始待压缩图片的PixelMap
* @param imageQuality图片质量参数
* @returns data返回压缩后的图片数据
*/
static async 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压缩后的图片数据
*/
static async 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 ImageHelper.packing(sourcePixelMap, imageQuality);
// 判断查找一个尽可能接近但不超过压缩目标的压缩大小
if (compressedImageData.byteLength <= maxCompressedImageByte) {
left = mid + 1;
if (mid === packingArray.length - 1) {
break;
}
// 获取下一次二分的图片质量参数mid+1压缩的图片文件流数据
compressedImageData = await ImageHelper.packing(sourcePixelMap, packingArray[mid + 1]);
// 判断用下一次图片质量参数mid+1压缩的图片大小是否大于指定图片的压缩目标大小。如果大于,说明当前图片质量参数mid压缩出来的图片大小最接近指定图片的压缩目标大小。传入当前图片质量参数mid,得到最终目标图片压缩数据。
if (compressedImageData.byteLength > maxCompressedImageByte) {
compressedImageData = await ImageHelper.packing(sourcePixelMap, packingArray[mid]);
break;
}
} else {
// 目标值不在当前范围的右半部分,将搜索范围的右边界向左移动,以缩小搜索范围并继续在下一次迭代中查找左半部分。
right = mid - 1;
}
}
return compressedImageData;
}
}