LoginSignup
8
5

More than 5 years have passed since last update.

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

Posted at

はじめに

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

sample.pngDrawableさえ用意できれば、三日月型でも角丸でも自由自在です。
ソースコードはこちらからどうぞ。

Custom transformations

Transformationを継承したクラスをtransforms()で渡すことで、Glideで読み込む画像にオリジナルの効果を付け足すことが出来ます。公式ドキュメントcircleCrop()のソースコードを参考にして書いていきます。

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

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をリソースから取得する処理を追加します。

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()のオーバーライドが必須なようなので、公式サンプルの通りに実装します

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()を実装すれば完成です。

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の伸縮を行うことで、アスペクト比を維持したまま画像をフィットさせています。
切り抜きの処理部分はPaintPorterDuffXfermodeをセットすることで、APIに丸投げしています。PorterDuffXfermodeについてはこの記事がわかりやすかったです。

CropTransformation


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)を指定します。

実際に利用する

左上から右下の順で記述してあります。

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

おわりに

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

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5