对图片或者界面运用高斯模糊作用,是设计师经常想要加上的作用,也是 Android 开发们最厌烦的作业,由于作用不理想并且做起来麻烦。

官方原汁原味的支持,是在 Android 12 才供给了的。在这之前,开发者一直是借助 RenderScript 来完结高斯模糊作用,目前比较盛行的库根本都是基于 RenderScript 库来完结,其实官方已经废弃了这一项技能,但由于 Android 江河日下,少有人乐意与时俱进的更新库,所以咱们根本上仍是运用很旧的库来完结这个功用。

从 Android 12 开始,官方供给了 RenderEffect 这一 API,能够很便利的供咱们对一个 View 做磨砂作用:

val blurEffect = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
view.setRenderEffect(blurEffect)

调用接口适当简单,并且作用也是十分可观的。

如果是 Compose, 则能够用 graphicsLayer 来完结:

Box(modifier = Modifier.graphicsLayer {
    renderEffect = BlurEffect(...)
}){
    //...
}

不过我测试了下,不知道是不是还有其它设置项,其右边缘和下边缘的磨砂好像会运用 Box 外的图层,导致有点突兀。

而关于 Android 12 之下,也有几种选择,一种是官方供给了 RenderScript 的替代品,一个由 native 完结的库 renderscript-intrinsics-replacement-toolkit,然而官方并没有供给 gradle 库,需要开发自己拉代码导入到项目中。

另一种选择就是直接用 Vulkan 去写,不过也是写 native 代码了,与 toolkit 不同的是,toolkit 是运用 CPU,而 Vulkan 则是能够运用 GPU。就速度方面,大部分情况或许 toolkit 最快,官方说是 RenderScript 的两倍多。

关于这几种技能,官方都给出了完结的 sample,关于技能细节比较关注的能够详细阅读源码

由于 Android 的碎片化, 咱们还得考虑 Android 12 以下的设备, 所以针对 Android 12 及以上,咱们能够用 RenderEffect 去获取最佳的性能与作用,关于 Android 12 以下,则只能运用 toolkit 来降级。关于事务方而言,当然不期望每次都写 if else 去做不同处理,所以咱们得封装。

考虑运用场景,磨砂作用大体能够考虑:

  1. 对一个 Bitmap 进行磨砂,返回磨砂后的 Bitmap,供事务方运用
  2. 对整个 View 的内容进行磨砂
  3. View 的局部进行磨砂,用于凸出标题 / TopBar 之类的元素

关于 Bitmap 的磨砂:

suspend fun Bitmap.blur(radius: Int = DEFAULT_BLUR_RADIUS) = withContext(Dispatchers.IO){
    require(radius in 1..25) {
        "The radius should be between 1 and 25. $radius provided."
    }
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
        val imageReader = ImageReader.newInstance(
            width, height,
            PixelFormat.RGBA_8888, 1,
            HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
        )
        val renderNode = RenderNode("blur")
        val hardwareRenderer = HardwareRenderer()
        hardwareRenderer.setSurface(imageReader.surface)
        hardwareRenderer.setContentRoot(renderNode)
        renderNode.setPosition(0, 0, width, height)
        renderNode.setRenderEffect(RenderEffect.createBlurEffect(radius.toFloat(), radius.toFloat(), Shader.TileMode.CLAMP))
        val canvas = renderNode.beginRecording(width, height)
        canvas.drawBitmap(this@blur, 0f, 0f, null)
        renderNode.endRecording()
        hardwareRenderer.createRenderRequest()
            .setWaitForPresent(true)
            .syncAndDraw()
        val image = imageReader.acquireNextImage() ?: throw RuntimeException("No Image")
        val hardwareBuffer = image.hardwareBuffer ?: throw RuntimeException("No HardwareBuffer")
        val bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, null)
            ?: throw RuntimeException("Create Bitmap Failed")
        hardwareBuffer.close()
        image.close()
        bitmap
    } else {
        Toolkit.blur(this@blur, radius)
    }
}

高版别用 RenderEffect 完结,低版别用 toolkit。 高版别运用了 RenderNode,咱们的到的是 hardwar bitmap,能够节省内存与运用 GPU 加速渲染。

如果关于整个 View 进行磨砂,Android 12 上仍是直接用 RenderEffect 即可, Android 12 以下则首先将 View 转换为 Bitmap,然后对 Bitmap 进行磨砂,然后将磨砂后的 Bitmap 贴在 View 上面。

下面看看代码上如何封装,当然我的封装都是为 Compose 服务,没有纯 View 的完结:

