Glide 的 transformation

Glide 裏面內置了一套圖片改造機制,名叫 Transformation ;利用這套機制,我們可以輕鬆實現以下的效果:

在這裏插入圖片描述

調用的方法也很簡單,就拿其中一個來舉例:

RequestOptions requestOptions = new RequestOptions()
   .transforms(new CenterCrop(), new CircleCrop());
Glide.with(this)
     .load("圖片url")
     .apply(requestOptions)
     .into(imageViewRes);

我們調用的 api 非常簡單,但是我們需要知其所以然,因此我們有必要去深入源碼去看下

源碼分析

我們先來看下 CenterCropRoundedCorners 到底是怎麼回事,然後我們會發現它們都是 Transformation 的子類,具體關係如下:

在這裏插入圖片描述

我們先來前面的幾個類吧

Key

這是 Glide 用於生成加密簽名的接口類,供子類去實現生成簽名的方法:

public interface Key {
    /**
     * 添加一個加密的簽名
     *
     * @param messageDigest 用於提供信息加密算法的功能,如 MD5 或 SHA 算法
     */
    void updateDiskCacheKey(MessageDigest messageDigest);
   
    @Override
    boolean equals(Object o);
   
    @Override
    int hashCode();
}
Transformation

Transformation(轉換器),這個類可以說是 Glide 壓縮裁剪圖片的核心類,因爲該類功能就是依據要求輸出的寬高對原始資源進行壓縮裁剪的轉換

public interface Transformation<T> extends Key {
  
  /**
   * 轉換原始資源並返回轉換後的資源對象
   */
  Resource<T> transform(@NonNull Context context, @NonNull Resource<T> resource,
                          int outWidth, int outHeight);
}
BitmapTransformation

這個是 Transformation 的一個子類,它的功能就和它的名字一樣,主要負責將圖片資源轉換爲 Bitmap ,Glide 在這裏處理了一些 Bitmap 複用的邏輯:

public abstract class BitmapTransformation implements Transformation<Bitmap> {
  @Override
    public final Resource<Bitmap> transform(
            Context context,Resource<Bitmap> resource, int outWidth, int outHeight) {
        if (!Util.isValidDimensions(outWidth, outHeight)) {
            throw new IllegalArgumentException(
                    "Cannot apply transformation on width: " + outWidth + " or height: " + outHeight + " less than or equal to zero and not Target.SIZE_ORIGINAL");
        }
        //獲取Glide的Bitmap複用池
        BitmapPool bitmapPool = Glide.get(context).getBitmapPool();
        //從包裝類中取出Bitmap
        Bitmap toTransform = resource.get();
        //對比得知這次圖片轉換過程中的圖片裁剪尺寸
        int targetWidth = outWidth == Target.SIZE_ORIGINAL ? toTransform.getWidth() : outWidth;
        int targetHeight = outHeight == Target.SIZE_ORIGINAL ? toTransform.getHeight() : outHeight;
        //得到圖片轉換後的Bitmap對象
        Bitmap transformed = transform(bitmapPool, toTransform, targetWidth, targetHeight);
        //將Bitmap對象包裹起來進行傳遞
        final Resource<Bitmap> result;
        if (toTransform.equals(transformed)) {
            result = resource;
        } else {
            result = BitmapResource.obtain(transformed, bitmapPool);
        }
        return result;
    }
  
  /**
   * 轉換原始資源並返回轉換後的Bitmap對象
   * 供子類去實現
   */
   protected abstract Bitmap transform(
            BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight);
}

前面幾個類都介紹完了,其實到這裏大概也可以猜到了,CenterCropRoundedCorners 等,都是 BitmapTransformation 的子類,都是某種圖片資源的壓縮裁剪的具體實現:

public class CircleCrop extends BitmapTransformation {

    @Override
    protected Bitmap transform(BitmapPool pool,Bitmap toTransform, int outWidth, int outHeight) {
        //核心方法
        return TransformationUtils.circleCrop(pool, toTransform, outWidth, outHeight);
    }

    @Override
    public void updateDiskCacheKey(MessageDigest messageDigest) {
        messageDigest.update(ID_BYTES);
    }
}

去看源碼的話,可以發現 CenterCropFitCenterCenterInside 的源碼裏面,最終都是調用了 TransformationUtils 裏面的對應壓縮裁剪方法來生成指定的 Bitmap 對象,基本可以認定 TransformationUtils 就是 Glide 用來壓縮裁剪圖片的工具類

