Jetpack Compose 图片局部模糊:Canvas与RenderEffect优化
2025-03-06 16:31:14
Jetpack Compose 实现图片局部模糊效果
最近遇到一个需求:显示图片,但是只模糊图片的一部分,比如底部区域。试过用多个Image
组件叠加、设置不同的模糊程度,但性能不行,过渡也不平滑。这里提供一种性能好、效果更流畅的方法。 主要思路是通过Jetpack Compose的Canvas
API在绘制图片时只对特定部分应用模糊效果. 也希望能动态模糊从 URL 加载的图片。
为什么会出现这个问题?
直接在 Compose 中叠加多个带模糊的Image
,会导致多次绘制和模糊操作,特别是对大图,计算量会非常大,帧率直接往下掉,产生卡顿. 更理想的方式是在一次绘制过程中,就区分出清晰和模糊区域,并分别处理。
解决方案:
1. 使用 Canvas
和 drawImage
-
原理和作用:
Canvas
提供了一个底层的绘图画布,drawImage
方法可以直接绘制图片,并配合DrawScope
进行控制。可以在绘制图片到Canvas
之前,先通过clipRect
剪裁出一部分进行处理. 我们可以在绘制前先在单独的bitmap上做模糊。这样我们可以对原始图片的特定区域先模糊再绘制到Canvas,性能比较好。 -
代码示例:
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.applyCanvas
import coil.ImageLoader
import coil.request.ImageRequest
import coil.request.SuccessResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import androidx.compose.ui.graphics.BlendMode
@Composable
fun PartialBlurImage(imageUrl: String, blurBottomPercentage: Float = 0.3f, blurRadius: Int = 25) {
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
val context = LocalContext.current
LaunchedEffect(imageUrl) {
imageBitmap = withContext(Dispatchers.IO) {
loadImage(context, imageUrl)
}
}
if (imageBitmap != null) {
Canvas(modifier = Modifier.fillMaxSize()) {
val width = size.width.toInt()
val height = size.height.toInt()
val blurHeight = (height * blurBottomPercentage).toInt()
// 截取要模糊的部分到单独的Bitmap
val bottomRect = Rect(0f, height - blurHeight.toFloat(), width.toFloat(), height.toFloat())
val blurredBitmap = imageBitmap!!.asAndroidBitmap().crop(bottomRect).blur(context, blurRadius)
// 先绘制清晰的部分
drawIntoCanvas { canvas ->
val srcRect = android.graphics.Rect(0, 0, imageBitmap!!.width, imageBitmap!!.height - (imageBitmap!!.height * blurBottomPercentage).toInt())
val dstRect = android.graphics.Rect(0, 0, width, height - blurHeight)
canvas.nativeCanvas.drawBitmap(imageBitmap!!.asAndroidBitmap(),srcRect ,dstRect , null)
//再绘制模糊的部分.
canvas.nativeCanvas.drawBitmap(blurredBitmap,0f,height - blurHeight.toFloat(),null)
}
}
}
}
//从图像中剪裁一部分
fun Bitmap.crop(rect: Rect): Bitmap =
Bitmap.createBitmap(this, rect.left.toInt(), rect.top.toInt(), rect.width.toInt(),rect.height.toInt())
//图片加载方法.
suspend fun loadImage(context: android.content.Context, imageUrl: String): ImageBitmap? {
val loader = ImageLoader(context)
val request = ImageRequest.Builder(context)
.data(imageUrl)
.allowHardware(false) // 因为做了处理,这里需要禁用硬件加速.
.build()
return try {
val result = (loader.execute(request) as SuccessResult).drawable
val bitmap = BitmapFactory.decodeStream(result.toBitmap().byteInputStream())
bitmap.asImageBitmap()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun android.graphics.drawable.Drawable.toBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(
intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888
)
val canvas = android.graphics.Canvas(bitmap)
setBounds(0, 0, canvas.width, canvas.height)
draw(canvas)
return bitmap
}
// RenderScript 和 RenderEffect 进行模糊
fun Bitmap.blur(context: android.content.Context, radius: Int): Bitmap {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val renderEffect = RenderEffect.createBlurEffect(radius.toFloat(), radius.toFloat(), Shader.TileMode.CLAMP)
val bitmap = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888)
.applyCanvas { setRenderEffect(renderEffect)
drawBitmap(this@blur, 0f, 0f, null)
}
return bitmap
} else{
// 低版本使用 RenderScript.
val renderScript = android.renderscript.RenderScript.create(context)
val bitmapAlloc = android.renderscript.Allocation.createFromBitmap(renderScript, this)
val blurAlloc = android.renderscript.Allocation.createTyped(renderScript, bitmapAlloc.type)
val blurScript = android.renderscript.ScriptIntrinsicBlur.create(renderScript, android.renderscript.Element.U8_4(renderScript))
blurScript.setRadius(radius.toFloat())
blurScript.setInput(bitmapAlloc)
blurScript.forEach(blurAlloc)
val blurredBitmap = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888)
blurAlloc.copyTo(blurredBitmap)
renderScript.destroy()
return blurredBitmap
}
}
- 安全建议:
loadImage
方法中,捕获了异常并打印,生产环境中应更妥善处理加载失败的情况,如显示占位图或错误提示.- 记得禁用coil硬件加速, 避免出现
IllegalArgumentException
.
- 进阶使用技巧:
- 可以添加一个参数来控制模糊的开始位置,让效果更灵活。
- 如果需要模糊的区域是不规则形状,可以使用
clipPath
方法.
2.优化模糊性能
上面的代码中, 我们使用了 RenderScript 以及RenderEffect,RenderEffect是Android 12 (API 级别 31)引入的. 可以简单地向视图或绘制层次结构应用常见的图形效果. 这种方式简单方便且高性能, 是现在首选的模糊实现方式。RenderScript是一种比较老旧的技术了. 可以作为低版本的降级方案使用。
可以根据手机版本判断, 决定选择哪一个API去处理模糊.
总结
上面展示了如何在 Jetpack Compose 中实现图片的局部模糊。通过 Canvas
精细控制绘制过程,并结合使用 RenderScript 或RenderEffect,可以在一次绘制中完成对图片的局部模糊,提高效率和流畅度。 根据不同设备,也可以灵活使用 RenderScript 或者RenderEffect来达到兼容。