高斯模糊 (Gaussian Blur) 通常被用来实现 毛玻璃 效果。截取要模糊区域的背景,将 高斯模糊 应用此截取部分覆盖在背景上显示即可实现 毛玻璃 效果。

1. 从 RenderScript 迁移

Android 12 (API 31) 之前使用 RenderScript API 来实现 高斯模糊 。页面布局 activity_main.xml 内容如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_img"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:src="@drawable/sample_1"
        android:adjustViewBounds="true"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_img_1"
        android:layout_width="170dp"
        android:layout_height="wrap_content"
        android:src="@drawable/sample_2"
        android:adjustViewBounds="true"
        android:foreground="@drawable/shape_round_write_t10"
        app:layout_constraintStart_toStartOf="@+id/iv_img"
        app:layout_constraintEnd_toEndOf="@+id/iv_img"
        app:layout_constraintTop_toTopOf="@+id/iv_img"
        app:layout_constraintBottom_toBottomOf="@+id/iv_img" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt 示例代码如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val blurBitmap = ContextCompat.getDrawable(this, R.drawable.sample_2)?.let { drawable ->
            val bitmap = (drawable as BitmapDrawable).bitmap
            
            val sampling = 3f
            val scaledWidth = (bitmap.width / sampling).toInt()
            val scaledHeight = (bitmap.height / sampling).toInt()
            val output = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, false)

            val radius = 25f
            val renderScript = RenderScript.create(this)
            val tmpIn: Allocation = Allocation.createFromBitmap(renderScript, output)
            val tmpOut: Allocation = Allocation.createTyped(renderScript, tmpIn.type)
            val scriptIntrinsicBlur: ScriptIntrinsicBlur =
                ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
            scriptIntrinsicBlur.setRadius(radius)
            scriptIntrinsicBlur.setInput(tmpIn)
            scriptIntrinsicBlur.forEach(tmpOut)
            tmpOut.copyTo(output)
            output
        }

        val ivImg = findViewById<ImageView>(R.id.iv_img_1)
        ivImg.setImageBitmap(blurBitmap)
    }

}

图 1.1 展示了使用 RenderScript API 实现 高斯模糊 的效果。

图1.1

Android 12 (API 31) 开始,RenderScript API 已被废弃。它们将继续正常运行一段时间,但设备和组件制造商已停止提供硬件加速支持,并将在未来的版本中完全取消对 RenderScript 的支持。更多内容可查看 官方文档 - 从 RenderScript 迁移

如果以 Android 12 (API 31) 及更高版本为目标平台,可使用 RenderEffect,示例代码如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        	// >= Android 12
            ContextCompat.getDrawable(this, R.drawable.sample_2)?.let { drawable ->
                val ivImg = findViewById<ImageView>(R.id.iv_img_1)
                ivImg.setImageDrawable(drawable)
                ivImg.setRenderEffect(
                    RenderEffect.createBlurEffect(50f, 50f, Shader.TileMode.MIRROR)
                )
            }
        } else {
        	// < Android 12
        }
    }

}

图 1.2 展示了使用 RenderEffect 实现 高斯模糊 的效果。

图1.2

如果以 Android 12 (API 31) 之前的版本为目标平台,Google 提供了 RenderScript 内建函数替换工具包,此 工具包 相对于 RenderScript API 更易于使用,且性能最高可提高 2 倍。使用步骤如下:

  1. GitHub 下载 项目
  2. 构建 renderscript-toolkit moduleaar 使用。

注: 构建 renderscript-toolkit 使用的 JDK 版本不能高于 JDK 11,否则报错:Unsupported class file major version 61

也可以从 jitpack.io 线上构建后通过导入依赖使用,步骤如下:

  1. settings.gradle 文件中添加仓库地址;

    dependencyResolutionManagement {
        repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
        repositories {
            google()
            mavenCentral()
          	// 新增
            maven { url 'https://jitpack.io' }
        }
    }
    
  2. build.gradle 文件中添加依赖。

    dependencies {
    	implementation 'com.github.android:renderscript-intrinsics-replacement-toolkit:344be3f'
    }
    