TransformationUtils

從上面得知,TransformationUtils 就是 Glide 用來壓縮裁剪圖片的工具類,Glide 關於圖片的壓縮裁剪方法都是在這個工具類裏面,建議大家去看下這裏的源碼,可以知道 Glide 對圖片壓縮裁剪的一些套路,這裏就簡單分析一兩個方法吧,先看 circleCrop 裏面的裁剪圓形圖片的方法:

 /**
     * 使用圖片混合模式(PorterDuff.Mode.SRC_IN)將圖片轉爲圓形並調整到指定的寬度/高度
     *
     * @param pool     一個{@link BitmapPool} 對象,用於存儲或返回準備轉換的{@link Bitmap}對象
     * @param inBitmap 準備進行裁剪壓縮的圖片資源 {@link Bitmap}
     * @param width    轉換後的目標寬度
     * @param height   轉換後的目標高度
     * @return 圓形的  {@link Bitmap} 或者null
     */
    public static Bitmap circleCrop(BitmapPool pool,Bitmap inBitmap,
                                    int destWidth, int destHeight) {
        //計算出圓形的半徑
        int destMinEdge = Math.min(destWidth, destHeight);
        float radius = destMinEdge / 2f;

        int srcWidth = inBitmap.getWidth();
        int srcHeight = inBitmap.getHeight();
        //計算出最大縮小倍數
        float scaleXv = destMinEdge / (float) srcWidth;
        float scaleYv = destMinEdge / (float) srcHeight;
        float maxScale = Math.max(scaleXv, scaleYv);
        //計算出裁剪的中心區域
        float scaledWidth = maxScale * srcWidth;
        float scaledHeight = maxScale * srcHeight;
        float left = (destMinEdge - scaledWidth) / 2f;
        float top = (destMinEdge - scaledHeight) / 2f;
        RectF destRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
        //從池中取出可複用的Bitmap(使用inBitmap重新繪製該複用的Bitmap)
        Bitmap toTransform = getAlphaSafeBitmap(pool, inBitmap);
        //從池中取出一個可服用的Bitmap對象用於繪製toTransform
        //減少內存抖動
        Bitmap.Config outConfig = getAlphaSafeConfig(inBitmap);
        Bitmap result = pool.get(destMinEdge, destMinEdge, outConfig);
        result.setHasAlpha(true);
        //上鎖
        BITMAP_DRAWABLE_LOCK.lock();
        try {
            Canvas canvas = new Canvas(result);
            //將Canvas轉爲圓形Canvas
            canvas.drawCircle(radius, radius, radius, CIRCLE_CROP_SHAPE_PAINT);
            //在圓形Canvas上面繪製圖片
            canvas.drawBitmap(toTransform, null, destRect, CIRCLE_CROP_BITMAP_PAINT);
            //清空Canvas
            clear(canvas);
        } finally {
            BITMAP_DRAWABLE_LOCK.unlock();
        }
        if (!toTransform.equals(inBitmap)) {
            //存入進行復用
            pool.put(toTransform);
        }
        return result;
    }

核心地方都上面都寫好註釋了,應該蠻好理解的,因爲這裏就只是對圖片進行區域摳圖以及進行了一次圖片混合模式,不過這裏是不是有什麼問題呢?

是不是少了一些操作?是的,這裏並沒有對原圖進行壓縮裁剪的操作!

這就意味着這裏是針對傳入的圖片進行直接操作的,如果我們直接使用 circleCrop 的話,有相對大的 OOM 風險,因爲很有可能是直接對下載的原圖進行直接操作,沒有經過任何的壓縮裁剪處理!

這也是爲什麼一開始貼的代碼裏面,會有一個 CenterCrop 的原因:

RequestOptions requestOptions = new RequestOptions()
   .transforms(new CenterCrop(), new CircleCrop());

