[Android] GlideでcircleCrop以外のCropをする

# はじめに
URLを与えるだけで`ImageView`に画像が表示できるGlide便利ですよね。円形に切り抜く`circleCrop()`は用意されていますが、それ以外の形を利用したい場合には自分で処理を書かなければいけません。この記事では`Drawable`リソースを利用して切り抜く方法について説明したいと思います。

![sample.png](https://qiita-image-store.s3.amazonaws.com/0/152199/c794d445-fade-9a9b-5bcf-2071e1520b4c.png)`Drawable`さえ用意できれば、三日月型でも角丸でも自由自在です。
ソースコードは[こちら](https://github.com/shiita0903/GlideCrop)からどうぞ。

# Custom transformations
`Transformation`を継承したクラスを`transforms()`で渡すことで、Glideで読み込む画像にオリジナルの効果を付け足すことが出来ます。[公式ドキュメント](https://bumptech.github.io/glide/doc/transformations.html)と`circleCrop()`のソースコードを参考にして書いていきます。

`BitmapTransformation`を継承したものがサンプルとして示されていますが、`Drawable`をリソースから取得するための`Context`が必要となることを考慮して、`BitmapTransformation`で実装されている`Transformation<Bitmap>`を直接実装したクラスを作ります。`BitmapTransformation`の実装は以下のようになっているので

```java
public abstract class BitmapTransformation implements Transformation<Bitmap> {

  @NonNull
  @Override
  public final Resource<Bitmap> transform(
      @NonNull Context context, @NonNull 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");
    }
    BitmapPool bitmapPool = Glide.get(context).getBitmapPool();
    Bitmap toTransform = resource.get();
    int targetWidth = outWidth == Target.SIZE_ORIGINAL ? toTransform.getWidth() : outWidth;
    int targetHeight = outHeight == Target.SIZE_ORIGINAL ? toTransform.getHeight() : outHeight;
    Bitmap transformed = transform(bitmapPool, toTransform, targetWidth, targetHeight);

    final Resource<Bitmap> result;
    if (toTransform.equals(transformed)) {
      result = resource;
    } else {
      result = BitmapResource.obtain(transformed, bitmapPool);
    }
    return result;
  }

  protected abstract Bitmap transform(
      @NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight);
}
```

`Resource<Bitmap>`を返す方の`transform()`を参考に、`Drawable`をリソースから取得する処理を追加します。

```kotlin
override fun transform(context: Context, resource: Resource<Bitmap>, outWidth: Int, outHeight: Int): Resource<Bitmap> {
    if (!Util.isValidDimensions(outWidth, outHeight))
        throw IllegalArgumentException("Cannot apply transformation on width: $outWidth or height: $outHeight less than or equal to zero and not Target.SIZE_ORIGINAL")

    val bitmapPool = Glide.get(context).bitmapPool
    val toTransform = resource.get()
    val targetWidth = if (outWidth == Target.SIZE_ORIGINAL) toTransform.width else outWidth
    val targetHeight = if (outHeight == Target.SIZE_ORIGINAL) toTransform.height else outHeight
    val drawable = ResourcesCompat.getDrawable(context.resources, resId, null)
    val transformed = transform(bitmapPool, toTransform, targetWidth, targetHeight, drawable)

    return if (toTransform == transformed) resource else BitmapResource.obtain(transformed, bitmapPool)!!
}
```

`equals()`、`hashCode()`、`updateDiskCacheKey()`のオーバーライドが必須なようなので、公式サンプルの通りに実装します

```kotlin
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
    messageDigest.update(ID_BYTES)
    messageDigest.update(resId.toByte())
    messageDigest.update(if (out) Byte.MAX_VALUE else Byte.MIN_VALUE)
}

override fun equals(other: Any?): Boolean =
        other is CropTransformation && resId == other.resId && out == other.out

override fun hashCode(): Int =
        Util.hashCode(ID.hashCode(), Util.hashCode(resId, Util.hashCode(out)))

companion object {
    private val ID = CropTransformation::class.java.name
    private val ID_BYTES = ID.toByteArray(Charsets.UTF_8)
}
```

最後に実際のCrop処理を行う`transform()`を実装すれば完成です。

```kotlin
private fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int, drawable: Drawable?): Bitmap {
    drawable ?: return toTransform

    val srcWidth = toTransform.width
    val srcHeight = toTransform.height
    val scaleX = outWidth / srcWidth.toFloat()
    val scaleY = outHeight / srcHeight.toFloat()
    val maxScale = max(scaleX, scaleY)

    val scaledWidth = maxScale * srcWidth
    val scaledHeight = maxScale * srcHeight
    val left = (outWidth - scaledWidth) / 2f
    val top = (outHeight - scaledHeight) / 2f
    val destRect = RectF(left, top, left + scaledWidth, top + scaledHeight)

    val bitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    val paint = if (out) SRC_OUT_PAINT else SRC_IN_PAINT

    drawable.bounds = Rect(0, 0, outWidth, outHeight)
    drawable.draw(canvas)
    canvas.drawBitmap(toTransform, null, destRect, paint)
    return bitmap
}

companion object {
    private const val PAINT_FLAGS = Paint.DITHER_FLAG or Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG
    private val SRC_OUT_PAINT = Paint(PAINT_FLAGS).apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OUT) }
    private val SRC_IN_PAINT = Paint(PAINT_FLAGS).apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) }
}
```

`transform()`の実装は`circleCrop()`のソースコードをほぼ再利用しています。縦と横の倍率(入力サイズと出力サイズの比率)をそれぞれ計算し、大きい方に合わせて`Bitmap`の伸縮を行うことで、アスペクト比を維持したまま画像をフィットさせています。
切り抜きの処理部分は`Paint`に`PorterDuffXfermode`をセットすることで、APIに丸投げしています。`PorterDuffXfermode`については[この記事](https://tech.recruit-mp.co.jp/mobile/remember_canvas2/)がわかりやすかったです。

## CropTransformation
```kotlin

class CropTransformation(@DrawableRes private val resId: Int, private val out: Boolean = false) : Transformation<Bitmap> {
    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        messageDigest.update(ID_BYTES)
        messageDigest.update(resId.toByte())
        messageDigest.update(if (out) Byte.MAX_VALUE else Byte.MIN_VALUE)
    }

    override fun transform(context: Context, resource: Resource<Bitmap>, outWidth: Int, outHeight: Int): Resource<Bitmap> {
        if (!Util.isValidDimensions(outWidth, outHeight))
            throw IllegalArgumentException("Cannot apply transformation on width: $outWidth or height: $outHeight less than or equal to zero and not Target.SIZE_ORIGINAL")

        val bitmapPool = Glide.get(context).bitmapPool
        val toTransform = resource.get()
        val targetWidth = if (outWidth == Target.SIZE_ORIGINAL) toTransform.width else outWidth
        val targetHeight = if (outHeight == Target.SIZE_ORIGINAL) toTransform.height else outHeight
        val drawable = ResourcesCompat.getDrawable(context.resources, resId, null)
        val transformed = transform(bitmapPool, toTransform, targetWidth, targetHeight, drawable)

        return if (toTransform == transformed) resource else BitmapResource.obtain(transformed, bitmapPool)!!
    }

    private fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int, drawable: Drawable?): Bitmap {
        drawable ?: return toTransform

        val srcWidth = toTransform.width
        val srcHeight = toTransform.height
        val scaleX = outWidth / srcWidth.toFloat()
        val scaleY = outHeight / srcHeight.toFloat()
        val maxScale = max(scaleX, scaleY)

        val scaledWidth = maxScale * srcWidth
        val scaledHeight = maxScale * srcHeight
        val left = (outWidth - scaledWidth) / 2f
        val top = (outHeight - scaledHeight) / 2f
        val destRect = RectF(left, top, left + scaledWidth, top + scaledHeight)

        val bitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        val paint = if (out) SRC_OUT_PAINT else SRC_IN_PAINT

        drawable.bounds = Rect(0, 0, outWidth, outHeight)
        drawable.draw(canvas)
        canvas.drawBitmap(toTransform, null, destRect, paint)
        return bitmap
    }

    override fun equals(other: Any?): Boolean =
            other is CropTransformation && resId == other.resId && out == other.out

    override fun hashCode(): Int =
            Util.hashCode(ID.hashCode(), Util.hashCode(resId, Util.hashCode(out)))

    companion object {
        private val ID = CropTransformation::class.java.name
        private val ID_BYTES = ID.toByteArray(Charsets.UTF_8)
        private const val PAINT_FLAGS = Paint.DITHER_FLAG or Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG
        private val SRC_OUT_PAINT = Paint(PAINT_FLAGS).apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OUT) }
        private val SRC_IN_PAINT = Paint(PAINT_FLAGS).apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) }
    }
}
```

最終的に完成したクラスはこのようになります。コンストラクタで`Drawable`のリソースIDと切り抜きのオプション(`SRC_IN` or `SRC_OUT`)を指定します。

# 実際に利用する
左上から右下の順で記述してあります。

```kotlin
GlideApp.with(this)
        .load(URL)
        .into(defaultImageView)

GlideApp.with(this)
        .load(URL)
        .circleCrop()
        .into(circleCropImageView)

GlideApp.with(this)
        .load(URL)
        .transform(CropTransformation(R.drawable.ic_brightness))
        .into(drawableCropImageView)

GlideApp.with(this)
        .load(URL)
        .transform(CropTransformation(R.drawable.ic_brightness, true))
        .into(drawableCropOutImageView)

GlideApp.with(this)
        .load(URL)
        .transform(CropTransformation(R.drawable.rounded_rectangle))
        .into(roundedRectangleImageView)
```
![sample.png](https://qiita-image-store.s3.amazonaws.com/0/152199/c794d445-fade-9a9b-5bcf-2071e1520b4c.png)

# おわりに
Custom transformationが思ってたよりも簡単に作れてよかったです。角丸などCropの需要は大きいと思うので、ぜひ活用してみてください。