使用 RenderScript 内建函数替换工具包 实现 高斯模糊的示例代码如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // >= Android 12
        } else {
            // < Android 12
            ContextCompat.getDrawable(this, R.drawable.sample_2)?.let { drawable -> 
                val bitmap = (drawable as BitmapDrawable).bitmap
            	val sampling = 3f
            	val radius = 25
            	val scaledWidth = (bitmap.width / sampling).toInt()
            	val scaledHeight = (bitmap.height / sampling).toInt()
            	val output = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, false)
            	val ivImg = findViewById<ImageView>(R.id.iv_img_1)
            	ivImg.setImageBitmap(Toolkit.blur(output, radius))
            }
        }
    }

}

图 1.3 展示了使用 RenderScript 内建函数替换工具包 实现 高斯模糊 的效果。

图1.3

2. 图片加载库实现高斯模糊

使用主流的图片加载库如 GlideCoil 可以便利的实现网络图片和本地图片的 高斯模糊 效果。

2.1 Glide 实现高斯模糊

Glide 不提供内置的 高斯模糊 功能,可自定义 Transformations 或者使用三方 Transformations 库,如 wasabeef/glide-transformations

自定义 Transformations 实现 高斯模糊,新建类 GlideBlurTransformations 如下所示:

import android.graphics.Bitmap
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.google.android.renderscript.Toolkit
import java.security.MessageDigest

class GlideBlurTransformations @JvmOverloads constructor(
    private val radius: Float = DEFAULT_RADIUS,
    private val sampling: Float = DEFAULT_SAMPLING
) : BitmapTransformation() {

    init {
        require(radius in 0.0..25.0) { "radius must be in [0, 25]." }
        require(sampling > 0) { "sampling must be > 0." }
    }

    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        messageDigest.update("${GlideBlurTransformations::class.java.name}-$radius-$sampling".encodeToByteArray())
    }

    override fun transform(
        pool: BitmapPool,
        toTransform: Bitmap,
        outWidth: Int,
        outHeight: Int
    ): Bitmap {
        val scaledWidth = (toTransform.width / sampling).toInt()
        val scaledHeight = (toTransform.height / sampling).toInt()
        val output = Bitmap.createScaledBitmap(toTransform, scaledWidth, scaledHeight, false)
        return Toolkit.blur(output, radius.toInt())
    }

    private companion object {
        private const val DEFAULT_RADIUS = 10f
        private const val DEFAULT_SAMPLING = 1f
    }
}

使用方式如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val ivImg = findViewById<ImageView>(R.id.iv_img_1)
        Glide.with(this)
            .load(R.drawable.sample_2)
            .transform(GlideBlurTransformations(25f, 18f))
            .into(ivImg)
    }

}

图 2.1 展示了使用 Glide 实现 高斯模糊 的效果。

图2.1

2.2 Coil 实现高斯模糊

Coil2.0.0-alpha01 的更新中移除了 BlurTransformation 类,我们需要复制原有代码自行实现,新建类 CoilBlurTransformation 并实现如下:

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Paint
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap
import coil.size.Size
import coil.transform.Transformation
import com.google.android.renderscript.Toolkit

/**
 * Coil 高斯模糊
 * @param context
 * @param radius - 高斯模糊半径
 * @param sampling - 用于缩放图像的采样系数。 > 1 将缩小图像。 0 到 1 之间的值将放大图像
 */
