Android動態高斯模糊效果教程

Iamxiarui發表於2016-09-12

寫在前面

最近一直在做畢設專案的準備工作,考慮到可能要用到一個模糊的效果,所以就學習了一些高斯模糊效果的實現。比較有名的就是 FastBlur 以及它衍生的一些優化方案,還有就是今天要說的RenderScript 。

因為這東西是現在需要才去學習的,所以關於一些影像處理和渲染問題就不提了。不過在使用的過程中確實能感受到,雖然不同的方案都能實現相同的模糊效果,但是效率差別真的很大。

本篇文章實現的高斯模糊是根據下面這篇文章學習的,先推薦一下。本文內容與其內容差不多,只是稍微講的詳細一點,並修改了程式碼中部分實現邏輯和細節上的處理。不過主體內容不變,所以選擇哪篇文章去學都是一樣的。

下面就來看一下,如何去實現這樣的高斯模糊效果。

QQ圖片20160905201508

簡單聊聊 Renderscript

因為效果的實現是基於 Renderscript 的,所以有必要先來了解一下。

從它的官方文件來看,說的很是玄乎。我們只需要知道一點就好了:

RenderScript is a framework for running computationally intensive tasks at high performance on Android.

Renderscript 是 Android 平臺上進行高效能運算的框架。

既然是高效能運算,那麼說明 RenderScript 對影像的處理非常強大,所以用它來實現高斯模糊還是比較好的選擇。

那麼如何使用它呢?從官方文件中可以看到,如果需要在 Java 程式碼中使用 Renderscript 的話,就必須依賴 android.renderscript 或者android.support.v8.renderscript 中的 API 。既然有 API 那就好辦多了。

下面簡單說一下使用的步驟,這也是官方文件中的說明:

  • 首先需要通過 Context 建立一個 Renderscript ;
  • 其次通過建立的 Renderscript 來建立一個自己需要的指令碼( ScriptIntrinsic ),比如這裡需要模糊,那就是 ScriptIntrinsicBlur ;
  • 然後至少建立一個 Allocation 類來建立、分配記憶體空間;
  • 接著就是對影像進行一些處理,比如說模糊處理;
  • 處理完成後,需要剛才的 Allocation 類來填充分配好的記憶體空間;
  • 最後可以選擇性的對一些資源進行回收。

文件中的解釋永遠很規矩,比較難懂,我們結合原博主 湫水長天 的程式碼來看一看步驟:

/**
 * @author Qiushui
 * @description 模糊圖片工具類
 * @revision Xiarui 16.09.05
 */
public class BlurBitmapUtil {
    //圖片縮放比例
    private static final float BITMAP_SCALE = 0.4f;

    /**
     * 模糊圖片的具體方法
     *
     * @param context 上下文物件
     * @param image   需要模糊的圖片
     * @return 模糊處理後的圖片
     */
    public static Bitmap blurBitmap(Context context, Bitmap image,float blurRadius) {
        // 計算圖片縮小後的長寬
        int width = Math.round(image.getWidth() * BITMAP_SCALE);
        int height = Math.round(image.getHeight() * BITMAP_SCALE);

        // 將縮小後的圖片做為預渲染的圖片
        Bitmap inputBitmap = Bitmap.createScaledBitmap(image, width, height, false);
        // 建立一張渲染後的輸出圖片
        Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);

        // 建立RenderScript核心物件
        RenderScript rs = RenderScript.create(context);
        // 建立一個模糊效果的RenderScript的工具物件
        ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

        // 由於RenderScript並沒有使用VM來分配記憶體,所以需要使用Allocation類來建立和分配記憶體空間
        // 建立Allocation物件的時候其實記憶體是空的,需要使用copyTo()將資料填充進去
        Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
        Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);

        // 設定渲染的模糊程度, 25f是最大模糊度
        blurScript.setRadius(blurRadius);
        // 設定blurScript物件的輸入記憶體
        blurScript.setInput(tmpIn);
        // 將輸出資料儲存到輸出記憶體中
        blurScript.forEach(tmpOut);

        // 將資料填充到Allocation中
        tmpOut.copyTo(outputBitmap);

        return outputBitmap;
    }
}

