Android及Harmonyos实现图片进度显示效果

发布于:2025-06-18 ⋅ 阅读:(17) ⋅ 点赞:(0)

鸿蒙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());
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到