Android 5.0 下毛玻璃(磨砂)效果如何實現?

最近要做一款產品,是在Android手機上的一個launcher應用,PM想實現出和水果手機一樣的毛玻璃效果,但是真心好難。搞了好幾天調研了好久,徹底跪了。 多數的解決方案,一旦做到差不多的效果就需要犧牲很多,系統變得不流暢,內存似乎也佔用很大變得卡頓,如何優化一下。怎麼搞啊,各位大神,實現不了,俺就要回家種地了。

大神好多啊


剛剛做技術調研,可以給一些優缺點的對比.

目前主流實現毛玻璃效果(高斯模糊)分大致三種方法:

一 利用RenderScript介面

利用現有Android結構,通過RenderScript調用底層介面實現高斯模糊計算.

// Remix Blur
private void blur(Bitmap bkg, View view) {

RenderScript rs = RenderScript.create(getActivity());
Allocation overlayAlloc = Allocation.createFromBitmap(rs, overlay);
ScriptIntrinsicBlur blur =
ScriptIntrinsicBlur.create(rs, overlayAlloc.getElement());
blur.setInput(overlayAlloc);
blur.setRadius(radius);
blur.forEach(overlayAlloc);
overlayAlloc.copyTo(overlay);
view.setBackground(new BitmapDrawable(getResources(), overlay));
rs.destroy();
}

說明 :

這種方法直接利用Android系統提供的機制, 從代碼實現上是最簡單也相對高效的實現. 在此過程中遇到的很現實的問題是: 1. 當模糊半徑 (radius)變大後會直接出現計算性能問題. 2. ScriptIntrinsicBlur腳本模糊半徑不能大於25. 因此, 直接使用ScriptIntrinsicBlur不能得到模糊程度較高的圖片.

考慮到高斯模糊是計算周邊像素平均值,在模糊程度很高的情況下,逐點計算平均值從顯示效果上優勢並不明顯.可以通過將圖片壓縮成小圖,計算小圖的高斯模糊, 再將小圖放大的方法來模擬大半徑高斯模糊的效果.

效果如圖:模糊前/模糊後

二 在低版本Android中通過Java實現高斯模糊

可以參考如下代碼實現

blurring/FastBlur.java at master · paveldudka/blurring · GitHubva

這種方式通過在Java層中直接實現高斯模糊演算法.在大模糊半徑和大圖片情況下,性能會出現問題.基本目前只作為對低版本Android客戶端的兼容性方案使用.

三 通過OpenGL直接實現

沒有具體實踐過, 可以參考muzei壁紙的實現, 無論從效果和實時性來講都是最好的. 但需要根據需求來看是否真的有這樣的實時性要求. 這種方式從顯示效果上會優於第一種方法,甚至可以達到漸變的效果.但這些計算都是以耗電作為代價的. 作為Launcher這種系統級別的應用, 在顯示效果差異並不明顯的情況下, 建議綜合考慮耗電量, 以及內存和計算性能的消耗.

如圖:

此功能已經在RemixOS上實現, http://www.jide.com


參考這篇: Android高級模糊技術

這篇文章介紹了幾種類型的模糊技術.

其中一個關鍵技巧是: 先壓縮原圖大小再高斯. 這樣能夠大大減少高斯計算量, 提高性能.


@王宇龍 其實已經說了基本的實現,我補充下我怎麼做到流暢運行的(可以試試鬧鐘one,看其中的毛玻璃效果效率是否足夠高),細看程序實現,發現如果使用RenderScript和壓縮來計算高斯模糊,每次處理模糊的時間到不到20ms,大部分時間花費在了獲取當前控制項的ViewCache上,說下我的思路:

1:首先重寫一個布局(你想在哪個布局來實現模糊)code如下:

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.FrameLayout;

public class BlurFrameLayout extends FrameLayout{
private BlurCalculate mBlurCalculate;
public BlurFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
mBlurCalculate=new BlurCalculate(this);
}
public BlurFrameLayout(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
/***
* radius for linearlayout***/
public void setRadius(int arg0)
{
if(mBlurCalculate!=null)
mBlurCalculate.setRadius(arg0);
invalidate();
}
@Override
protected void onAttachedToWindow() {
// TODO Auto-generated method stub
super.onAttachedToWindow();
mBlurCalculate.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
// TODO Auto-generated method stub
super.onDetachedFromWindow();
mBlurCalculate.BluronDetachedFromWindow();
}
@Override
protected void dispatchDraw(Canvas canvas) {
// TODO Auto-generated method stub
if(mBlurCalculate.isCanvasChanged(canvas))
mBlurCalculate.BlurCanvas();
else
{
mBlurCalculate.DrawCanvas(canvas);
super.dispatchDraw(canvas);

}
}
}