// 运用方只需要用 `BlurBox` 包裹内容,设置 `radius`
@Composable
fun BlurBox(modifier: Modifier, radius: Int = DEFAULT_BLUR_RADIUS, content: @Composable (updateReporter: ()-> Unit) -> Unit) {
    AndroidView(
        factory = { context ->
            BlurView(context, radius).apply {
                setContent(content)
            }
        },
        modifier = modifier,
        update = {
            it.updateRadius(radius)
        }
    )
}
class BlurView(context: Context, radius: Int = DEFAULT_BLUR_RADIUS) : FrameLayout(context) {
    private val blurApi: BlurApi
    init {
        // 依照版别运用不同完结
        blurApi = if (Build.VERSION.SDK_INT >= 31) {
            BlurRenderEffectImpl(this, radius)
        } else {
            BlurBitmapEffectImpl(this, radius)
        }
    }
    ...
}
@TargetApi(31)
internal class BlurRenderEffectImpl(
    private val blurView: BlurView,
    private var radius: Int = DEFAULT_BLUR_RADIUS
) : BlurApi {
    private var view = ComposeView(blurView.context)
    init {
        blurView.addView(
            view, FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        )
        // Android 12 以上直接用 `RenderEffect`,由于前面提到过 `Compose` 自己完结的小瑕疵,所以仍是套壳完结。
        view.setRenderEffect(RenderEffect.createBlurEffect(radius.toFloat(), radius.toFloat(), Shader.TileMode.CLAMP))
    }
    override fun setContent(content: @Composable (reportContentUpdate: () -> Unit) -> Unit) {
        view.setContent {
            content {
            }
        }
    }
    override fun updateRadius(radius: Int) {
        if (this.radius != radius) {
            this.radius = radius
            if (radius == 0) {
                view.setRenderEffect(null)
            } else {
                view.setRenderEffect(
                    RenderEffect.createBlurEffect(radius.toFloat(), radius.toFloat(), Shader.TileMode.CLAMP)
                )
            }
        }
    }
}
internal class BlurBitmapEffectImpl(
    private val blurView: BlurView,
    private var radius: Int = DEFAULT_BLUR_RADIUS
) : BlurApi {
    private var view = ComposeView(blurView.context)
    private var blurImageView = FakeImageView(blurView.context)
    private var generateJob: Job? = null
    private var updateVersion = 0
    init {
        blurView.addView(
            view, FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        )
        blurView.addView(
            blurImageView, FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        )
        updateBlurImage()
    }
    override fun setContent(content: @Composable (reportContentUpdate: () -> Unit) -> Unit) {
        view.setContent {
            content {
                updateBlurImage()
            }
        }
    }
    override fun updateRadius(radius: Int) {
        if (this.radius != radius) {
            this.radius = radius
            updateBlurImage()
        }
    }
    private fun updateBlurImage() {
        generateJob?.cancel()
        updateVersion++
        if (radius == 0) {
            blurImageView.clear()
            return
        }
        val currentVersion = updateVersion
        OneShotPreDrawListener.add(view) {
            view.post {
                if (view.width <= 0 || view.height <= 0) {
                    EmoLog.w(TAG, "blur ignored because of size issue(w=${view.width}, h=${view.height})")
                    updateBlurImage()
                    return@post
                }
                view.findViewTreeLifecycleOwner()?.apply {
                    generateJob = lifecycleScope.launch {
                        if (currentVersion != updateVersion) {
                            return@launch
                        }
                        try {
                            val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
                            val canvas = Canvas(bitmap)
                            view.draw(canvas)
                            val blurImage = withContext(Dispatchers.IO) {
                                Toolkit.blur(bitmap, radius)
                            }
                            if (currentVersion == updateVersion) {
                                blurImageView.setBitmap(blurImage, 0f, 0f)
                            }
                        } catch (e: Throwable) {
                            if (e !is CancellationException) {
                                EmoLog.e(TAG, "blur image failed", e)
                            }
                        }
                    }
                }
            }
        }
        view.invalidate()
    }
}

其实麻烦的是低版别的完结,咱们要等内容准备好之后通知上层去生成 Bitmap 然后进行磨砂,所以这儿需要由开发自动调用 reportContentUpdate 告知有内容更新,有一个更好的计划是用 drawWithCache,官方文档说的是有状况改变了才会重新调用生成cache,理论是可行的,不过我测试没跑通,原因还没去找,或许又是我哪里傻叉写了点古怪的代码。

另一个问题就是低版别下 BlurBox 不能有 hardware bitmap,运用上也是一个需要注意的点。

这篇文章就暂时先写到这儿了,针对 View 局部磨砂的完结,咱们留待下一篇文章再来展开。库的开发是在 emo 上进行的,由于还没完结,所以也没有发布,写完下一篇文章,估量库的封装也就差不多了。