鴻蒙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;}@Overrideprotected 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;}@Overridepublic void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {messageDigest.update(("VerticalProgressGrayscaleTransformation" + grayRatio).getBytes());}
}