上面就是處理高斯模糊的程式碼,其中註釋寫的十分詳細,而且已經將圖片縮放處理了一下。結合剛才說的步驟,大家應該能有一個大概的印象,實在不懂也沒關係,這是一個工具類,直接 Copy 過來即可。

當然,原博主將程式碼封裝成輪子了,也可以直接在專案中引用 Gradle 也是可以的,但是我覺得原始碼還是要看一看的。

簡單的模糊

好了,有了一個大概的印象後,來看一下如何實現高斯模糊效果吧!

首先你可以在專案中直接引用原博主封裝的輪子:

compile 'com.qiushui:blurredview:0.8.1'

如果不想引用的話,就必須在當前 Module 的 build.gradle 中新增如下程式碼:

defaultConfig {
    renderscriptTargetApi 19
    renderscriptSupportModeEnabled true
}

等構建好就可以使用了。如果構建失敗的話,只需要把 minSdkVersion 設定成 19 就好了,暫時不知是何原因。不過從 StackOverflow 中瞭解到這是個Bug ,那就不必深究。

現在來看程式碼實現,首先佈局檔案中就一個 ImageView ,沒啥好說的,從上面的模糊圖片工具類可以看出,要想獲得一個高斯模糊效果的圖片,需要三樣東西:

  • Context:上下文物件
  • Bitmap:需要模糊的圖片
  • BlurRadius:模糊程度

這裡需要注意一下:

目前這種方案只適用於 PNG 格式的圖片,而且圖片大小最好小一點,雖然程式碼中已經縮放了圖片,但仍然可能會出現卡頓的情況。

現在只要設定一下圖片和模糊程度就好了:

/**
 * 初始化View
 */
@SuppressWarnings("deprecation")
private void initView() {
    basicImage = (ImageView) findViewById(R.id.iv_basic_pic);
    //拿到初始圖
    Bitmap initBitmap = BitmapUtil.drawableToBitmap(getResources().getDrawable(R.raw.pic));
    //處理得到模糊效果的圖
    Bitmap blurBitmap = BlurBitmapUtil.blurBitmap(this, initBitmap, 20f);
    basicImage.setImageBitmap(blurBitmap);
}

來看一下執行圖:

模糊

可以看到,圖片已經實現了模糊效果,而且速度還蠻快的,總的來說通過 BlurBitmapUtil.blurBitmap()就能得到一張模糊效果的圖 。

自定義模糊控制元件

原博主的輪子裡給我們封裝了一個自定義的 BlurredView ,剛開始我覺得沒必要自定義。後來發現自定義的原因是需要實現動態模糊效果。

那為什麼不能手動去設定模糊程度呢?他給出的解釋是:

“如果使用上面的程式碼進行實時渲染的話,會造成介面嚴重的卡頓。”

我也親自試了一試,確實有點卡。他實現動態模糊處理的方案是這樣的:

“先將圖片進行最大程度的模糊處理,再將原圖放置在模糊後的圖片上面,通過不斷改變原圖的透明度(Alpha值)來實現動態模糊效果。”

這個方案確實很巧妙的實現動態效果,但是注意如果要使用這種方式,就必須有兩張一模一樣的圖片。如果在程式碼中直接寫,就需要兩個控制元件,如果圖片多的話,顯然是不可取的。所以輪子裡有一個自定義的 BlurredView 。

不過這個 BlurredView 封裝的不是太好,我刪減了一部分內容,原因稍後再說。先來看一下核心程式碼。

首先是自定義的 BlurredView 繼承於 RelativeLayout ,在佈局檔案中可以看到,裡面有兩個 ImageView,且是疊在一起的。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <ImageView
        android:id="@+id/blurredview_blurred_img"
        .../>

    <ImageView
        android:id="@+id/blurredview_origin_img"
        .../>

</FrameLayout>

同時也定義了一些屬性:

