Android 磨砂效果(下)

上一篇文章,我介绍了下 Android 端磨砂效果实现的接口以及方案的变更,并提到其应用场景主要有以下几种:

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

我已经给出了 1 和 2 的大致的封装方案:

  1. 通过调用 Bitmap.blur(radius) 实现对 Bitmap 的磨砂
  2. 通过 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
        )
    }
}

业务使用方主要关心两个参数:

  1. partProvider: 需要磨砂的区域信息
  2. 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)
    }
}

这样整个实现就搞定了,就可以愉快的用在业务上了,但我们依旧需要留意一下:

  1. Android S 以下由于最大 radius 只能是 25,所以磨砂效果只能到那个程度,比不得 RenderEffect 的实现
  2. Android S 以下由于是 CPU 实现,所以无法使用 Hardware Bitmap,基本上图片加载框架都会默认启用 Hardware Bitmap,所以需要判断版本关掉。

目前还没有打包上传 Maven Central,还在纠结要不要删除 Toolkit 里与 blur 无关的代码,让 so 尽可能小,也在纠结要不要去体验下 Vulkan 的实现,或许在低版本效果比 Toolkit 实现更好~

相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen5 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone5 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui