上一篇文章,我介绍了下 Android 端磨砂效果实现的接口以及方案的变更,并提到其应用场景主要有以下几种:
- 对一个
Bitmap
进行磨砂,返回磨砂后的Bitmap
,供业务方使用 - 对整个
View
的内容进行磨砂 - 对
View
的局部进行磨砂,用于凸出标题 /TopBar
之类的元素
我已经给出了 1 和 2 的大致的封装方案:
- 通过调用
Bitmap.blur(radius)
实现对Bitmap
的磨砂 - 通过
BlurBox
包裹内容,就可以实现对整个View
的磨砂
今天我们来讨论第 3 种场景的实现与封装
这种对于图片的磨砂效果可能是最多的,我提供的封装为 PartBlurBox
,上述效果的实现代码为:
kotlin
Box(modifier = Modifier.width(300.dp).height(300.dp)){
val infoHeight = 56.dp
val infoHeightPx = with(LocalDensity.current){
infoHeight.toPx()
}
PartBlurBox(
modifier = Modifier.fillMaxSize(),
partProvider = { w, h ->
Rect(0, (h - infoHeightPx).toInt(), w, h)
},
radius = 100
) { reporter ->
Image(
painter = painterResource(id = R.mipmap.avatar),
contentDescription = "",
contentScale = ContentScale.Crop
)
LaunchedEffect(Unit){
reporter.onContentUpdate()
}
}
Box(modifier = Modifier
.fillMaxWidth()
.height(infoHeight)
.align(Alignment.BottomCenter)
.background(Color.Black.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
){
Text(
text = "关注公众号-古哥E下,可私聊,群聊",
lineHeight = 56.sp,
color = Color.White,
fontSize = 16.sp
)
}
}
业务使用方主要关心两个参数:
partProvider
: 需要磨砂的区域信息radius
:磨砂的半径,越大,磨砂效果越好,但是越耗费性能,在低于 Android S 的手机上,radius 最大取值为 25,当然组件内会处理好这个事情。
在其实现上,我们依旧是在 Android S 及以上用 RenderEffect
的实现,而在低版本使用 Toolkit
的实现。
RenderEffect
一般是作用于整个 View
,但其实它也可以作用于 RenderNode
,这为我们对某个区域进行磨砂提供了可能:
kotlin
// 创建两个 RenderNode
val contentNode = RenderNode("content")
val blurNode = RenderNode("blur")
fun draw(canvas: Canvas){
// 将原本的内容 draw 到contentNode 上
contentNode.setPosition(0, 0, width, height)
val rnCanvas = contentNode.beginRecording()
super.draw(rnCanvas)
contentNode.endRecording()
// 将 contentNode draw 回 View 的 canvas
canvas.drawRenderNode(contentNode)
if(this.radius > 0){
// 对 blurNode 应用 RenderEffect
blurNode.setRenderEffect(RenderEffect.createBlurEffect(this.radius.toFloat(), this.radius.toFloat(),
Shader.TileMode.CLAMP))
// 获取磨砂区域
val part = partProvider(width, height)
blurNode.setPosition(0, 0, part.width(), part.height())
blurNode.translationY = part.top.toFloat()
blurNode.translationX = part.left.toFloat()
// 将内容再 draw 到 blurNode 上
val blurCanvas = blurNode.beginRecording()
blurCanvas.translate(-part.left.toFloat(), -part.top.toFloat())
blurCanvas.drawRenderNode(contentNode)
blurNode.endRecording()
// 将 blurNode draw 回 View 的 canvas
canvas.drawRenderNode(blurNode)
}
}
这代码基本上就是抄 Medium 上的文章 RenderNode for Bigger, Better Blurs
对于低版本,其处理逻辑和整个 View
的磨砂差异不大,就是生成一个区域的 Bitmap
, 由于代码重叠度较高,所以抽取一个通用的辅助类:
kotlin
class ViewToBlurBitmapCreator(
private val contentView: View,
private val onBlurBitmapCreated: (bitmap: Bitmap?, x: Float, y: Float) -> Unit
) : LogTag{
private var generateJob: Job? = null
private var updateVersion = 0
fun run(radius: Int, partProvider: ((w: Int, h: Int) -> Rect)?){
generateJob?.cancel()
updateVersion++
if(radius == 0){
onBlurBitmapCreated(null, 0f, 0f)
}
val safeRadius = radius.coerceAtMost(25)
val currentVersion = updateVersion
OneShotPreDrawListener.add(contentView) {
contentView.post {
contentView.findViewTreeLifecycleOwner()?.apply {
generateJob = lifecycleScope.launch {
if(currentVersion != updateVersion){
return@launch
}
if(contentView.width <= 0 || contentView.height <= 0){
EmoLog.w(TAG, "blur ignored because of size issue(w=${contentView.width}, h=${contentView.height})")
// retry
run(safeRadius, partProvider)
return@launch
}
try {
var x = 0f
var y = 0f
val bitmap = if(partProvider == null){
Bitmap.createBitmap(contentView.width, contentView.height, Bitmap.Config.ARGB_8888).also {
val canvas = Canvas(it)
contentView.draw(canvas)
}
} else {
val part = partProvider(contentView.width, contentView.height)
val w = part.width()
val h = part.height()
if (w <= 0 || h <= 0 ||
part.left < 0 || part.top < 0 || part.right > contentView.width || part.bottom > contentView.height
) {
throw IllegalStateException("part is illegal")
}
x = part.left.toFloat()
y = part.top.toFloat()
Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888).also {
val canvas = Canvas(it)
canvas.translate(-x, -y)
contentView.draw(canvas)
}
}
val blurImage = withContext(Dispatchers.IO) {
Toolkit.blur(bitmap, safeRadius)
}
if(currentVersion == updateVersion){
onBlurBitmapCreated(blurImage, x, y)
}
} catch (e: Throwable) {
if(e !is CancellationException){
EmoLog.e(TAG, "blur image failed", e)
}
}
}
}
}
}
contentView.invalidate()
}
}
然后在 View
中直接使用它完成所需的逻辑:
kotlin
internal class PartBlurBitmapEffectView(
context: Context,
radius: Int = DEFAULT_BLUR_RADIUS,
partProvider: (width: Int, height: Int)-> Rect
) : PartBlurView(context, radius, partProvider) {
private val blurImageView = FakeImageView(context)
private val blurBitmapCreator = ViewToBlurBitmapCreator(contentView){ bitmap, x, y ->
if(bitmap != null){
blurImageView.setBitmap(bitmap, x, y)
} else {
blurImageView.clear()
}
}
init {
addView(blurImageView, LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
))
onContentUpdate()
}
override fun onContentUpdate() {
blurBitmapCreator.run(radius, partProvider)
}
}
这样整个实现就搞定了,就可以愉快的用在业务上了,但我们依旧需要留意一下:
- Android S 以下由于最大
radius
只能是 25,所以磨砂效果只能到那个程度,比不得RenderEffect
的实现 - Android S 以下由于是 CPU 实现,所以无法使用
Hardware Bitmap
,基本上图片加载框架都会默认启用Hardware Bitmap
,所以需要判断版本关掉。
目前还没有打包上传 Maven Central
,还在纠结要不要删除 Toolkit
里与 blur
无关的代码,让 so 尽可能小,也在纠结要不要去体验下 Vulkan
的实现,或许在低版本效果比 Toolkit
实现更好~