原创作者:AchillesL
若转载文章,请在明显的位置标明文章出处
最近在做仿写网易云音乐播放界面时,学习过图片的毛玻璃效果处理,毛玻璃效果其实就是图片模糊。
简单的模糊
对于模糊效果,我们一般使用StackBlur模糊算法以及它衍生的一些优化方案。在这里,我们会使用到StackBlur模糊算法的java实现类。
public class FastBlurUtil {
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);
}
}
代码看不懂没关系,这是个工具类,我们先学会怎么去用也是可以的。先看下doBlur方法:
public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap)
第一个参数是需要模糊化的Bitmap对象,第二个参数是模糊半径,第三个参数表示是否复用。
这里需要注意一下
一般来讲,使用这个工具类时,传入的图片对象不能太大,否则容易OOM,同时性能上也会有问题。因此,我们一般都会先将图片缩小处理。
使用起来非常简单,传入图片,设置模糊半径就好了:
private Bitmap getBlurBitmap() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap originBitmap = null;
try {
originBitmap = BitmapFactory.decodeStream(getAssets().open(PIC_NAME), null,
options);
} catch (IOException e) {
e.printStackTrace();
}
Bitmap thumbnailBitmap = Bitmap.createScaledBitmap(originBitmap, originBitmap.getWidth() / 10,
originBitmap.getHeight() / 10, false);
Bitmap blurBitmap = FastBlurUtil.doBlur(thumbnailBitmap, 8, false);
return blurBitmap;
}
看一下效果:
可以看到,图片已经实现了模糊效果,质量还是挺不错的。
动态模糊
我们进一步探索,思考怎么实现动态模糊。可以直接动态修改上面的模糊半径吗?不行,解释如下:
如果使用上述代码进行实时渲染,会造成严重的掉帧
一般来讲,我们实现动态模糊的方法如下:
先得到模糊后的图片,然后把原图叠加在上面,通过不断改变原图的透明度(Alpha),实现动态模糊
这个方法很巧妙,但是需要注意这种实现方式,必须用到两张图片,有读者可能想到使用两个ImageView来加载两张图。
其实,有更好的方法,那就是使用LayerDrawable。
LayerDrawable类似photoShop图层的概念。
它包含一个Drawable数组,并将这些Drawable对象的数组顺序来绘制它们,索引最大的Drawable对象将会被绘制在最上面。
我们先在布局中添加一个SeekBar控件,用于动态调节模糊程度。
<SeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="20dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"/>
先创建一个两个元素的Drawable数组,分别存放模糊后的图片以及原图。原图的索引为1,因此会显示在最上层。最后用Drawable数组来初始化LayerDrawable对象。
Drawable[] drawables = new Drawable[2];
drawables[0] = new BitmapDrawable(blurBitmap);
drawables[1] = new BitmapDrawable(originBitmap);
LayerDrawable layerDrawable = new LayerDrawable(drawables);
当SeekBar滑动时,我们通过LayerDrawable的getDrawable方法取出原图,用SeekBar的值算出并改变原图的透明度,然后动态设置背景。
mSeekBar.setMax(1000);
mSeekBar.setProgress(0);
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
int alpha = (int) (255 - (i * 1.0 / 1000) * 255);
mLayerDrawable.getDrawable(1).setAlpha(alpha);
mRootView.setBackground(mLayerDrawable);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
我们可以看到效果,还是挺不错的:
但是我们发现一个细节,SeekBar从0滑动到一半这个阶段,模糊的效果并不显著。如果需要前半段范围的效果更明显一点,该怎么办呢?
这种方法的模糊效果,和我们计算透明度的算法有关。
明显的,我们计算透明度算法,是个斜率k= 1的一次函数,如果改成平方根函数呢?如下图所示,可以看到平方根函数比斜率k为1 的一次函数向Y轴“倾向的时间更早”。
于是我们改一下计算透明度的算法:
int alpha = (int) (255 - Math.sqrt((i * 1.0 / 1000)) * 255);
运行程序:
看到效果,明显看到比第一种的算法要好一些。