最近在开发项目的时候遇到一个UI提出的效果就是PS里面的高斯模糊效果,上图
下面其实是一段文本内容,但是当用户没有获取到某种权限的时候,是不能查看具体的文字内容的(但是又给用户一种下面有文字内容的模糊的感觉)。当用户点击偷瞄一下的时候需要获取某种权限,使这个模糊(遮罩)效果消失,显露出真正的文字内容。UI说在PS里面这叫高斯模糊。自己网上看了看,其实在Android里面也有高斯模糊效果的API,在API 11的时候RenderScript,用来进行高效的图片处理。其实所谓的模糊,就是在以我们真实的图片的基础上,对这张照片来进行处理,然后将处理后的照片,放在真实图片上面,给人一种放否能看清又不能看清的感觉,当年的微信发红包看图片应该也是这样实现的吧,只是将我这里的偷瞄一眼改成了需要发红包,其实是异曲同工之妙吧。先来看看RenderScript对高斯模糊的操作方法吧
public static Bitmap blurBitmap(Bitmap bitmap,Context context){
//Let's create an empty bitmap with the same size of the bitmap we want to blur
Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
//Instantiate a new Renderscript
RenderScript rs = RenderScript.create(context);
//Create an Intrinsic Blur Script using the Renderscript
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
//Create the Allocations (in/out) with the Renderscript and the in/out bitmaps
Allocation allIn = Allocation.createFromBitmap(rs, bitmap);
Allocation allOut = Allocation.createFromBitmap(rs, outBitmap);
//Set the radius of the blur
blurScript.setRadius(1.f); //其实这里就是设置的模糊程度 值越大模糊越厉害 最后可能一团黑 哈哈
//Perform the Renderscript
blurScript.setInput(allIn);
blurScript.forEach(allOut);
//Copy the final bitmap created by the out Allocation to the outBitmap
allOut.copyTo(outBitmap);
//recycle the original bitmap
bitmap.recycle();
//After finishing everything, we destroy the Renderscript.
rs.destroy();
return outBitmap;
}
这里高斯模糊的逻辑其实就是 ,根据你所传入的bitmap,在setRadius(int)设置模糊程度,用RenderScript调用之后 模糊之后 返回模糊之后的图片,再将我们模糊之后的bitmap作为一张照片"盖"在我们所需要模糊(保护)的内容这块区域之上,这样就给人一种神秘的感觉。这是google 给我们提供的api里面有的,在性能更方面应该是比较优的,但是当时没有选择用这种方法,而是选择了github上面的FastBlur方法,至于原因后续再说。但是正如前面所说,RenderScript是api11之后才能使用的,但是低版本怎么办呢,这就需要考虑到兼容性了,还好google也提供了一套方案。下面就兼容低版本的高斯模糊做一些配置问题。其实低版本可以将
supportV8中的包拷贝进来。
1.首先去你的sdk的目录下build-tool目录下找到所需要的jar包,我的路径是 H:\android-sdk-windows\build-tools\23.0.3\renderscript\lib,将renderscript-v8.jar拷贝到我们的工程当中libs下面,以后所有的用到renderScript的相关操作的类,都引用这里面的,不要引用系统默认的。例如import android.renderscript.RenderScript; 改为 import android.support.v8.renderscript.RenderScript;
2.使用RenderScript库,在某些手机或Android版本奔溃的问题 ,错误信息:
H:/AndroidRuntime(4476): android.support.v8.renderscript.RSRuntimeException: Error loading RS
jni library: java.lang.UnsatisfiedLinkError: Couldn't load RSSupport: findLibrary returned null
导入官方jar renderscript-v8.jar 报这个错误 android.support.v8.renderscript.RSRuntimeException: Error loadin 或者 java.lang.UnsatisfiedLinkError: Couldn't load RSSupport from loader dalvik.system.PathClassLoader
这个错误原因是因为在4.4以上的手机上自带 librsjni.so和libRSSupport.so 而在4.0以下,或者某些奇葩手机是没有这两个jni 的.所以我将我H:\android-sdk-windows\build-tools\23.0.3\renderscript\lib\packaged 目录下的文件全部拷贝到lib下面对应的文件夹下面 没有对应的文件夹就创建,如果有对应的文件夹就将文件夹中的内容拷贝进去arm64-v8a,armeabi-v7a,mips,x86。
这2步进行了之后 ,我们就可以完全兼容低版本的问题了额。
在进行高斯模糊的时候 ,我们有时候会遇到这样一种需求,就是当我们的界面(Activity)显示完成之后,我们希望我们模糊效果就能出来,我上面的渲染bitmap的函数中因为函数中用到bitmap.getWidth(),getHeight()获得其宽高,那我们应该在什么时候去调用我们的这个方法 使其能够正常运行呢?在onStart()方法中?还是在onResume()回调方法中?呵呵,其实都不是。实际中做实验的时候,你可去尝试,这个两个函数中是获取不到其宽高的呢,那么下面有几种方法可以使用 ,都是我经过尝试使用之后,能够运行的。
1,如果你在Activity启动完成,界面显示出来的时候需要显示我们对某个控件的模糊效果,我们可以使用Activity的回调方法。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
// TODO Auto-generated method stub
super.onWindowFocusChanged(hasFocus);
if(hasFocus){ //界面完全渲染完成
//我们应该在这里面执行我们的模糊图片的效果
}else{ //界面焦点失去
}
}
为什么在这个方法之中执行呢?不在上面所说的onResume()方法中呢?其实我也说不了很清楚,说说自己的理解吧,可能在onResume()回调方法当中,所有界面并没有完全渲染完成,所有有些控件我们是不能得到他的宽高等属性的,而在onWindowFcusChanged回调方法中,我们所有的UI都渲染完成,能够获得我们的宽高了,上面的blurBitmap也不会报错了。
2,view.post(runnable),将我们的模糊执行的逻辑放在runnalbe中执行,这里我的理解是,通过post将一个runnable投递到消息队列的队尾,然后等待Looper调用此runnable的时候,view已经初始化完成了。
3。ViewTreeObserver:使用ViewTreeObserver的众多回调可以完成这个功能,比如使用addOnPreDrawListener这个接口,不知道为什么这里获取宽高也不会出错。
hover_view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
hover_view.getViewTreeObserver().removeOnPreDrawListener(this); //以免重复执行 所以先移除
textview.buildDrawingCache();
Bitmap bmp = textview.getDrawingCache();
FastBlur.blur(bmp, textview, hover_view, context);
return true;
}
});
我的具体需求是在listView中去给每一个Item拥有模糊效果,由于这个模糊涉及到bitmap,所以当数据Item很多的时候,滑动会存在卡顿现象。因为在blurBitmap函数的27行有一句代码bitmap.recycle(); 当我们listView的item 复用时候 由于bitmap已经被回收了,会造成程序崩溃的bug。所以当时我就考虑用github上面的FastBlur去实现(后来才发现其实RenderScript也可以实现)这种模糊效果。
看看gitHub上面的FastBulr的模糊效果的具体实现:
public static Bitmap doBlur(Bitmap sentBitmap, int radius,
boolean canReuseInBitmap) {
Bitmap bitmap;
if (canReuseInBitmap) {
bitmap = sentBitmap;
} else {
bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
}
if (radius < 1) {
return (null);
}
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] pix = new int[w * h];
bitmap.getPixels(pix, 0, w, 0, 0, w, h);
int wm = w - 1;
int hm = h - 1;
int wh = w * h;
int div = radius + radius + 1;
int r[] = new int[wh];
int g[] = new int[wh];
int b[] = new int[wh];
int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
int vmin[] = new int[Math.max(w, h)];
int divsum = (div + 1) >> 1;
divsum *= divsum;
int dv[] = new int[256 * divsum];
for (i = 0; i < 256 * divsum; i++) {
dv[i] = (i / divsum);
}
yw = yi = 0;
int[][] stack = new int[div][3];
int stackpointer;
int stackstart;
int[] sir;
int rbs;
int r1 = radius + 1;
int routsum, goutsum, boutsum;
int rinsum, ginsum, binsum;
for (y = 0; y < h; y++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
for (i = -radius; i <= radius; i++) {
p = pix[yi + Math.min(wm, Math.max(i, 0))];
sir = stack[i + radius];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rbs = r1 - Math.abs(i);
rsum += sir[0] * rbs;
gsum += sir[1] * rbs;
bsum += sir[2] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
}
stackpointer = radius;
for (x = 0; x < w; x++) {
r[yi] = dv[rsum];
g[yi] = dv[gsum];
b[yi] = dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if (y == 0) {
vmin[x] = Math.min(x + radius + 1, wm);
}
p = pix[yw + vmin[x]];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer = (stackpointer + 1) % div;
sir = stack[(stackpointer) % div];
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];
yi++;
}
yw += w;
}
for (x = 0; x < w; x++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
yp = -radius * w;
for (i = -radius; i <= radius; i++) {
yi = Math.max(0, yp) + x;
sir = stack[i + radius];
sir[0] = r[yi];
sir[1] = g[yi];
sir[2] = b[yi];
rbs = r1 - Math.abs(i);
rsum += r[yi] * rbs;
gsum += g[yi] * rbs;
bsum += b[yi] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
if (i < hm) {
yp += w;
}
}
yi = x;
stackpointer = radius;
for (y = 0; y < h; y++) {
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16)
| (dv[gsum] << 8) | dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if (x == 0) {
vmin[y] = Math.min(y + r1, hm) * w;
}
p = x + vmin[y];
sir[0] = r[p];
sir[1] = g[p];
sir[2] = b[p];
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer = (stackpointer + 1) % div;
sir = stack[stackpointer];
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];
yi += w;
}
}
bitmap.setPixels(pix, 0, w, 0, 0, w, h);
return bitmap;
}
其实也就是对bitmap进行了一定的像素处理。参数sentBitmap其实就是我们要处理的图片,radius就是传入的要处理达到的模糊度,canReuseInBitmap标识是否能够重用这个sentBitmap. 加载bitmap和渲染的问题我是采用上面的
addOnPreDrawListener来处理的。对于处理listview中加载并且模糊的卡顿效果我采用的方法是对bitmap采取压缩之后再模糊处理 将处理之后的图片在通过bitmap的缩放,放大到我需要的指定大小。
public static void blur(Bitmap bkg, TextView textview, ImageView view,
Context context) {
int radius = 2;
float scaleFactor = 8;
Bitmap overlay = Bitmap.createBitmap(
(int) (textview.getMeasuredWidth() / scaleFactor),
(int) (textview.getMeasuredHeight() / scaleFactor),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(overlay);
Paint paint = new Paint();
paint.setFlags(Paint.FILTER_BITMAP_FLAG);
canvas.translate(-textview.getLeft() / scaleFactor, -textview.getTop()
/ scaleFactor);
canvas.scale(1 / scaleFactor, 1 / scaleFactor);
canvas.drawBitmap(bkg, 0, 0, paint);
// overlay = doBlur(overlay, radius, true);
overlay = blurBitmap(overlay, context);
view.setBackground(new BitmapDrawable(context.getResources(), zoomImg(
overlay, textview.getMeasuredWidth(),
textview.getMeasuredHeight())));
}
/**
* 处理图片
*
* @param bm
* 所要转换的bitmap
* @param newWidth新的宽
* @param newHeight新的高
* @return 指定宽高的bitmap
*/
public static Bitmap zoomImg(Bitmap bm, int newWidth, int newHeight) {
// 获得图片的宽高
int width = bm.getWidth();
int height = bm.getHeight();
// 计算缩放比例
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// 取得想要缩放的matrix参数
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
// 得到新的图片
Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix,
true);
return newbm;
}
函数blur通过我们需要模糊的控件(此处为TextView)产生的bitmap结合textView的宽和高创建一个缩小了scaleFactor倍大小的已经有模糊效果的bitmap对象,最后再通过zoomImg方法放大scaleFactor倍数(这里的通用性可能不好,应该将scaleFactor写成输入参数)放到我们定义好的以及悬浮在TextView(需要模糊效果)的上面的wrapContent大小的ImageView里面,用setBackGround放大设置位背景,这样就覆盖住了TextView。由于开始ImageView没有任何内容和背景颜色 ,所有他并不现实出来,之后设置背景之后才会现实出来了。下面是效果的对比。传送门