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

最近一个需求要实现类似微信状态的模糊效果,还要求不能引入库,增加包的大小。网上搜了一圈,只有 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部分
        //绘制上面模糊处理后的图片
        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);
        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],如有侵权,请联系删除。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 你可以使用 Python 的 time 模块来测量代码的执行时间。首先你需要导入 time 模块,然后在你要测量的代码的开始和结束处分别调用 time.perf_counter() 函数获取当前时间戳。最后,将两次获取的时间戳相减,就可以得到代码的执行时间。 下面是一个示例: ``` import time def my_function(): # 这里是要测量的代码 time.sleep(1) start_time = time.perf_counter() my_function() end_time = time.perf_counter() print(f'代码执行时间: {end_time - start_time:0.6f} 秒') ``` 这样就可以得到 my_function 函数的执行时间了。 ### 回答2: 要使用Python实现输出代码的耗时,我们可以使用内置的time模块来实现。具体步骤如下: 1. 首先,在代码开始处导入time模块,使用以下代码: ```python import time ``` 2. 在代码开始前,记录当前时间,使用以下代码: ```python start_time = time.time() ``` 3. 在代码完成后,记录当前时间,使用以下代码: ```python end_time = time.time() ``` 4. 计算代码的运行时间,使用以下代码: ```python elapsed_time = end_time - start_time ``` 5. 最后,输出代码的运行时间,使用以下代码: ```python print("代码的运行时间为:", elapsed_time, "秒") ``` 这样,当运行代码时,会在控制台输出代码的耗时时间(单位为秒)。 需要注意的是,这种方式只适用于较小规模的代码,如果代码运行时间过长,可能会导致耗费较多的系统资源。对于较大规模的代码,我们可以考虑使用专业的性能分析工具来进行耗时分析,如cProfile等。 ### 回答3: 在Python中,我们可以使用time模块来测量代码的执行时间。具体步骤如下: 首先,要导入time模块。可以使用以下代码实现: ```python import time ``` 然后,在代码开始执行之前,记录开始时间。可以使用`time.time()`函数获取当前时间戳,以秒为单位。以下代码展示了如何记录开始时间: ```python start_time = time.time() ``` 接下来,在代码执行完毕后,记录结束时间。同样使用`time.time()`函数获取当前时间戳。以下代码展示记录结束时间的方法: ```python end_time = time.time() ``` 最后,计算代码执行的耗时。通过结束时间减去开始时间,可以得到代码的执行时间。以下代码展示了如何计算耗时: ```python execution_time = end_time - start_time print(f"代码执行耗时:{execution_time} 秒") ``` 这样,我们就可以使用Python来测量代码的耗时了。在需要测试耗时的代码前后插入上述代码,即可得到代码的执行时间。注意,这种测试方法只适用于整体代码的耗时,而不适用于单个函数或语句块的耗时。如果需要测试一个函数或语句块的耗时,可以将其封装在一个独立的函数中,并进行相同的计时操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值