高斯模糊 (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
实现 高斯模糊
的效果。
从 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
实现 高斯模糊
的效果。
如果以 Android 12 (API 31)
之前的版本为目标平台,Google
提供了 RenderScript 内建函数替换工具包
,此 工具包
相对于 RenderScript API
更易于使用,且性能最高可提高 2 倍。使用步骤如下:
- 从
GitHub
下载 项目; - 构建
renderscript-toolkit module
为aar
使用。
注: 构建
renderscript-toolkit
使用的JDK
版本不能高于JDK 11
,否则报错:Unsupported class file major version 61
也可以从 jitpack.io 线上构建后通过导入依赖使用,步骤如下:
-
在
settings.gradle
文件中添加仓库地址;dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // 新增 maven { url 'https://jitpack.io' } } }
-
在
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 内建函数替换工具包
实现 高斯模糊
的效果。
2. 图片加载库实现高斯模糊
使用主流的图片加载库如 Glide
、Coil
可以便利的实现网络图片和本地图片的 高斯模糊
效果。
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.2 Coil 实现高斯模糊
Coil
在 2.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
实现 高斯模糊
的效果。
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 展示了动态 高斯模糊
效果。