Android图片添加高斯模糊背景实现方案

1,325 阅读4分钟

效果图

image.png 项目要求比例为非1:1 的图片则增加高斯模糊背景效果,事先并不知晓图片的宽高。项目中集成了两种图片框架Fresco和Glide

方案-:使用Fresco图片加载框架

使用两层ImageView的方式,上层显示原始图片,下层显示高斯模糊背景,xml 实现如下:(SHImageView, PhotoDraweeView为继承自SimpleDraweeView实现的图片控件,封装了一些项目中常用的属性, SimpleDraweeView为Fresco的图片显示控件)

<?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">


    <com.shizhi.shihuoapp.library.imageview.SHImageView
        android:id="@+id/iv_blur_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:visibility="gone"
        app:viewAspectRatio="1"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

    <cn.shihuo.modulelib.views.photodraweeview.PhotoDraweeView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

首先加载原始图片,获取到图片的宽高,判断图片宽高比是否为1:1

url?.let {
    loadImage(it, img) { isBlur, imageInfo ->
        //如果图片宽高比是非1:1的图片, 就设置高斯模糊背景
        if (isBlur) {
            blurImg.resetVisibility(true)
            loadBlurImage(imageInfo, it, blurImg)
        } else {
            blurImg.resetVisibility(false)
        }
    }
}
private fun loadImage(url: String, image: PhotoDraweeView, doBlurImage: ((isBlur: Boolean, imageInfo: ImageInfo?)-> Unit)?) {
    val requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url))
    val request = requestBuilder.build()
    val controller = Fresco.newDraweeControllerBuilder()
        .setAutoPlayAnimations(true)
        .setOldController(image.controller)
        .setControllerListener(object : BaseControllerListener<ImageInfo?>() {
            override fun onFinalImageSet(
                id: String,
                imageInfo: ImageInfo?,
                animatable: Animatable?
            ) {
                super.onFinalImageSet(id, imageInfo, animatable)
                if (imageInfo == null) {
                    return
                }
                image.update(imageInfo.width, imageInfo.height)
                val imgRatio = imageInfo.width/imageInfo.height
                val isBlur = imgRatio != 1
                doBlurImage?.invoke(isBlur, imageInfo)
            }
        })
        .setImageRequest(request)
    image.controller = controller.build()
}

当图片宽高比不等于1:1 时,给图片的叠加一层高斯模糊背景,高斯模糊图片也使用Fresco实现 第一种方案就实现完成了,但是发现这种方式显示的交互效果并不友好,需要原图片加载完成之后再去加载高斯模糊背景,就会导致图片比背景先显示出来,有一个背景的白屏闪动。

private fun loadBlurImage(url: Uri, blurImg: PhotoDraweeView, isBlur: Boolean) {
   val requestBuilder = ImageRequestBuilder.newBuilderWithSource(url)
   requestBuilder.postprocessor = IterativeBoxBlurPostProcessor(6, 50)
   val request = requestBuilder.build()
   Fresco.getImagePipeline().prefetchToBitmapCache(request, context)
   val controller = Fresco.newDraweeControllerBuilder()
       .setLowResImageRequest(request)
       .setImageRequest(request)
       .setOldController(blurImg.controller)
       .setControllerListener(object : BaseControllerListener<ImageInfo?>() {
           override fun onFinalImageSet(
               id: String,
               imageInfo: ImageInfo?,
               animatable: Animatable?
           ) {
               super.onFinalImageSet(id, imageInfo, animatable)
               if (imageInfo == null) {
                   return
               }
               blurImg.update(imageInfo.width, imageInfo.height)
           }
       })
   blurImg.controller = controller.build()
}

这里考虑到是否可以先通过缓存来解决问题,通过LruCache缓存图片,当打开页面的时候直接读取缓存,会比从网络加载要快,这里实现了BitmapLruCacheUtils 通过自定义LruCache缓存背景图片,除此之外,还可以先填充原图片,给原图片设置一个透明度去进行过渡,优化后的方案。

private fun loadBlurImage(imageInfo: ImageInfo?, url: String?, blurImg: SHImageView) {
    //优先从缓存中获取bitmap, 解决大图模式和详情页交互的时候高斯模糊背景闪烁问题
    if (BitmapLruCacheUtils.instance.isBitmapCache(url)) {
        BitmapLruCacheUtils.instance.loadBitmap(url, blurImg)
        return
    }
    imageInfo?.let {
        if ((imageInfo as? CloseableStaticBitmap)?.underlyingBitmap != null && (imageInfo as? CloseableStaticBitmap)?.underlyingBitmap?.isRecycled == false) {
            blurImg.setImageBitmap((imageInfo as? CloseableStaticBitmap)?.underlyingBitmap)
            blurImg.imageAlpha = 125
        }
        ThreadUtils.executeByIo(object : ThreadUtils.SimpleTask<Bitmap>() {
            override fun doInBackground(): Bitmap? {
                var realBitmap = (imageInfo as? CloseableStaticBitmap)?.underlyingBitmap?.let {
                    Bitmap.createScaledBitmap(it, imageInfo.width / 5, imageInfo.height / 5, false)
                }
                if (realBitmap != null && !realBitmap.isRecycled) {
                    NativeBlurFilter.iterativeBoxBlur(realBitmap, 6, 20)
                    //将bitmap放入缓存
                    BitmapLruCacheUtils.instance.putBitmapToCache(url, realBitmap)
                    return realBitmap
                }
                return null
            }

            override fun onSuccess(result: Bitmap?) {
                if (result != null && !result.isRecycled) {
                    blurImg.imageAlpha = 255
                    blurImg.setImageBitmap(result)
                }
            }

        })
    }
}

