本文正在参与「金石计划 . 分割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
。
核算出高度后,咱们还不能对图片直接进行高斯含糊,要先要对图片进行缩放。为什么要先进行紧缩呢?有两点原因:
- 运用
RenderScript
进行高斯含糊,最大含糊半径是 25,含糊作用不抱负 - 高斯含糊的半径超越 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部分,这儿界说了两个本地变量 cutImageHeight
和 sampling
;cutImageHeight
是要裁剪图片的高度,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。
最终一步制作中间的原图,就大功告成了,点击发动就能看到突变含糊作用了。文章最终就求一个免费的赞吧