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 实现更好~

相关推荐
踏雪羽翼6 小时前
android TextView实现文字字符不同方向显示
android·自定义view·textview方向·文字方向·textview文字显示方向·文字旋转·textview文字旋转
lxysbly6 小时前
安卓玩MRP冒泡游戏:模拟器下载与使用方法
android·游戏
夏沫琅琊9 小时前
Android 各类日志全面解析(含特点、分析方法、实战案例)
android
程序员JerrySUN9 小时前
OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录
android·java·开发语言·redis·yolo·架构
TeleostNaCl10 小时前
Android | 启用 TextView 跑马灯效果的方法
android·经验分享·android runtime
TheNextByte111 小时前
Android USB文件传输无法使用?5种解决方法
android
quanyechacsdn12 小时前
Android Studio创建库文件用jitpack构建后使用implementation方式引用
android·ide·kotlin·android studio·implementation·android 库文件·使用jitpack
程序员陆业聪13 小时前
聊聊2026年Android开发会是什么样
android
编程大师哥13 小时前
Android分层
android
极客小云15 小时前
【深入理解 Android 中的 build.gradle 文件】
android·安卓·安全架构·安全性测试