就是當布局在屏幕上顯示的時候才能進行模糊計算,在不顯示的時候不計算,充分利用資源。

對於計算的方法,放到另外一段代碼中,code如下:

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.support.v8.renderscript.Allocation;
import android.support.v8.renderscript.Element;
import android.support.v8.renderscript.RenderScript;
import android.support.v8.renderscript.ScriptIntrinsicBlur;
import android.view.View;
import android.view.ViewTreeObserver;

public class BlurCalculate {
private View mView;
private Bitmap bitmap;
private Canvas mCanvas;
private Rect mRect;
private Matrix mMatrix;
private Matrix mDrawMatrix;
private int realheight,realwidth;
// rs
private RenderScript rs;
private Allocation input;
private Allocation output;
private ScriptIntrinsicBlur script;
private int radius=5;
int i=-1;
private int action=0;
private static final float BITMAP_RATIO=0.1f;
public BlurCalculate(View view) {
this.mView = view;
rs = RenderScript.create(view.getContext());
mCanvas=new Canvas();
mRect=new Rect();
mMatrix=new Matrix();
mDrawMatrix=new Matrix();

//mDrawMatrix.setScale(10.0f, 10.0f);
}
public void setaction(int action)
{
this.action=action;
}
public boolean isCanvasChanged(Canvas canvas)
{
return canvas==mCanvas;
}
public void onAttachedToWindow() {
mView.getViewTreeObserver().addOnPreDrawListener(onPreDrawListener);
}

public void BluronDetachedFromWindow() {
mView.getViewTreeObserver().removeOnPreDrawListener(onPreDrawListener);
if(bitmap!=null)
bitmap.recycle();
bitmap=null;
}
public void DrawCanvas(Canvas canvas)
{
if(bitmap!=null)
canvas.drawBitmap(bitmap, mDrawMatrix, null);

}
public void BlurCanvas()
{
input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE,
Allocation.USAGE_SCRIPT);
output = Allocation.createTyped(rs, input.getType());
script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(radius);
script.setInput(input);
script.forEach(output);
output.copyTo(bitmap);
}
public void setRadius(int arg0)
{
radius=arg0;
}
private void getScreenBitmap()
{
mView.getGlobalVisibleRect(mRect);
realh=mView.getHeight();
realw=mView.getWidth();
int w=Math.round(realwidth*BITMAP_RATIO)+4;
int h=Math.round(realheight*BITMAP_RATIO)+(action==0?-4:4);
w = w ~0x03;
h = h ~0x03;
if(w&<=0||h&<=0) return; if (bitmap == null || bitmap.getWidth() != w || bitmap.getHeight() != h) { bitmap=Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); mMatrix.setScale(BITMAP_RATIO, BITMAP_RATIO); mMatrix.invert(mDrawMatrix); } //(mView.getBottom()-mView.getTop()) float dx = -(Math.min(0, mView.getLeft()) + mRect.left); float dy = action==0?(-(Math.min(0, mView.getTop()) + mRect.top)):-(mRect.bottom-((mView.getBottom()-mView.getTop()))); mCanvas.restoreToCount(1); mCanvas.setBitmap(bitmap); mCanvas.setMatrix(mMatrix); mCanvas.translate(dx, dy); mCanvas.save(); mView.getRootView().draw(mCanvas); //mCanvas.setBitmap(null); } private final ViewTreeObserver.OnPreDrawListener onPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { if (mView.getVisibility() == View.VISIBLE) getScreenBitmap(); return true; } }; }

BITMAP_RATIO 可以調節圖片壓縮的程序,建議在0.2左右就行。

實現的原理就是將每次重寫布局需要繪製的東西攔截下來,進行一次模糊處理,而不是在每次繪製完成了才通過view的截圖功能獲取view要顯示的內容並對其做模糊處理。

---------------------------------------------------------------------------

最後題主提到了如何在5.0上達到高效的繪製,Android 5.0中提供了一個新的高效繪製線程 RenderThread, 沒有細看是什麼,看文檔介紹是如果在主線程卡頓的情況下,依然可以流暢的運行動畫和計算,大牛們可以補充下,希望對題主有幫助 ^-^。


JNI 是逃不掉的了,模糊演算法在 Java 層實現必定對性能造成很大影響。

關於模糊演算法有個可以參考的倒是:

Android StackBlur 圖片模糊效果工具類

項目地址:https://github.com/kikoso/android-stackblur

Demo地址:https://github.com/kikoso/android-stackblur/blob/master/StackBlurDemo/bin/StackBlurDemo.apk?raw=true

文檔介紹:https://github.com/kikoso/android-stackblur#usage

Morning Routine 里的模糊就是用的這個。


