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

鸿蒙Harmonyos实现,使用ImageKnife自定义transform来实现图片进度效果

TypeScript 复制代码
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实现

java 复制代码
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());
    }
}
相关推荐
TT_Close40 分钟前
【Flutter×鸿蒙】FVM 不认鸿蒙 SDK?4步手动塞进去
flutter·swift·harmonyos
雨白2 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk2 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
TT_Close2 小时前
【Flutter×鸿蒙】一个"插队"技巧,解决90%的 command not found
flutter·harmonyos
LING3 小时前
RN容器启动优化实践
android·react native
恋猫de小郭5 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker10 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴10 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭21 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读