鸿蒙Harmonyos实现,使用ImageKnife自定义transform来实现图片进度效果
import { Context } from '@ohos.abilityAccessCtrl';
import { image } from '@kit.ImageKit';
import { drawing } from '@kit.ArkGraphics2D';
import { GrayScaleTransformation, PixelMapTransformation } from '@ohos/imageknife';
/**
* 垂直进度灰度变换:顶部灰度,底部彩色,带波浪边界
* @param grayRatio 顶部灰度区域占比 [0-1]
* @param enableWave 是否启用波浪边界效果,false为直线分割(性能更佳)
*/
@Sendable
export class VerticalProgressGrayscaleTransformation extends PixelMapTransformation {
private readonly grayRatio: number;
private readonly enableWave: boolean;
private static readonly WAVE_AMPLITUDE_RATIO: number = 0.08; // 波浪振幅比例
private static readonly WAVE_FREQUENCY: number = 2.5; // 波浪频率
private static readonly WAVE_STEPS: number = 40; // 波浪平滑度(降低提升性能)
constructor(grayRatio: number, enableWave: boolean = true) {
super();
this.grayRatio = Math.max(0, Math.min(1, grayRatio));
this.enableWave = enableWave;
}
override async transform(context: Context, toTransform: PixelMap, width: number, height: number): Promise<PixelMap> {
try {
// 边界情况快速处理
if (this.grayRatio <= 0.001) {
return toTransform;
}
if (this.grayRatio >= 0.999) {
return new GrayScaleTransformation().transform(context, toTransform, width, height);
}
// 获取实际图片尺寸
const imageInfo = await toTransform.getImageInfo();
if (!imageInfo.size) {
return toTransform;
}
const actualWidth = imageInfo.size.width;
const actualHeight = imageInfo.size.height;
const grayHeight = Math.floor(actualHeight * this.grayRatio);
// 如果灰度区域太小,直接返回原图
if (grayHeight < 5) {
return toTransform;
}
return this.applyVerticalGrayEffect(context, toTransform, actualWidth, actualHeight, grayHeight);
} catch (err) {
console.error('[VerticalProgressGrayscaleTransformation] Error:', err);
return toTransform;
}
}
/**
* 应用垂直灰度效果(性能优化版)
*/
private async applyVerticalGrayEffect(
context: Context,
original: PixelMap,
width: number,
height: number,
grayHeight: number
): Promise<PixelMap> {
try {
// 创建结果PixelMap并复制原图
const result = await this.createClonedPixelMap(original, width, height);
// 创建灰度图
const grayPixelMap = await new GrayScaleTransformation().transform(context, original, width, height);
// 一次性完成Canvas操作
const canvas = new drawing.Canvas(result);
canvas.save();
// 设置裁剪路径并绘制灰度区域
canvas.clipPath(this.createOptimizedClipPath(width, grayHeight));
canvas.drawImage(grayPixelMap, 0, 0, new drawing.SamplingOptions(drawing.FilterMode.FILTER_MODE_LINEAR));
canvas.restore();
return result;
} catch (error) {
console.error('[VerticalProgressGrayscaleTransformation] Apply effect error:', error);
return original;
}
}
/**
* 创建克隆PixelMap(优化版)
*/
private async createClonedPixelMap(original: PixelMap, width: number, height: number): Promise<PixelMap> {
const opts: image.InitializationOptions = {
size: { width, height },
pixelFormat: image.PixelMapFormat.RGBA_8888,
editable: true,
alphaType: image.AlphaType.PREMUL
};
const cloned = await image.createPixelMap(new ArrayBuffer(width * height * 4), opts);
new drawing.Canvas(cloned).drawImage(original, 0, 0);
return cloned;
}
/**
* 创建优化的分割路径(波浪或直线)
*/
private createOptimizedClipPath(width: number, grayHeight: number): drawing.Path {
const path = new drawing.Path();
// 直线分割模式(高性能)
if (!this.enableWave) {
path.addRect({
left: 0,
top: 0,
right: width,
bottom: grayHeight
});
return path;
}
// 波浪分割模式
const amplitude = Math.min(25, grayHeight * VerticalProgressGrayscaleTransformation.WAVE_AMPLITUDE_RATIO);
// 波浪太小时使用直线
if (amplitude < 2) {
path.addRect({
left: 0,
top: 0,
right: width,
bottom: grayHeight
});
return path;
}
// 构建波浪路径
path.moveTo(0, 0);
path.lineTo(width, 0);
path.lineTo(width, grayHeight);
// 优化的波浪计算
const steps = VerticalProgressGrayscaleTransformation.WAVE_STEPS;
const stepWidth = width / steps;
const waveFreq = VerticalProgressGrayscaleTransformation.WAVE_FREQUENCY;
let prevX = width;
let prevY = grayHeight;
for (let i = 1; i <= steps; i++) {
const x = width - i * stepWidth;
const wavePhase = (i / steps) * Math.PI * 2 * waveFreq;
const y = grayHeight + amplitude * Math.sin(wavePhase);
// 使用二次贝塞尔曲线优化连接
const controlX = (prevX + x) * 0.5;
const controlY = (prevY + y) * 0.5;
path.quadTo(controlX, controlY, x, y);
prevX = x;
prevY = y;
}
path.lineTo(0, 0);
path.close();
return path;
}
getKey(): string {
return `VerticalProgressGray_${this.grayRatio}_${this.enableWave ? 'wave' : 'line'}`;
}
}
Android实现,使用Glide自定义transform实现
public class VerticalProgressGrayscaleTransformation extends BitmapTransformation {
private final float grayRatio; // 灰度区域高度占比,取值范围 [0, 1]
public VerticalProgressGrayscaleTransformation(float grayRatio) {
this.grayRatio = grayRatio;
}
@Override
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
int width = toTransform.getWidth();
int height = toTransform.getHeight();
Bitmap.Config config = toTransform.getConfig() != null ? toTransform.getConfig() : Bitmap.Config.ARGB_8888;
Bitmap bitmap = pool.get(width, height, config);
Canvas canvas = new Canvas(bitmap);
// 1. 首先绘制完整的原始彩色图像作为底层
canvas.drawBitmap(toTransform, 0, 0, null);
// 对 grayRatio 进行边界处理,确保在 [0, 1] 范围内
float clampedGrayRatio = Math.max(0f, Math.min(1f, this.grayRatio));
// 只有当灰度占比大于一个极小值时才应用灰度效果
if (clampedGrayRatio > 0.001f) {
Paint grayPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 添加抗锯齿
ColorMatrix matrix = new ColorMatrix(new float[]{
0.299f, 0.587f, 0.114f, 0, 0,
0.299f, 0.587f, 0.114f, 0, 0,
0.299f, 0.587f, 0.114f, 0, 0,
0, 0, 0, 1, 0,
});
grayPaint.setColorFilter(new ColorMatrixColorFilter(matrix));
// 波浪的基准Y坐标,即灰色区域的平均下边界
int waveBaseY = (int) (height * clampedGrayRatio);
Path grayRegionPath = new Path();
grayRegionPath.moveTo(0, 0); // 移动到左上角
grayRegionPath.lineTo(width, 0); // 画到右上角
// 定义波浪的振幅
float amplitude;
if (clampedGrayRatio <= 0.001f || clampedGrayRatio >= 0.999f) {
amplitude = 0f; // 如果完全着色或完全灰色,则没有波浪
} else {
float baseAmplitude = height * 0.03f; // 基础振幅为图片高度的3%
// 确保振幅不会使波浪超出图片顶部或底部
amplitude = Math.min(baseAmplitude, waveBaseY);
amplitude = Math.min(amplitude, height - waveBaseY);
}
// 从右向左绘制波浪线作为灰色区域的下边界
grayRegionPath.lineTo(width, waveBaseY); // 连接到右侧波浪基准点
int numCycles = 3; // 波浪周期数
float waveLength = (float) width / numCycles; // 每个周期的长度
float currentX = width;
for (int i = 0; i < numCycles * 2; i++) { // 每个周期包含一个波峰和波谷,共 numCycles * 2段
float nextX = Math.max(0, currentX - waveLength / 2); // 下一个X点
float controlX = (currentX + nextX) / 2; // 控制点X坐标
// 控制点Y坐标,交替形成波峰和波谷
// 从右往左画,i为偶数时是波峰(相对基准线向上,Y值减小),奇数时是波谷(Y值增大)
float controlY = waveBaseY + ((i % 2 == 0) ? -amplitude : amplitude);
grayRegionPath.quadTo(controlX, controlY, nextX, waveBaseY); // 二阶贝塞尔曲线
currentX = nextX;
if (currentX == 0) {
break; // 到达左边界
}
}
grayRegionPath.lineTo(0, waveBaseY); // 确保连接到左侧波浪基准点
grayRegionPath.close(); // 闭合路径,连接回 (0,0)
// 保存画布状态
canvas.save();
// 将画布裁剪为波浪路径定义的区域
canvas.clipPath(grayRegionPath);
//在裁剪区域内,使用灰度画笔再次绘制原始图像
canvas.drawBitmap(toTransform, 0, 0, grayPaint);
// 恢复画布状态
canvas.restore();
}
return bitmap;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(("VerticalProgressGrayscaleTransformation" + grayRatio).getBytes());
}
}