效果图
项目要求比例为非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的效果实现效果更好,不会出现图片和背景显示不一致的问题。