在 Android 開發中,Glide 的強大不僅在于其高效的加載和緩存能力,更在于其無與倫比的可擴展性,尤其是在圖像處理層面。當內置的?fitCenter()
?和?circleCrop()
?無法滿足你的設計需求時,自定義?Transformation
?便是你的終極武器。本文將深入探討如何創建自定義變換,處理不同資源類型,并組合它們以實現復雜的效果。
1. 自定義 Transformation 的核心原理
在 Glide 中,一個?Transformation
?負責在圖片被顯示到?ImageView
?之前對其進行修改。它接收一個?Resource<T>
?對象(通常是?Bitmap
?或?GifDrawable
),并返回一個包含修改后數據的新的?Resource<T>
?對象。
關鍵生命周期:
transform
: 核心方法,在此執行實際的圖像變換邏輯。updateDiskCacheKey
:?極其重要的方法,用于生成唯一的緩存鍵。Glide 使用此鍵來緩存變換后的結果。如果兩個變換的邏輯相同,它們的?updateDiskCacheKey
?輸出也必須相同,否則將導致錯誤的緩存命中或未命中。equals
/hashCode
: 必須正確重寫,用于內存緩存和變換對象的復用判斷。
2. 實現自定義 BitmapTransformation
對于靜態圖片,最常用的是繼承?BitmapTransformation
?抽象類。它已經幫你處理了部分樣板代碼(如資源管理),你只需專注于?Bitmap
?的變換邏輯。
下面我們實現三個經典效果:黑白、圓角、毛玻璃。
a) 黑白(灰度)效果
kotlin
import android.graphics.* import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import java.security.MessageDigestclass GrayscaleTransformation : BitmapTransformation() {override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {// 1. 從BitmapPool中獲取一個可重用的Bitmap,避免頻繁創建對象,優化性能。val result = pool.get(source.width, source.height, Bitmap.Config.ARGB_8888)?: Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)// 2. 使用Canvas和ColorMatrix來應用灰度效果val canvas = Canvas(result)val paint = Paint()val colorMatrix = ColorMatrix()colorMatrix.setSaturation(0f) // 將飽和度設置為0即可得到灰度圖paint.colorFilter = ColorMatrixColorFilter(colorMatrix)canvas.drawBitmap(source, 0f, 0f, paint)// 3. 如果result是從pool中get的,可以安全返回。如果是新創建的,也需要返回。return result}// 必須重寫此方法,為變換生成唯一的緩存標識符。override fun updateDiskCacheKey(messageDigest: MessageDigest) {messageDigest.update("grayscale_transformation".toByteArray())}// 重寫equals和hashCode是Glide內存緩存機制正確工作的保證。override fun equals(other: Any?): Boolean {return other is GrayscaleTransformation}override fun hashCode(): Int {return "grayscale_transformation".hashCode()} }
使用方式:
kotlin
Glide.with(context).load(url).transform(GrayscaleTransformation()).into(imageView)
b) 圓角效果(支持任意角)
內置的?RoundedCorners
?變換要求所有角半徑相同。自定義可以實現更靈活的效果。
kotlin
class CustomRoundedCornersTransformation(private val topLeft: Float,private val topRight: Float,private val bottomRight: Float,private val bottomLeft: Float ) : BitmapTransformation() {override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {val width = source.widthval height = source.heightval result = pool.get(width, height, Bitmap.Config.ARGB_8888)?: Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)val canvas = Canvas(result)val paint = Paint(Paint.ANTI_ALIAS_FLAG) // 關鍵:開啟抗鋸齒// 設置BitmapShader,將原圖作為紋理val shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)paint.shader = shader// 繪制圓角路徑val path = Path()val radii = floatArrayOf(topLeft, topLeft,topRight, topRight,bottomRight, bottomRight,bottomLeft, bottomLeft)path.addRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()), radii, Path.Direction.CCW)canvas.drawPath(path, paint)return result}override fun updateDiskCacheKey(messageDigest: MessageDigest) {messageDigest.update("rounded_${topLeft}_${topRight}_${bottomRight}_${bottomLeft}".toByteArray())}override fun equals(other: Any?): Boolean {if (other !is CustomRoundedCornersTransformation) return falsereturn topLeft == other.topLeft &&topRight == other.topRight &&bottomRight == other.bottomRight &&bottomLeft == other.bottomLeft}override fun hashCode(): Int {var result = topLeft.hashCode()result = 31 * result + topRight.hashCode()result = 31 * result + bottomRight.hashCode()result = 31 * result + bottomLeft.hashCode()return result} }
使用方式:
kotlin
// 僅左上和右上有20像素圓角 Glide.with(context).load(url).transform(CustomRoundedCornersTransformation(20f, 20f, 0f, 0f)).into(imageView)
c) 毛玻璃(模糊)效果
kotlin
import androidx.annotation.IntRange import android.renderscript.*class BlurTransformation(private val context: Context,@IntRange(from = 1, to = 25) private val radius: Int = 10 ) : BitmapTransformation() {// 使用RenderScript進行高效模糊(注意:RenderScript API已deprecated,但很多項目仍在用)// 替代方案可使用Coil庫的BlurTransformation或自己實現RenderScript的替代品override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {val width = source.widthval height = source.heightval result = pool.get(width, height, Bitmap.Config.ARGB_8888)?: Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)val rs = RenderScript.create(context)val input = Allocation.createFromBitmap(rs, source)val output = Allocation.createTyped(rs, input.type)val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))script.setRadius(radius.coerceAtMost(25).toFloat())script.setInput(input)script.forEach(output)output.copyTo(result)// 及時回收資源input.destroy()output.destroy()script.destroy()rs.destroy()return result}override fun updateDiskCacheKey(messageDigest: MessageDigest) {messageDigest.update("blur_$radius".toByteArray())}override fun equals(other: Any?): Boolean {if (other !is BlurTransformation) return falsereturn radius == other.radius}override fun hashCode(): Int {return "blur_$radius".hashCode()} }
注意:由于 RenderScript 已被棄用,在新項目中可以考慮使用其他高效的模糊算法庫。
3. 處理 GIF:Transformation<GifDrawable>
如果你想對 GIF 動畫的每一幀都應用變換(例如讓一個 GIF 變成黑白動畫),你需要實現?Transformation<GifDrawable>
?接口。
kotlin
import com.bumptech.glide.load.resource.gif.GifDrawable import com.bumptech.glide.load.Transformationclass GifGrayscaleTransformation : Transformation<GifDrawable> {override fun transform(context: Context,resource: Resource<GifDrawable>,outWidth: Int,outHeight: Int): Resource<GifDrawable> {val gifDrawable = resource.get()// 核心:獲取GIF的每一幀Bitmap并應用變換val firstFrame = gifDrawable.firstFrameval transformedFirstFrame = applyGrayscale(firstFrame) // 復用之前的灰度變換邏輯// 創建一個新的GifDrawable(這里簡化了,實際需要處理每一幀)// 注意:這是一個復雜操作,需要深入理解GifDrawable的結構。// 更實際的做法可能是用一個包裝器,在draw()時應用ColorFilter。val transformedGifDrawable = GifDrawable(gifDrawable.gifDecoder,gifDrawable.bitmapPool,gifDrawable.frameTransformation, // 這里本應傳入一個能處理每一幀的FrameTransformationgifDrawable.targetWidth,gifDrawable.targetHeight,gifDrawable.frameLoader)transformedGifDrawable.setFirstFrame(transformedFirstFrame)return SimpleResource(transformedGifDrawable)}private fun applyGrayscale(source: Bitmap): Bitmap {// ... 實現同上的灰度效果 ...}override fun updateDiskCacheKey(messageDigest: MessageDigest) {messageDigest.update("gif_grayscale".toByteArray())}// ... 同樣必須重寫equals和hashCode ... }
重要提示:完整地變換一個 GIF 的每一幀是一項非常復雜且性能開銷巨大的任務,通常不建議在生產環境中這樣做。更常見的需求是對 GIF 的第一幀或封面進行變換,這可以通過先加載靜態圖來實現。
4. 組合變換:MultiTransformation 的強大應用
現實中的設計需求往往是多種效果的疊加,例如“先圓角,再模糊”。Glide 提供了?MultiTransformation
?類來優雅地解決這個問題。
MultiTransformation
?會按照你傳入的順序依次應用變換。
kotlin
// 組合變換:先裁剪成圓角,再應用毛玻璃效果 val multiTransformation = MultiTransformation(CustomRoundedCornersTransformation(16f, 16f, 16f, 16f), // 第一步:16dp圓角BlurTransformation(context, 15) // 第二步:15px模糊 )Glide.with(context).load(url).transform(multiTransformation).into(imageView)// 鏈式調用.transform() 是等價的,且更簡潔 Glide.with(context).load(url).transform(CustomRoundedCornersTransformation(16f, 16f, 16f, 16f),BlurTransformation(context, 15)).into(imageView)
緩存機制:MultiTransformation
?會將其所有子變換的緩存鍵組合起來,生成一個全新的、唯一的緩存鍵。這意味著?圓角+模糊
?和?模糊+圓角
?會被認為是兩種完全不同的變換,并分別緩存。這符合預期,因為變換順序可能導致不同的最終結果。
總結與最佳實踐
性能第一:變換是 CPU 密集型操作,務必使用?
BitmapPool
?來復用?Bitmap
?對象,避免內存抖動。緩存是關鍵:永遠正確重寫?
updateDiskCacheKey
,?equals
, 和?hashCode
?方法。這是 Glide 緩存機制正確工作的基石。明確需求:問自己是否真的需要對 GIF 每一幀進行變換。通常處理第一幀或使用靜態封面是更好的選擇。
善用組合:使用?
MultiTransformation
?將簡單的原子變換組合成復雜的效果,讓代碼更清晰、更可復用。考慮替代方案:對于一些非常復雜的效果(如高級模糊),可以考慮在服務器端處理圖片,或者使用專門的 Native 庫(如 OpenCV)來處理,再將結果傳遞給 Glide。
通過掌握自定義?Transformation
,你幾乎可以應對任何 UI 設計對圖片效果的苛刻要求,將 Glide 的圖片處理能力提升到全新的高度。