本文正在参与「金石计划 . 分割6万现金大奖」

最近一个需求要完成相似微信状况的含糊作用,还要求不能引入库,添加包的巨细。网上搜了一圈,只有 Flutter 的完成。没办法只能自己开撸,完成作用如下,上面的图是我的完成作用,下面的是微信的完成作用。

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

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

完成原理

首先,咱们观察一下下面的微信状况的完成作用。能够看出上部分是截取了头发部分进行了高斯含糊;而下面部分则是对围裙进行高斯含糊。

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

拿原图进行比照,咱们能够发现,突变高斯含糊的部分遮住了原图片,一起还有突变的作用。最终,图片好像加了一层灰色的遮罩,整体偏灰。

接下来,咱们要做的工作就清楚了。

第一步:选取原图片的上下两部分别离进行高斯含糊 第二步:自界说 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

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

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

  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 的核算方式。

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

第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部分,咱们声明了几个变量,用来辅佐核算。为了方便了解,我画了如下示意图:

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

srcTopOrBottomPadding: 是原图依照份额缩放、居中摆放时空白的高度 overSrcPadding: 是含糊图片遮罩原图片的高度,也便是突变含糊图片的高度 dx: 依照高度缩放时,缩放后的含糊图片的x方向的偏移 dy: 依照宽缩放时,缩放后的含糊图片的y方向的偏移 blurScale: 图上没有标出,是高斯含糊图片的缩放份额。保证高斯含糊的图片能够铺满

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

最终一步制作中间的原图,就大功告成了,点击发动就能看到突变含糊作用了。文章最终就求一个免费的赞吧