はじめに
URLを与えるだけでImageView
に画像が表示できるGlide便利ですよね。円形に切り抜くcircleCrop()
は用意されていますが、それ以外の形を利用したい場合には自分で処理を書かなければいけません。この記事ではDrawable
リソースを利用して切り抜く方法について説明したいと思います。
Drawable
さえ用意できれば、三日月型でも角丸でも自由自在です。
ソースコードはこちらからどうぞ。
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
の伸縮を行うことで、アスペクト比を維持したまま画像をフィットさせています。
切り抜きの処理部分はPaint
にPorterDuffXfermode
をセットすることで、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)
おわりに
Custom transformationが思ってたよりも簡単に作れてよかったです。角丸などCropの需要は大きいと思うので、ぜひ活用してみてください。