上一篇文章,我介绍了下 Android 端磨砂作用完结的接口以及计划的改变,并提到其运用场景主要有以下几种:

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

我现已给出了 1 和 2 的大致的封装计划:

  1. 通过调用 Bitmap.blur(radius) 完结对 Bitmap 的磨砂
  2. 通过 BlurBox 包裹内容,就能够完结对整个 View 的磨砂

今天咱们来讨论第 3 种场景的完结与封装

Android 磨砂效果(下)

这种对于图片的磨砂作用或许是最多的,我供给的封装为 PartBlurBox,上述作用的完结代码为:

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
        )
    }
}

事务运用方主要关怀两个参数:

  1. partProvider: 需求磨砂的区域信息
  2. radius:磨砂的半径,越大,磨砂作用越好,但是越消耗性能,在低于 Android S 的手机上,radius 最大取值为 25,当然组件内会处理好这个事情。

在其完结上,咱们依旧是在 Android S 及以上用 RenderEffect 的完结,而在低版别运用 Toolkit 的完结。

RenderEffect 一般是作用于整个 View,但其实它也能够作用于 RenderNode,这为咱们对某个区域进行磨砂供给了或许:

// 创建两个 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, 由于代码重叠度较高,所以抽取一个通用的辅助类:

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 中直接运用它完结所需的逻辑:

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)
    }
}

这样整个完结就搞定了,就能够愉快的用在事务上了,但咱们依旧需求留心一下:

  1. Android S 以下由于最大 radius 只能是 25,所以磨砂作用只能到那个程度,比不得 RenderEffect 的完结
  2. Android S 以下由于是 CPU 完结,所以无法运用 Hardware Bitmap,基本上图片加载结构都会默许启用 Hardware Bitmap,所以需求判断版别关掉。

现在还没有打包上传 Maven Central,还在纠结要不要删除 Toolkit 里与 blur 无关的代码,让 so 尽或许小,也在纠结要不要去体验下 Vulkan 的完结,或许在低版别作用比 Toolkit 完结更好~