<resources>
    <declare-styleable name="BlurredView">
        <attr name="src" format="reference"/>
        <attr name="disableBlurred" format="boolean"/>
    </declare-styleable>
</resources>

一個是設定圖片,一個是設定是否禁用模糊。最後就是 BlurredView 類,程式碼如下,有大量刪減,只貼出核心程式碼:

/**
 * @author Qiushui
 * @description 自定義模糊View類
 * @revision Xiarui 16.09.05
 */
public class BlurredView extends RelativeLayout {

    /*========== 全域性相關 ==========*/
    private Context mContext;//上下文物件
    private static final int ALPHA_MAX_VALUE = 255;//透明最大值
    private static final float BLUR_RADIUS = 25f;//最大模糊度(在0.0到25.0之間)

    /*========== 圖片相關 ==========*/
    private ImageView mOriginImg;//原圖ImageView
    private ImageView mBlurredImg;//模糊後的ImageView
    private Bitmap mBlurredBitmap;//模糊後的Bitmap
    private Bitmap mOriginBitmap;//原圖Bitmap

    /*========== 屬性相關 ==========*/
    private boolean isDisableBlurred;//是否禁用模糊效果

    ...

    /**
     * 以程式碼的方式新增待模糊的圖片
     *
     * @param blurredBitmap 待模糊的圖片
     */
    public void setBlurredImg(Bitmap blurredBitmap) {
        if (null != blurredBitmap) {
            mOriginBitmap = blurredBitmap;
            mBlurredBitmap = BlurBitmapUtil.blurBitmap(mContext, blurredBitmap, BLUR_RADIUS);
            setImageView();
        }
    }
    ...

    /**
     * 填充ImageView
     */
    private void setImageView() {
        mBlurredImg.setImageBitmap(mBlurredBitmap);
        mOriginImg.setImageBitmap(mOriginBitmap);
    }

    /**
     * 設定模糊程度
     *
     * @param level 模糊程度, 數值在 0~100 之間.
     */
    @SuppressWarnings("deprecation")
    public void setBlurredLevel(int level) {
        //超過模糊級別範圍 直接拋異常
        if (level < 0 || level > 100) {
            throw new IllegalStateException("No validate level, the value must be 0~100");
        }

        //禁用模糊直接返回
        if (isDisableBlurred) {
            return;
        }

        //設定透明度
        mOriginImg.setAlpha((int) (ALPHA_MAX_VALUE - level * 2.55));
    }

    ...
}

從程式碼中可以看到,最核心的就是下面三個方法:

  • setBlurredImg(Bitmap blurredBitmap):設定圖片,並複製兩份;
  • setImageView():給兩個ImageView設定相應的圖片,內部呼叫;
  • setBlurredLevel(int level):設定透明程度;

思路就是先選定一張圖片,一張作為原圖,一張作為模糊處理過的圖。再分別將這兩張圖設定給自定義 BlurredView 中的兩個 ImageView ,最後處理模糊過後的那張圖的透明度。

好了,現在來寫一個自定義的模糊效果圖,首先是佈局,很簡單:

 <com.blurdemo.view.BlurredView
        android:id="@+id/bv_custom_blur"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:src="@raw/pic"
        app:disableBlurred="false" />

可以看到,設定了圖片,設定了開啟模糊,那麼我們在Activity中只需設定透明程度即可:

private void initView() {
        customBView = (BlurredView) findViewById(R.id.bv_custom_blur);
        //設定模糊度
        customBView.setBlurredLevel(100);
    }

效果圖與上圖一樣,這裡就不重複貼了。可以看到,程式碼簡單了很多,不過僅僅因為方便簡單可不是自定義 View 的作用,作用在於接下來要說的 動態模糊效果 的實現。

動態模糊

我們先來看一下啥叫動態模糊效果:

動態模糊

從圖中可以看到,隨著我們觸控螢幕的時候,背景的模糊程度會跟著變化。如果要直接設定其模糊度會及其的卡頓,所以正如原博主所說,可以用兩張圖片來實現。

