注册

耗时一周,实现高仿微信渐变模糊效果——纯原生实现

最近一个需求要实现类似微信状态的模糊效果,还要求不能引入库,增加包的大小。网上搜了一圈,只有 Flutter 的实现。没办法只能自己开撸,实现效果如下,上面的图是我的实现效果,下面的是微信的实现效果。



Screenshot_2022-11-12-14-46-40-11_2d4809a04714b92ad0ec5f736efb755b.jpg


956ac151237d8da1ad0bc9edebd6744a.jpg



实现原理


首先,我们观察一下下面的微信状态的实现效果。可以看出上部分是截取了头发部分进行了高斯模糊;而下面部分则是对围裙进行高斯模糊。


image.png


拿原图进行对比,我们可以发现,渐变高斯模糊的部分遮住了原图片,同时还有渐变的效果。最后,图片好像加了一层灰色的遮罩,整体偏灰。

接下来,我们要做的事情就清楚了。


第一步:选取原图片的上下两部分分别进行高斯模糊
第二步:自定义 OnDraw 方法,让高斯模糊的部分覆盖原图片的上下两部分
第三步:让高斯模糊的图片实现渐变效果


选取原图片的上下两部分分别进行高斯模糊


在开始高斯模糊前,我们需要先确定上下两部分的高度。需要注意的是,我们不能直接使用图片的高度,因为图片的宽不一定等于屏幕的宽度。因此,我们需要按照比例计算出图片缩放后的高度。代码如下:



//最后要求显示的图片宽度为屏幕宽度
int requireWidth = UIUtils.getScreenWidth(context);
int screenHeight = UIUtils.getScreenHeight(context);
//按照比例,计算出要求显示的图片高度
int requireHeight = requireWidth * source.getHeight() / source.getWidth();
int topOrBottomBlurImageHeight = (int) ((screenHeight - requireHeight) / 2 + requireHeight * 0.25f);

如下图所示,最后一步 (screenHeight - requireHeight) / 2 获取到缩放后的图片居中时的上下两部分的高度。但是,渐变高斯模糊的部分还需要增加 padding 来遮住原图片的部分内容,这里的 padding 取的是 requireHeight * 0.25f


企业微信截图_89ec2f08-1741-4789-9c7c-943be98e3f68.png


计算出高度后,我们还不能对图片直接进行高斯模糊,要先要对图片进行缩放。为什么要先进行压缩呢?有两点原因:



  1. 使用 RenderScript 进行高斯模糊,最大模糊半径是 25,模糊效果不理想
  2. 高斯模糊的半径超过 10 之后就有性能问题

为了解决上面的问题,我们需要先对图片进行缩放,再进行高斯模糊。核心代码如下,为了后面使用协程,这里是用 kotlin 实现的。



private val filter = PorterDuffColorFilter(Color.argb(140, 0, 0, 0), PorterDuff.Mode.SRC_ATOP)

private fun blurBitmap(
source: Bitmap,
radius: Int,
top: Boolean,
topOrBottomBlurImageHeight: Int,
screenHeight: Int,
context: Context?
): Bitmap? {

//第1部分
val cutImageHeight = topOrBottomBlurImageHeight * source.height / screenHeight
val sampling = 30

//第2部分
val outBitmap = Bitmap.createBitmap(source.width / sampling,
cutImageHeight / sampling, Bitmap.Config.ARGB_8888)
val canvas = Canvas(outBitmap)
canvas.scale(1 / sampling.toFloat(), 1 / sampling.toFloat())
val paint = Paint()
paint.flags = Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG
//过滤颜色值
paint.colorFilter = filter
val dstRect = Rect(0, 0, source.width, cutImageHeight)
val srcRect: Rect = if (top) {//截取顶部
Rect(0, 0, source.width, cutImageHeight)
} else {//截取底部
Rect(0, source.height - cutImageHeight, source.width, source.height)
}
canvas.drawBitmap(source, srcRect, dstRect, paint)

//高斯模糊
val result = realBlur(context, outBitmap, radius)

//创建指定大小的新 Bitmap,内部会对传入的原 Bitmap 进行拉伸
val scaled = Bitmap.createScaledBitmap(
result,
(source.width),
(cutImageHeight),
true)
return scaled
}

代码看不懂?没关系,下面会一一来讲解:


第1部分,这里定义了两个本地变量 cutImageHeightsamplingcutImageHeight 是要裁剪图片的高度,sampling 是缩放的比例。你可能会奇怪 cutImageHeight 的计算方式。如下图所示,cutImageHeight 是用 topOrBottomBlurImageHeight 占屏幕高度的比例计算的,目的是让不同的图片裁剪的高度不同,这也是微信状态模糊的效果。如果你想固定裁剪比例,完全可以修改 cutImageHeight 的计算方式。


image.png