class CoilBlurTransformation @JvmOverloads constructor(
    private val context: Context,
    private val radius: Float = DEFAULT_RADIUS,
    private val sampling: Float = DEFAULT_SAMPLING
) : Transformation {

    init {
        require(radius in 0.0..25.0) { "radius must be in [0, 25]." }
        require(sampling > 0) { "sampling must be > 0." }
    }

    override val cacheKey = "${CoilBlurTransformation::class.java.name}-$radius-$sampling"

    override suspend fun transform(input: Bitmap, size: Size): Bitmap {
        val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)

        val scaledWidth = (input.width / sampling).toInt()
        val scaledHeight = (input.height / sampling).toInt()
        val output = createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888)
        output.applyCanvas {
            scale(1 / sampling, 1 / sampling)
            drawBitmap(input, 0f, 0f, paint)
        }
        
        // 使用 renderscript-toolkit 实现高斯模糊
        return Toolkit.blur(output, radius.toInt())
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        return other is CoilBlurTransformation &&
                context == other.context &&
                radius == other.radius &&
                sampling == other.sampling
    }

    override fun hashCode(): Int {
        var result = context.hashCode()
        result = 31 * result + radius.hashCode()
        result = 31 * result + sampling.hashCode()
        return result
    }

    override fun toString(): String {
        return "BlurTransformation(context=$context, radius=$radius, sampling=$sampling)"
    }

    private companion object {
        private const val DEFAULT_RADIUS = 10f
        private const val DEFAULT_SAMPLING = 1f
    }

}

使用方式如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val ivImg = findViewById<ImageView>(R.id.iv_img_1)
        ivImg.load(R.drawable.sample_2) {
            transformations(CoilBlurTransformation(this@MainActivity, 25f, 2f))
        }
    }

}

图 2.2 展示了使用 Coil 实现 高斯模糊 的效果。

图2.2

3. 动态高斯模糊

要实现动态的 高斯模糊 效果,则需要在页面发送时及时截取对应位置的页面背景应用 高斯模糊 效果。通过 ViewTreeObserver.OnPreDrawListener 即可监听视图树将要发生的变化。实现动态 高斯模糊 效果的示例代码如下。

MainActivity.kt

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
import android.os.Bundle
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import android.view.ViewTreeObserver
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap


class MainActivity : AppCompatActivity() {

    private var ivMask: ImageView? = null
    private var mBlurBitmap: Bitmap? = null
    private var mBitmap: Bitmap? = null
    private var mCanvas: Canvas? = null
    private val paint: Paint by lazy { Paint() }
    private var init: Boolean = false