大體思路就是,上面的圖片模糊處理,下面的圖片不處理,然後通過手勢改變上面模糊圖片的透明度即可。

所以跟前面的程式碼幾乎一樣,只需要重寫 onTouchEvent 方法即可:

/**
 * 初始化View
 */
private void initView() {
    customBView = (BlurredView) findViewById(R.id.bv_dynamic_blur);
    //設定初始模糊度
    initLevel = 100;
    customBView.setBlurredLevel(initLevel);
}

/**
 * 觸控事件
 */
@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downY = ev.getY();
            break;

        case MotionEvent.ACTION_MOVE:
            float moveY = ev.getY();
            //手指滑動距離
            float offsetY = moveY - downY;
            //螢幕高度 十倍是為了看出展示效果
            int screenY = getWindowManager().getDefaultDisplay().getHeight() * 10;
            //手指滑動距離佔螢幕的百分比
            movePercent = offsetY / screenY;
            currentLevel = initLevel + (int) (movePercent * 100);
            if (currentLevel < 0) {
                currentLevel = 0;
            }
            if (currentLevel > 100) {
                currentLevel = 100;
            }
            //設定模糊度
            customBView.setBlurredLevel(currentLevel);
            //更改初始模糊等級
            initLevel = currentLevel;
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.onTouchEvent(ev);
}

從程式碼中可以看到,這裡是通過手指滑動距離佔螢幕的百分比來計算改變後的透明等級的,程式碼應該不難,很容易理解。當然原博主部落格中是通過進度條來改變的,也是可以的,就不在贅述了。

與 RecylcerView 相結合

先來看一張效果圖,這個圖也是仿照原博主去實現的,但是還是有略微的不同。

RecylcerView

本來的自定義 BlurredView 中還有幾段程式碼是改變背景圖的位置的,因為希望上拉下拉的時候背景圖也是可以移動的,但是從體驗來看效果不是太好,上拉的過程中會出現留白的問題。

雖然原博主給出瞭解決方案:手動給背景圖增加一個高度,但這並不是最好的解決方式,所以我就此功能給刪去了,等找到更好的實現方式再來補充。

現在來看如何實現?首先佈局就是底下一層自定義的 BlurredView ,上面一個 RecylcerViewRecylcerView 有兩個 Type ,一個是頭佈局,一個是底下的列表,很簡單,就不詳細說了。

重點仍然是動態模糊的實現,在上面的動態模糊中,我們採取了重寫 onTouchEvent 方法,但是這裡剛好是 RecylcerView ,我們可以根據它的滾動監聽,也就是 onScrollListener 來完成動態改變透明度,核心方法如下:

    //RecyclerView 滾動監聽
    mainRView.setOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            //滾動距離
            mScrollerY += dy;
            //根據滾動距離控制模糊程度 滾動距離是模糊程度的十倍
            if (Math.abs(mScrollerY) > 1000) {
                mAlpha = 100;
            } else {
                mAlpha = Math.abs(mScrollerY) / 10;
            }
            //設定透明度等級
            recyclerBView.setBlurredLevel(mAlpha);
        }
    });

程式碼很簡單,就是在 onScrolled 方法中計算並動態改變透明度,只要掌握了原理,實現起來還是很容易的。

總結

從前面所有的動態圖可以看到,執行起來還是比較快的,但是我從 Android Monitor 中看到,在每一次剛開始渲染模糊的時候,GPU 渲染的時間都很長,所以說可能在效能方面還是有所欠佳。

GPU情況

當然也可能跟模擬器有關係,真機上測試是很快的。而且貌似比 FastBlur 還快一點,等有空測試幾個高斯模糊實現方法的效能,來對比一下。

到此,這種實現高斯模糊的方法已經全部講完了,感謝原博主這麼優秀的文章,再次附上鍊接:

湫水長天 – 教你一分鐘實現動態模糊效果

其他參考資料

RenderScript – Android Developers

Android RenderScript入門(1)

高斯模糊效果實現方案及效能對比 – lcyFox

專案原始碼

BlurDemo – IamXiaRui – Github

相關文章