第2部分,这里就做了一件事,就是截取原图的部分并压缩。这里比较难理解的就是为什么创建 Bitmap 时,它的宽高已经缩小了,但是还需要调用 canvas.scale。其实,canvas.scale 只会作用于 canvas.drawBitmap 里的原 Bitmap


高斯模糊这里可以采取你项目里之前使用的方式就行,如果之前没做过高斯模糊,可以看Android图像处理 - 高斯模糊的原理及实现。这里使用的是 Google 原生的方式,代码如下:


@Throws(RSRuntimeException::class)
private fun realBlur(context: Context?, bitmap: Bitmap, radius: Int): Bitmap {
var rs: RenderScript? = null
var input: Allocation? = null
var output: Allocation? = null
var blur: ScriptIntrinsicBlur? = null
try {
rs = RenderScript.create(context)
rs.messageHandler = RenderScript.RSMessageHandler()
input = Allocation.createFromBitmap(
rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE,
Allocation.USAGE_SCRIPT
)
output = Allocation.createTyped(rs, input.type)
blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
blur.setInput(input)
blur.setRadius(radius.toFloat())
blur.forEach(output)
output.copyTo(bitmap)
} finally {
rs?.destroy()
input?.destroy()
output?.destroy()
blur?.destroy()
}
return bitmap
}

还有一点细节,由于我们给高斯模糊的图片加了 filter ,为了保持一致性。我们也需要给原 Bitmap 进行过滤。代码如下:


private fun blurSrc(bitmap: Bitmap): Bitmap? {
if (bitmap.isRecycled) {
return null
}
val outBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(outBitmap)
val paint = Paint()
paint.flags = Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG
paint.colorFilter = filter
canvas.drawBitmap(bitmap, 0f, 0f, paint)
return outBitmap
}

最后,我们可以使用协程来获取处理后的 Bitmap ,代码如下


fun wxBlurBitmap(source: Bitmap, topOrBottomBlurImageHeight: Int, screenHeight: Int, context: Context?, imageView: BlurImageView) {
if(source.isRecycled) {
return
}
GlobalScope.launch(Dispatchers.Default) {
val time = measureTimeMillis {
val filterBitmap = async {
blurSrc(source)
}
val topBitmap = async {
blurBitmap(source, 10, true, topOrBottomBlurImageHeight, screenHeight, context)
}
val bottomBitmap = async {
blurBitmap(source, 10, false, topOrBottomBlurImageHeight, screenHeight, context)
}
val src = filterBitmap.await()
val top = topBitmap.await()
val bottom = bottomBitmap.await()
launch(Dispatchers.Main) {
if(top == null || bottom == null) {
imageView.setImageBitmap(source)
} else {
imageView.setBlurBitmap(src, top, bottom, topOrBottomBlurImageHeight)

}

}
}
}
}

自定义 ImageView


上面的操作,我们获得了3个 Bitmap,要把它们正确的摆放就需要我们自定义一个 ImageView。如果对自定义 View 不了解的话,可以看看扔物线大佬的 Hencoder 的自定义View系列 教程。代码如下:


public class BlurImageView extends androidx.appcompat.widget.AppCompatImageView {

private Bitmap mSrcBitmap;
private Bitmap mTopBlurBitmap;
private Bitmap mBottomBlurBitmap;
private Matrix mDrawMatrix;
private Paint mPaint;
private Shader mTopShader;
private Shader mBottomShader;
private PorterDuffXfermode mSrcPorterDuffXfermode;
private PorterDuffXfermode mBlurPorterDuffXfermode;
private int mTopOrBottomBlurImageHeight;

public BlurImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

/**
* 设置图片
* @param src 原图片的 Bitmap
* @param top 原图片top部分的 Bitmap
* @param bottom 原图片bottom部分的 Bitmap
* @param topOrBottomBlurImageHeight 模糊图片要求的高度
*/
public void setBlurBitmap(Bitmap src, Bitmap top, Bitmap bottom, int topOrBottomBlurImageHeight) {
this.mSrcBitmap = src;
this.mTopBlurBitmap = top;
this.mBottomBlurBitmap = bottom;
this.mTopOrBottomBlurImageHeight = topOrBottomBlurImageHeight;
invalidate();
}

private void init() {
mPaint = new Paint();
mDrawMatrix = new Matrix();
mPaint.setAntiAlias(true);
mSrcPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP);
mBlurPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP);
}

@Override
protected void onDraw(Canvas canvas) {
if(mSrcBitmap == null || mTopBlurBitmap == null || mBottomBlurBitmap == null) {
super.onDraw(canvas);
return;
}
if(mSrcBitmap.isRecycled() || mTopBlurBitmap.isRecycled() || mBottomBlurBitmap.isRecycled()) {
mSrcBitmap = null;
mTopBlurBitmap = null;
mBottomBlurBitmap = null;
return;
}

int save = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);