到这里方案一的实现就完成了,但是并不能完全解决背景后加载显示的问题

方案二:使用Glide实现

Glide的transform,可以进行图片变换,那么我们可以利用transform在图片加载过程中进行处理,拿到原图片的bitmap后,对bitmap进行高斯模糊处理,然后将原bitmap 和高斯模糊后的bitmap合成为一个bitmap再返回显示。

实现BitmapBlurTransformation 继承自BitmapTransformation

/**
 * @desc: 图片高斯模糊转换合成类
 * @author: wzc
 * @date: 2023/3/7
 **/
class BitmapBlurTransformation(
    val radius: Int,
    val sampling: Int,
    private val bitmapWidth: Int,
    private val bitmapHeight: Int
): BitmapTransformation() {
    companion object {
        private const val ID = "com.module.commdity.blur.BitmapBlurTransformation"
        private val CHARSET: Charset = Charset.forName(STRING_CHARSET_NAME)
    }

    override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val imgRatio = toTransform.width/toTransform.height.toFloat()
        if (imgRatio == 1.0f) {
            return toTransform
        }
        //非1:1的图片实现高斯模糊
        var composeBitmap: Bitmap = pool[toTransform.width / sampling, toTransform.height / sampling, Bitmap.Config.ARGB_8888]
        if (composeBitmap == null) {
            composeBitmap = Bitmap.createBitmap(toTransform.width / sampling, toTransform.height / sampling, Bitmap.Config.ARGB_8888)
        }
        return composeBitmap(toTransform, composeBitmap)
    }

    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        messageDigest.update((ID + radius + sampling).toByteArray(CHARSET))
    }

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

    override fun equals(other: Any?): Boolean {
        return other is BitmapBlurTransformation && other.radius == radius && other.sampling == sampling
    }

    override fun hashCode(): Int {
        return ID.hashCode() + radius * 1000 + sampling * 10
    }
    
    /**
    * 两个bitmap合成为一个bitmap
    */
    private fun composeBitmap(source: Bitmap, composeBitmap: Bitmap): Bitmap {
        try {
            val canvas =Canvas(composeBitmap)
            canvas.scale(1.0f / sampling.toFloat(), 1.0f / sampling.toFloat())
            val paint = Paint()
            paint.flags = Paint.FILTER_BITMAP_FLAG
            paint.isAntiAlias = true
            canvas.drawBitmap(source, 0.0f, 0.0f, paint)
            NativeBlurFilter.iterativeBoxBlur(composeBitmap, sampling, radius)
            val newBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, source.config)
            val newCanvas = Canvas(newBitmap)
            val scaleGlideBitmap = scaleBlurBitmap(composeBitmap)
            val scaleResourceBitmap = scaleOriginBitmap(source)
            newCanvas.drawBitmap(scaleGlideBitmap, 0.0f, 0.0f, paint)
            //获取原图片bitmap的宽高
            val originWidth = source.width
            val originHeight = source.height
            //如果宽大于高,计算绘制原图片的位置top 点位置
            //(bitmapHeight - scaleResourceBitmap.height) / 2.toFloat()
            if (originWidth > originHeight) {
                newCanvas.drawBitmap(scaleResourceBitmap, 0f, (bitmapHeight - scaleResourceBitmap.height) / 2.toFloat(), paint)
            } else { //如果高大于宽,则计算绘制原图片的位置left 点位置
                     //(bitmapWidth - scaleResourceBitmap.width) / 2.toFloat()
                newCanvas.drawBitmap(scaleResourceBitmap, (bitmapWidth - scaleResourceBitmap.width) / 2.toFloat(), 0f, paint)
            }
            newCanvas.save() //保存
            newCanvas.restore() //存储
            return newBitmap
        } catch (e: Exception) {
            e.printStackTrace()
            //异常返回原图
            return source
        }

    }
    

    /**
    * 计算原图片真实的宽高比例,而不是控件的宽高
    */
    private fun scaleOriginBitmap(bkg: Bitmap): Bitmap {
        val originWidth = bkg.width
        val originHeight = bkg.height
        val scaleWidth = bitmapHeight.toFloat() / originHeight
        val scaleHeight = bitmapWidth.toFloat() / originWidth
        val matrix = Matrix()
        if (originWidth > originHeight) {
            matrix.postScale(scaleHeight, scaleHeight)
        } else {
            matrix.postScale(scaleWidth, scaleWidth)
        }
        return Bitmap.createBitmap(
            bkg, 0, 0, bkg.width, bkg.height, matrix,
            true
        )
    }
    
    /**
    * 拉伸背景图片的宽高为控件的显示宽高
    */
    private fun scaleBlurBitmap(bitmap: Bitmap): Bitmap {
        val scaleWidth = bitmapWidth.toFloat() / bitmap.width
        val scaleHeight = bitmapHeight.toFloat() / bitmap.height
        val matrix = Matrix()
        matrix.postScale(scaleWidth, scaleHeight)
        return Bitmap.createBitmap(
            bitmap, 0, 0, bitmap.width, bitmap.height, matrix,
            true
        )
    }

}

然后通过Glide 加载图片显示

/**
 * 带高斯模糊背景的图片
 */
private fun loadCompressImage(binding: DetailItemLevelOneChannelListBinding, url: String?) {
    Glide.with(context)
        .load(url)
        .apply(RequestOptions().transform(BitmapBlurTransformation(20, 6, dp2px(68f), dp2px(68f))))
        .into(binding.detailItemChannelImage)
}

两种方案对比下来,Glide的效果实现效果更好,不会出现图片和背景显示不一致的问题。