很簡單!!!!!給一個圖片,質量壓縮到很小,然後方大,然後對這個圖片進行blur,blur網上有很多方法,都可以用。壓縮到很小後,放大然後blur,速度非常快。。。就是這麼簡單。。。。


這個問題要分兩部分來看。

第一是模糊演算法,主要目前有兩種方案:

(1)藉助Renderscript做高斯模糊,這個本質上是做了個卷積計算。

(2)用jni實現StackBlur 演算法對圖片進行模糊處理,這個可以看下源碼,相較於高斯模糊,計算量小了很多。

兩種方案都可以進行對Bitmap對象的模糊處理,但當模糊半徑增大時,StackBlur能夠保持較好的性能,且不受Renderscript半徑25px的限制。這兩種方案都可以通過對原圖進行壓縮採樣方式優化。至於模糊結果,可以看實際效果,個人認為均可滿足需求。

第二是使用場景,同樣分為兩種:

(1)非實時處理:比如說用做背景虛化,這種場合下對性能要求不是太苛刻,只要保證在非主線程處理好圖片,之後填充即可。上面兩種方案都可滿足需求。

下面是stackblur的一個android實現

kikoso/android-stackblur · GitHub

(2)實時處理:比如說需要在滾動的ListView或者ScrollView上添加毛玻璃遮罩。由於待虛化的Bitmap不是固定不變的,這就需要我們逐幀處理。這個時候半徑如果大的話,Renderscript會出現明顯的卡頓,而StackBlur方案在中高端機器上會有不錯的表現。

下面是一個比較好的開源的android實現,使用起來比較簡單

harism/android_anndblur · GitHub

其原理是自定義容器控制項,通過重繪的方式獲取自身原本要繪製的bitmap紋理,然後實時處理並填充到自身的背景中,能保證基本的流暢度。注意會除了模糊處理,還會引發多餘的繪製操作。鐵定會拖慢UI的流暢度。

對於實時毛玻璃效果,建議還是多做測試,畢竟耗費計算資源,甚至可以監控fps設置閾值對特效進行開關。


不管使用任何模糊實現,只有一個建議,把bitmap控制到最小可用,模糊後再放大使用,才是王道。

經測試50k以內使用RenderScript基本是10ms以內,可以接受;StackBlur和FastBlur兩個差不多,50k以內的bitmap也在10-20ms。

  1. 不管使用Picasso、ImageLoader載入原圖片,一定要設置需要的with、height,減少原始bitmap內存佔用,設置到100*100以下能接受的最小值,即使bitmap是100*100,也會有300kb+內存佔用,還是很大,所以設置盡量小
  2. 如果你使用Glide,你很可能收到異常RSIllegalArgumentException: Unsuported element type,這是因為Glide默認使用RGB_565載入圖片,但是ScriptIntrinsicBlur只支持U8_4 and U8兩種格式,so, You"ll have to convert your bitmap to ARGB_8888 before you send it to RenderScript。
  3. 對bitmap進行壓縮,也很有效,壓縮時間基本是個位數ms級的,壓縮後+模糊,比直接模糊一張大bitmap,時間還要快。

    Matrix matrix = new Matrix();
    matrix.postScale(scaleWidth, scaleHeight);
    Bitmap bitmap = Bitmap.createBitmap(bgimage, 0, 0, (int) width, (int) height, matrix, true);

  4. 如果你使用v8版本的RenderScript兼容包,很好,但是你要能接收你的apk增加2MB左右大小,看自己取捨
  5. 如果使用RenderScript,一定要清楚對象的生命周期,很可能收到各種異常,比如非同步載入圖片回來做模糊,結果Activity已經finish銷毀,你就會收到第1種錯誤,可以在生命周期設置isDestroyed標識,發現已經destory就不要再創建和使用RenderScript對象了(或者傳入application)

    RSInvalidStateException: Calling RS with no Context active.
    RSIllegalArgumentException: Attempting to use an object across contexts.
    RSInvalidStateException: Object already destroyed.
    RSIllegalArgumentException: Cannot update allocation from bitmap, sizes mismatch
    Attempt to invoke virtual method "void android.renderscript.RenderScript.validate()" on a null object reference

  6. 模糊很誘人,使用需謹慎,over


使用的 renderscript


推薦閱讀:

為什麼Google Chrome在書籤欄打開新網頁都在同一個標籤頁打開,不覺得這個設計很蛋疼嗎?
如何用手機賺一些錢或者用互聯網賺些錢?
2016年,醫療界發生了那些大事?
2016 年哪些互聯網技術開始變得流行,哪些過時了?

TAG:互聯網 | iOS應用 | Android開發 | 用戶體驗設計 | Android |