如果無法確保操作圖片的尺寸大小的情況下,建議配合具備壓縮裁剪功能的工具類來使用,來看下 TransformationUtilscenterCrop 方法吧,CenterCrop 裏面最終調用的核心方法就是這個:

 public static Bitmap centerCrop(BitmapPool pool,Bitmap inBitmap, int width,int height) {
        if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) {
            return inBitmap;
        }
        //擴大或縮小的倍數
        final float scale;
        //x軸的平移距離
        final float dx;
        //y軸的平移距離
        final float dy;
        Matrix matrix = new Matrix();
        if (inBitmap.getWidth() * height > inBitmap.getHeight() * width) {
            scale = (float) height / (float) inBitmap.getHeight();
            dx = (width - inBitmap.getWidth() * scale) * 0.5f;
            dy = 0;
        } else {
            scale = (float) width / (float) inBitmap.getWidth();
            dx = 0;
            dy = (height - inBitmap.getHeight() * scale) * 0.5f;
        }
        //設置縮放(擴大)倍數
        matrix.setScale(scale, scale);
        //設置平移,確保繪製的是圖片的中央部分
        matrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
        //取出複用的bitmap對象
        Bitmap result = pool.get(width, height, getNonNullConfig(inBitmap));
        //保持透明度一致
        TransformationUtils.setAlpha(inBitmap, result);
        //進行矩陣轉換(也就是對圖片進行壓縮裁剪)
        applyMatrix(inBitmap, result, matrix);
        return result;
    }

    private static void applyMatrix(Bitmap inBitmap, Bitmap targetBitmap, Matrix matrix) {
        //上鎖
        BITMAP_DRAWABLE_LOCK.lock();
        try {
            //新的畫布
            Canvas canvas = new Canvas(targetBitmap);
            //在畫布上繪製新的位圖並使用matrix進行轉換
            canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT);
            //清空畫布
            clear(canvas);
        } finally {
            BITMAP_DRAWABLE_LOCK.unlock();
        }
    }

可以看到 Glide 對圖片進行壓縮裁剪是通過 Matrix 來進行線性壓縮的,這種壓縮方法相對耗時,不過壓縮過程一般都是在子線程裏面,所以影響不怎麼大

自定義Transformation

或者有人會覺得同時使用 CenterCropCircleCrop 會比較麻煩,那麼可以自己去定義一個 Transformation , 針對靜態圖片,通過繼承 BitmapTransformation 來實現自己的變換就可以了,比分說給圓形圖片加個外邊框:

public class CircleCropWithBorderCorp extends BitmapTransformation {
   private final Paint mPaint;

   public CircleCropWithBorderCorp(int dp) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(dp);
        mPaint.setColor(Color.RED);
   }
  
  @Override
   protected Bitmap transform(BitmapPool pool,  Bitmap toTransform
     , int outWidth, int outHeight) {
        Bitmap bitmap = TransformationUtils.centerCrop(pool, toTransform, outWidth, outHeight);
        Bitmap result = TransformationUtils.circleCrop(pool, bitmap, outWidth, outHeight);
        int destMinEdge = Math.min(outWidth, outHeight);
        int radius = (int) (destMinEdge / 2f);
        Canvas canvas = new Canvas(result);
        canvas.drawCircle(radius, radius, radius - mPaint.getStrokeWidth() / 2, mPaint);
        return result;
    }
}

這樣子就可以了,然後使用一下:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ImageView show = findViewById(R.id.iv_show);
        RequestOptions options = new RequestOptions().transform(
          new CircleCropWithBorderCorp(10));
        Glide.with(this)
          .load("圖片url")
          .apply(options)
          .into(show);
    }

運行效果如下:

在這裏插入圖片描述

多個Transformation

這裏還有一個疑問,通過自定義 transformation 可以將 CenterCropCircleCrop 的效果合併在一起,那麼我們一開始配置:

RequestOptions requestOptions = new RequestOptions()
   .transforms(new CenterCrop(), new CircleCrop());

它們是怎麼合併到一起的?再去源碼看下,可以發現:

public RequestOptions transforms(@NonNull Transformation<Bitmap>... transformations) {
  return transform(new MultiTransformation<>(transformations), /*isRequired=*/ true);
}

它們都是傳給 MultiTransformation 來做構建參數,然後去查看 MultiTransformation 的源碼的話,就可以看到下面的代碼:

public class MultiTransformation<T> implements Transformation<T> {
  //省去部分代碼
   public Resource<T> transform(Context context, Resource<T> resource
                                , int outWidth, int outHeight) {
    Resource<T> previous = resource;
    //遍歷全部到Transformation並不斷迭代轉換
    for (Transformation<T> transformation : transformations) {
      Resource<T> transformed = transformation.transform(context, previous, outWidth, outHeight);
      if (previous != null && !previous.equals(resource) && !previous.equals(transformed)) {
        previous.recycle();
      }
      previous = transformed;
    }
    return previous;
  }
  //省去部分代碼
}

現在就很清楚了, MultiTransformation 是通過 for 循環對全部到 Transformation 進行迭代轉換,從而將不同的 Transformation 的效果進行合併

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章