本文已授权「玉刚说」微信公众号独家发布
毛玻璃效果实际上是对原图片的严重劣化,突出朦胧感,一般都是通过图片的缩放+模糊算法来实现,从性能角度考虑,模糊半径不能大于25,所以要更高的模糊效果则需要进行缩放。具体实现方案有以下几种。
Stack模糊
算法RenderScript
实现关于模糊算法及上面各种方案的性能分析可以参考Android动态模糊实现的研究这篇文章
Java代码实现毛玻璃效果基本上都是采用的Stack模糊
算法,该算法比高斯模糊
及均值模糊
算法更高效,效果更好。实现代码如下。
public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) {
// Stack Blur v1.0 from
// http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
//
// Java Author: Mario Klingemann
// http://incubator.quasimondo.com
// created Feburary 29, 2004
// Android port : Yahel Bouaziz
// http://www.kayenko.com
// ported april 5th, 2012
// This is a compromise between Gaussian Blur and Box blur
// It creates much better looking blurs than Box Blur, but is
// 7x faster than my Gaussian Blur implementation.
//
// I called it Stack Blur because this describes best how this
// filter works internally: it creates a kind of moving stack
// of colors whilst scanning through the image. Thereby it
// just has to add one new block of color to the right side
// of the stack and remove the leftmost color. The remaining
// colors on the topmost layer of the stack are either added on
// or reduced by one, depending on if they are on the right or
// on the left side of the stack.
//
// If you are using this algorithm in your code please add
// the following line:
//
// Stack Blur Algorithm by Mario Klingemann
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);
}
由于是Java实现的,所以该方案不存在兼容性问题,也正因为是Java实现的,所以性能不会很好。因此该方案一般作为降级方案使用。
RenderScript
是一个在Android上以高性能运行计算密集型任务的框架。它对执行图像处理,计算摄影或计算机视觉的应用程序尤其有用。RenderScript
提供了一个实现高斯模糊
的类ScriptIntrinsicBlur
,代码如下。
public Bitmap blurBitmap(Bitmap bitmap, int radius) {
//创建一个空bitmap,其大小与我们想要模糊的bitmap大小相同
Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
//实例化一个新的Renderscript
RenderScript rs = RenderScript.create(getApplicationContext());
//创建Allocation对象
Allocation allIn = Allocation.createFromBitmap(rs, bitmap);
Allocation allOut = Allocation.createFromBitmap(rs, outBitmap);
//创建ScriptIntrinsicBlur对象,该对象实现了高斯模糊算法
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
//设置模糊半径,0
blurScript.setRadius(radius);
//执行Renderscript
blurScript.setInput(allIn);
blurScript.forEach(allOut);
//将allOut创建的Bitmap复制到outBitmap
allOut.copyTo(outBitmap);
//释放内存占用
bitmap.recycle();
//销毁Renderscript。
rs.destroy();
return outBitmap;
}
由于RenderScript
的最低支持版本是11,但很多方法都是在17及以后添加的,所以使用RenderScript
的最低版本应该为17。但如果要向下兼容则需要使用谷歌提供的向下兼容库——android.support.v8.renderscript
。由于该库会明显增加APK大小,所以慎重使用。
关于更多RenderScript内容可以去官网查看。
Blurry是GitHub一个比较热门的毛玻璃效果实现库。首先导入该库。
dependencies {
compile 'jp.wasabeef:blurry:3.x.x'
}
由于该库并没有使用RenderScript
的向下兼容库,所以不会导入一些so文件,也就不会增加APK大小。
Blurry
在使用上是非常简单的,只要使用过Glide
,基本上就能快速上手。使用方式如下。
//for ViewGroup
Blurry.with(context)
.radius(10)//模糊半径
.sampling(8)//缩放大小,先缩小再放大
.color(Color.argb(66, 255, 255, 0))//颜色
.async()//是否异步
.animate(500)//显示动画,目前仅支持淡入淡出,默认时间是300毫秒,仅支持传入控件为ViewGroup
.onto(viewGroup);
//for view
Blurry.with(context)
.radius(10)//模糊半径
.sampling(8)//缩放大小,先缩小再放大
.color(Color.argb(66, 255, 255, 0))//颜色
.async()//是否异步
.capture(view)//传入View
.into(view);//显示View
//for bitmap
Blurry.with(context)
.radius(10)//模糊半径
.sampling(8)//缩放大小,先缩小再放大
.color(Color.argb(66, 255, 255, 0))//颜色
.async()//是否异步
.from(bitmap)//传入bitmap
.into(view);//显示View
想必到这里就能很熟练的使用Blurry
了吧。前面介绍过毛玻璃的实现原理,那么Blurry
是怎么来实现毛玻璃效果的尼?其实它就是通过RenderScript
+Java来实现的。来看它的Blur
类,在该类的of
方法中实现了毛玻璃效果。
public static Bitmap of(Context context, Bitmap source, BlurFactor factor) {
int width = factor.width / factor.sampling;
int height = factor.height / factor.sampling;
if (Helper.hasZero(width, height)) {
return null;
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
//进行缩放
canvas.scale(1 / (float) factor.sampling, 1 / (float) factor.sampling);
Paint paint = new Paint();
paint.setFlags(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
PorterDuffColorFilter filter =
new PorterDuffColorFilter(factor.color, PorterDuff.Mode.SRC_ATOP);
//设置颜色
paint.setColorFilter(filter);
canvas.drawBitmap(source, 0, 0, paint);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
//如果当前sdk版本大于17则采用RenderScript实现毛玻璃效果
try {
bitmap = Blur.rs(context, bitmap, factor.radius);
} catch (RSRuntimeException e) {
//当RenderScript出现意外时,采用Java代码来实现毛玻璃效果
bitmap = Blur.stack(bitmap, factor.radius, true);
}
} else {//如果当前sdk版本小于等于17则采用Java来实现毛玻璃效果
bitmap = Blur.stack(bitmap, factor.radius, true);
}
...
}
可以发现上面代码是在对Bitmap缩放后进行处理的,由于RenderScript
的兼容性限制,所以采用了Java实现作为降级方案,因此该库不会存在兼容性问题。实现效果如下。
可以发现Blurry
仅支持在本地图片上实现毛玻璃效果,那么如何对网络图片实现毛玻璃效果尼?可以参考glide-transformations、picasso-transformations、fresco-processors这三个库的实现,由于它们与Blurry
的作者是同一人。所以它们的实现原理与Blurry
一样,但有一点需要注意,glide-transformations
有使用RenderScript
的向下兼容库,所以会明显增加APK大小。
blurkit-android也是GitHub上比较热门的毛玻璃效果实现库。首先导入该库。
dependencies {
implementation 'io.alterac.blurkit:blurkit:1.1.1'
}
blurkit-android
有两种使用方式,使用BlurLayout
控件或者直接对View及Bitmap进行高斯模糊。
先来看BlurLayout
的使用,非常简单。
当然仅在xml文件中定义还不够,还需要在onStart
及onStop
中开启与暂停。
@Override
protected void onStart() {
super.onStart();
blurLayout.startBlur();
blurLayout.lockView();
}
@Override
protected void onStop() {
super.onStop();
blurLayout.pauseBlur();
}
根据以上代码就可以使用BlurLayout
控件。把BlurLayout
作为遮罩,效果还是蛮不错的。效果如下。
关于直接对View及Bitmap进行高斯模糊的使用就更简单了。
//进行BlurKit初始化,在Application中初始化
BlurKit.init(this);
//通过RenderScript进行高斯模糊并返回一个bitmap,iv1可以是一个View,也可以是一个ViewGroup,25是模糊半径
Bitmap bt=BlurKit.getInstance().blur(iv1, 25);
//通过RenderScript进行高斯模糊并返回一个bitmap,传入的是一个bitmap,25是模糊半径
Bitmap bt=BlurKit.getInstance().blur(bitmap, 25);
//通过RenderScript进行高斯模糊并返回一个bitmap,iv1可以是一个View,也可以是一个ViewGroup,25是模糊半径,2代表缩放比例,如果值太大可能会出现OOM
Bitmap bt=BlurKit.getInstance().fastBlur(iv1,25,2)
通过上面的说明想必了解blurkit-android
的使用了。当然blurkit-android
的毛玻璃实现原理也很简单,通过RenderScript
来实现的。
//在类BlurKit中
public Bitmap blur(Bitmap src, int radius) {
final Allocation input = Allocation.createFromBitmap(rs, src);
final Allocation output = Allocation.createTyped(rs, input.getType());
final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(radius);
script.setInput(input);
script.forEach(output);
output.copyTo(src);
return src;
}
下面就来说明一下blurkit-android
中存在的一些问题。
blurkit-android
的1.1.1版本(目前最新版本),不要使用1.1.0版本(虽然GitHub上的使用文档还是1.1.0)。因为使用1.1.1版本时minSdkVersion的值可以是17,而使用1.1.0版本时minSdkVersion的值必须是21。RenderScript
兼容包所需的一些so文件,从而增加APK大小,虽然这些文件并没有用到。BlurLayout
的blk_fps
属性要慎重设置,因为BlurLayout
会每隔(1000/fps)的时间重新绘制一次,也就是BlurLayout
会不停的重新绘制,就会消耗一定的CPU。如果fps为0则绘制一次即可。blurkit-android
的目前的代码中(包括最新版本),BlurLayout
的blk_alpha
属性并不能使用。因为在代码中存在类型转换错误。英语好的同学可以去提issue
。在BlurLayout
中,blk_alpha
属性的类型是float,但在获取值时却以dimension的类型来接收,所以就会出现类型转换错误。
<declare-styleable name="BlurLayout">
...
<attr name="blk_alpha" format="float" />
declare-styleable>
public BlurLayout(Context context, AttributeSet attrs) {
super(context, attrs);
...
try {
//其实blk_alpha的类型是float,把这里的getDimension改成getFloat即可
mAlpha = a.getDimension(R.styleable.BlurLayout_blk_alpha, DEFAULT_ALPHA);
} finally {
a.recycle();
}
...
}
前面讲了2个GitHub上比较热门的开源项目实现,但这两个项目基本上都是通过RenderScript
或者RenderScript
+Java来实现毛玻璃效果的。那么如果要通过NDK或者OpenGL来实现尼?下面就来简单介绍几个通过NDK或者OpenGL来实现毛玻璃效果的开源项目。
RenderScript
兼容包+NDK来实现毛玻璃效果。NDK实现是Stack模糊
算法的C语言版本。需要注意一点的是,在该项目里不能直接编译so文件,需要将blur.c及*.mk文件拿出来单独编译。Stack模糊
、高斯模糊
及均值模糊
这三种算法的实现,也有它们的Java版本、C语言版本、RenderScript
及OpenGL版本。虽然在使用上难度不是很大,但学习起来就有一定难度。有兴趣的话可以去看看上面两个项目,当然需要一定的C语言基础。
到这里想必对Android中毛玻璃效果的实现及原理有了一定的了解,那么在应用中该如何选择实现方案尼?本着以现有轮子优先的原则,下面给出一个选择参考。
Blurry
blurkit-android
,虽然它有点小坑,但完全可以自己解决glide-transformations
、picasso-transformations
、fresco-processors
这三个项目,但不建议直接导入,毕竟仅为一个毛玻璃效果而导入整个库,有点不划算。android-stackblur
及HokoBlur
这两个项目【参考资料】
Android图像处理 - 高斯模糊的原理及实现