//第1部分
final int srcWidth = mSrcBitmap.getWidth();
final int srcHeight = mSrcBitmap.getHeight();
final int topWidth = mTopBlurBitmap.getWidth();
final int topHeight = mTopBlurBitmap.getHeight();
final int contentWidth = getWidth() - getPaddingLeft() - getPaddingRight();
final int contentHeight = getHeight() - getPaddingTop() - getPaddingBottom();
float scrBitmapScale = (float) contentWidth / (float) srcWidth;
float srcTopOrBottomPadding = (contentHeight - srcHeight * scrBitmapScale) * 0.5f;
int requireBlurHeight = mTopOrBottomBlurImageHeight;
float overSrcPadding = requireBlurHeight - srcTopOrBottomPadding;//要求的模糊图片的高度
float dx = 0;//缩放后的模糊图片的x方向的偏移
float dy = 0;//缩放后的模糊图片的y方向的偏移
float blurScale = 0;//高斯模糊图片的缩放比例
if(requireBlurHeight * topWidth >= topHeight * contentWidth) {
//按照高缩放
blurScale = (float) requireBlurHeight / (float) topHeight;
dx = (contentWidth - topWidth * blurScale) * 0.5f;
} else {
//按照宽缩放,因为按照高缩放时,当前Bitmap无法铺满
blurScale = (float) contentWidth / (float) topWidth;
dy = (requireBlurHeight - topHeight * blurScale) * 0.5f;
}

//第2部分
//绘制上面模糊处理后的图片,注意如果作为RecyclerView的Item,则不能复用mTopShader,
//需要每次 new 一个新的对象
if(mTopShader == null) {
mTopShader = new LinearGradient((float) contentWidth / 2, requireBlurHeight, (float) contentWidth / 2, srcTopOrBottomPadding, new int[]{
0x00FFFFFF,
0xFFFFFFFF
}, null, Shader.TileMode.CLAMP);
}
mPaint.setShader(mTopShader);
mDrawMatrix.setScale(blurScale, blurScale);
mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
canvas.drawBitmap(mTopBlurBitmap, mDrawMatrix, null);
mPaint.setXfermode(mBlurPorterDuffXfermode);
canvas.drawRect(0, srcTopOrBottomPadding, contentWidth, requireBlurHeight, mPaint);
//绘制下面模糊处理后的图片
float padding = contentHeight - requireBlurHeight;
mDrawMatrix.setScale(blurScale, blurScale);
mDrawMatrix.postTranslate(Math.round(dx), Math.round(padding + dy));
canvas.drawBitmap(mBottomBlurBitmap, mDrawMatrix, null);
//注意如果作为RecyclerView的Item,则不能复用mBottomShader,
//需要每次 new 一个新的对象
if(mBottomShader == null) {
mBottomShader = new LinearGradient((float) contentWidth/2, padding + overSrcPadding, (float) contentWidth/2, padding, new int[]{
0xFFFFFFFF,
0x00FFFFFF
}, null, Shader.TileMode.CLAMP);
}
mPaint.setShader(null);
mPaint.setShader(mBottomShader);
canvas.drawRect(0, padding + overSrcPadding, contentWidth, padding, mPaint);

//绘制中间的原图
mPaint.setShader(null);
mPaint.setXfermode(mSrcPorterDuffXfermode);
float srcScale = (float) contentWidth / (float) srcWidth;
mDrawMatrix.setScale(srcScale, srcScale);
mDrawMatrix.postTranslate(0, Math.round(srcTopOrBottomPadding));
canvas.drawBitmap(mSrcBitmap, mDrawMatrix, mPaint);
canvas.restoreToCount(save);
}
}

BlurImageView 得核心代码在 onDraw 里面。我们按照上面注释的顺序,一个一个来分析:


第1部分,我们声明了几个变量,用来辅助计算。为了方便理解,我画了如下示意图:


image.png


srcTopOrBottomPadding: 是原图按照比例缩放、居中摆放时空白的高度
overSrcPadding: 是模糊图片遮罩原图片的高度,也就是渐变模糊图片的高度
dx: 按照高度缩放时,缩放后的模糊图片的x方向的偏移
dy: 按照宽缩放时,缩放后的模糊图片的y方向的偏移
blurScale: 图上没有标出,是高斯模糊图片的缩放比例。确保高斯模糊的图片能够铺满


第2部分,这里的作用是绘制上下两部分的模糊图片,并对图片的部分进行渐变处理。以上面部分的图片为例,第一步先绘制已经处理好的 mTopBlurBitmap,这里设置了 Matrix ,在绘制过程中会对图片进行缩放和移动,让图片的位置摆放正确。第二步就是对部分图片进行渐变处理,这里合成模式选择了 DST_ATOP


最后一步绘制中间的原图,就大功告成了,点击启动就能看到渐变模糊效果了。文章最后就求一个免费的赞吧🥺🥺


作者:新一代螺丝工
链接:https://juejin.cn/post/7165030436366712845
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册