本文已授權「玉剛說」微信公眾號獨家釋出
毛玻璃效果實際上是對原圖片的嚴重劣化,突出朦朧感,一般都是通過圖片的縮放+模糊演算法來實現,從效能角度考慮,模糊半徑不能大於25,所以要更高的模糊效果則需要進行縮放。具體實現方案有以下幾種。
- Java實現,一般都是採用
Stack模糊
演算法 RenderScript
實現- Native實現
- OpenCV或者OpenGL實現,由於其複雜度,本文暫不討論該方案
關於模糊演算法及上面各種方案的效能分析可以參考Android動態模糊實現的研究這篇文章
1、Java實現
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 <mario at quasimondo.com>
// http://incubator.quasimondo.com
// created Feburary 29, 2004
// Android port : Yahel Bouaziz <yahel at kayenko.com>
// 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 <mario@quasimondo.com>
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實現的,所以效能不會很好。因此該方案一般作為降級方案使用。
2、RenderScript實現
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 <radius <= 25
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內容可以去官網檢視。
3、開源專案
3.1、Blurry的使用
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大小。
3.2、blurkit-android的使用
blurkit-android也是GitHub上比較熱門的毛玻璃效果實現庫。首先匯入該庫。
dependencies {
implementation 'io.alterac.blurkit:blurkit:1.1.1'
}
複製程式碼
blurkit-android
有兩種使用方式,使用BlurLayout
控制元件或者直接對View及Bitmap進行高斯模糊。
先來看BlurLayout
的使用,非常簡單。
<io.alterac.blurkit.BlurLayout xmlns:blurkit="http://schemas.android.com/apk/res-auto"
android:id="@+id/blurLayout"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_centerInParent="true"
blurkit:blk_fps="60"//每過1000/fps的時間重新繪製一次BlurLayout,
blurkit:blk_alpha="0.5"//透明度
blurkit:blk_blurRadius="15"//模糊半徑
blurkit:blk_cornerRadius="30dp"//BlurLayout的圓角半徑
blurkit:blk_downscaleFactor="0.12"//縮放大小,是先放大再縮小,所以值太大則有可能OOM
>
複製程式碼
當然僅在xml檔案中定義還不夠,還需要在onStart
及onStop
中開啟與暫停。
@Override
protected void onStart() {
super.onStart();
blurLayout.startBlur();
blurLayout.lockView();
}
@Override
protected void onStop() {
super.onStop();
blurLayout.pauseBlur();
}
複製程式碼
根據以上程式碼就可以使用BlurLayout
控制元件。把BlurLayout
作為遮罩,效果還是蠻不錯的。效果如下。
//進行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();
}
...
}
複製程式碼
3.3、其他開源專案
前面講了2個GitHub上比較熱門的開源專案實現,但這兩個專案基本上都是通過RenderScript
或者RenderScript
+Java來實現毛玻璃效果的。那麼如果要通過NDK或者OpenGL來實現尼?下面就來簡單介紹幾個通過NDK或者OpenGL來實現毛玻璃效果的開源專案。
- android-stackblur是GitHub一個比較熱門的毛玻璃效果開源專案,但使用起來就要麻煩一點。它沒有現成的包可以匯入,需要直接複製程式碼到應用中,所以也需要我們自己來編譯so檔案。它採用了Java+
RenderScript
相容包+NDK來實現毛玻璃效果。NDK實現是Stack模糊
演算法的C語言版本。需要注意一點的是,在該專案裡不能直接編譯so檔案,需要將blur.c及*.mk檔案拿出來單獨編譯。 - HokoBlur這個開源專案雖然star不多,但是也挺有意思的,它不僅有
Stack模糊
、高斯模糊
及均值模糊
這三種演算法的實現,也有它們的Java版本、C語言版本、RenderScript
及OpenGL版本。雖然在使用上難度不是很大,但學習起來就有一定難度。
有興趣的話可以去看看上面兩個專案,當然需要一定的C語言基礎。
4、總結
到這裡想必對Android中毛玻璃效果的實現及原理有了一定的瞭解,那麼在應用中該如何選擇實現方案尼?本著以現有輪子優先的原則,下面給出一個選擇參考。
- 如果對Bitmap或者View進行模糊處理則優先使用
Blurry
- 如果要遮罩效果,則優先使用
blurkit-android
,雖然它有點小坑,但完全可以自己解決 - 如果要對網路圖片進行模糊處理,可以參考
glide-transformations
、picasso-transformations
、fresco-processors
這三個專案,但不建議直接匯入,畢竟僅為一個毛玻璃效果而匯入整個庫,有點不划算。 - 如果想要自己來實現毛玻璃效果,可以參考
android-stackblur
及HokoBlur
這兩個專案
【參考資料】