    /**
     * 即将绘制视图树时执行的回调
     */
    private val onPreDrawListener = object : ViewTreeObserver.OnPreDrawListener {
        override fun onPreDraw(): Boolean {
            ivMask?.takeIf { mBitmap != null && mCanvas != null }?.let { imageView ->
                // 获取要应用高斯模糊的 view 在屏幕上的显示位置
                val locations = IntArray(2)
                imageView.getLocationOnScreen(locations)
                val x = locations[0]
                val y = locations[1]

                // 把 canvas 的信息保存
                val rc: Int = mCanvas!!.save()
                try {
                    // 设置 X 和 Y 方向上的缩放因子,使得 view 映射在 bitmap 上的画面示效果一致
                    // 即 bitmap 与 view 画面大小的倍数
                    mCanvas!!.scale(
                        1f * mBitmap!!.width / imageView.width,
                        1f * mBitmap!!.height / imageView.height
                    )
                    // 平移, 即重新映射画布上的原点位置 (0,0)
                    // 使得将要应用高斯模糊的 view 上展示的画面即为此 view 覆盖下的背景画面
                    mCanvas!!.translate(-x.toFloat(), -y.toFloat())
                    window.peekDecorView().rootView.draw(mCanvas!!)

                    // 高斯模糊
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                        // >= Android 12 使用 RenderEffect,但需要在图像变更时重新调用 setImageBitmap 设置模糊背景
                        // 调用 setImageBitmap 会再次触发 OnPreDrawListener 监听,
                        // 但当对比到两次截取的 bitmap 图像内容一致则不会再继续调用 setImageBitmap
                        if (mBlurBitmap == null || !compare2Image(mBlurBitmap, mBitmap)) {
                            mBlurBitmap = createBitmap(mBitmap!!.width, mBitmap!!.height, Bitmap.Config.ARGB_8888)
                            mBlurBitmap?.applyCanvas {
                                drawBitmap(mBitmap!!, 0f, 0f, paint)
                            }
                            ivMask?.setImageBitmap(mBitmap)
                        }
                        if (!init) {
                            // 只需在初始化时调用 setRenderEffect 一次
                            init = true
                            ivMask?.setRenderEffect(
                                RenderEffect.createBlurEffect(50f, 50f, Shader.TileMode.MIRROR)
                            )
                        }
                    } else {
                        // < Android 12 使用 RenderScript Api,且只需调用 setImageBitmap 一次
                        // 不使用 renderscript-toolkit ,因为会对实际截取的背景产生影响,出现高斯模糊效果叠加
                        val renderScript = RenderScript.create(this@MainActivity)
                        val tmpIn: Allocation = Allocation.createFromBitmap(renderScript, mBitmap)
                        val tmpOut: Allocation = Allocation.createTyped(renderScript, tmpIn.type)
                        val scriptIntrinsicBlur: ScriptIntrinsicBlur = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
                        scriptIntrinsicBlur.setRadius(25f)
                        scriptIntrinsicBlur.setInput(tmpIn)
                        scriptIntrinsicBlur.forEach(tmpOut)
                        tmpOut.copyTo(mBitmap)
                        if (!init || mBlurBitmap != mBitmap) {
                            // 只需在初始化或 mBitmap 对象重新实例化后调用 setImageBitmap 一次
                            init = true
                            mBlurBitmap = mBitmap
                            ivMask?.setImageBitmap(mBitmap)
                        }
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                } finally {
                    // 恢复 canvas 到特定的保存点
                    mCanvas!!.restoreToCount(rc)
                }
            }
            return true
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ivMask = findViewById(R.id.iv_img_mask)
        ivMask?.post {
            mBitmap = Bitmap.createBitmap(ivMask!!.width / 2, ivMask!!.height / 2, Bitmap.Config.ARGB_8888)
            mCanvas = Canvas(mBitmap!!)
        }
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        window.decorView.viewTreeObserver.addOnPreDrawListener(onPreDrawListener)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        window.decorView.viewTreeObserver.removeOnPreDrawListener(onPreDrawListener)
    }

    /**
     * 简单对比两个 Bitmap 是否内容相同
     * @return false - 不同, true - 相同
     */
    private fun compare2Image(bmp1: Bitmap?, bmp2: Bitmap?): Boolean {
        if(bmp1 == null || bmp2 == null) return false
        else if (bmp1.width != bmp2.width || bmp1.height != bmp2.height) return false

        // 简单取样,对比像素是否相同
        val iteration = Math.min(bmp1.width, bmp1.height)
        for (i in 0 until iteration) {
            if (bmp1.getPixel(i, i) != bmp2.getPixel(i, i)) return false
        }
        return true
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/scroll_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_img_1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:src="@drawable/sample_1"
                android:adjustViewBounds="true" />

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_img_3"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:src="@drawable/sample_3"
                android:adjustViewBounds="true" />

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_img_4"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:src="@drawable/sample_4"
                android:adjustViewBounds="true" />

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_img_5"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:src="@drawable/sample_5"
                android:adjustViewBounds="true" />

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_img_6"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:src="@drawable/sample_6"
                android:adjustViewBounds="true" />

        </androidx.appcompat.widget.LinearLayoutCompat>

    </androidx.core.widget.NestedScrollView>

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/iv_img_mask"
        android:layout_width="250dp"
        android:layout_height="150dp"
        android:foreground="@drawable/shape_round_write_t10"
        android:layout_gravity="center"/>

</FrameLayout>

图 3.1 展示了动态 高斯模糊 效果。

图3.1