1. 程式人生 > >封裝個 Android 的高斯模糊元件

封裝個 Android 的高斯模糊元件

最近基於 Android StackBlur 開源庫,根據自己碰到的需求場景,封裝了個高斯模糊元件,順便記錄一下。

為什麼要自己重複造輪子?

其實也談不上重頭自己造輪子,畢竟是基於大神的開源庫,做了二次封裝。封裝的目的在於,方便外部使用。畢竟有著自己的程式設計習慣,大神的開源庫也只是提供了基礎的功能,現實程式設計中,產品的需求是各種各樣的。

導致每次使用時,都蠻麻煩的,需要額外自己處理蠻多東西。而一旦新的專案又需要接入高斯模糊了,又得重新寫一些程式碼,複製貼上也麻煩,經常由於各種業務耦合報錯。

既然如此,乾脆花時間抽個基礎、公用的高斯模糊元件,需要時直接依賴即可。

基礎理論

高斯模糊

高斯模糊的原理和演算法就不介紹了,我也不懂,沒深入,這裡就大概講講我的粗坯理解:

我們知道,一張圖片,本質上其實是一個個畫素點構成的,雖然經過計算機處理後,呈現在我們眼前的是具體的影象。但在計算機中,其實就是一堆陣列資料。

陣列中每個單位就是一個個畫素點,那麼每個畫素點是儲存什麼內容呢,其實也就是 RGB 或者 ARGB 之類格式的資料。

高斯模糊,大體上就是對這張圖片中的每個畫素點都重新進行計算,每個畫素點的新值等於以它為中心,半徑為 r 的周圍區域內所有畫素點各自按照不同權重相加之和的平均值。

可以粗坯的理解為,本來這個畫素點是要呈現它自己本身的內容,但經過高斯模糊計算後,摻雜進它周圍區域畫素點的內容了。就像加水稀釋類似的道理,既然都摻雜進周圍的內容了,那麼它呈現的內容相比最初,肯定就不那麼清晰了

而如果摻雜的半徑越大,混合進的內容也就越多,那麼它本身的內容就越淡了,是不是這個理。所以,這就是為什麼每個開源的高斯模糊元件庫,使用時基本都需要我們傳入一個 radius 半徑的引數。而且,半徑越大,越模糊。

這麼一粗坯的解釋,就理解多了,是吧。

為什麼需要大概掌握這個理論基礎呢?

想想,高斯模糊是遍歷所有畫素點,對每個畫素點都重新計算。那麼,這自然是一個耗時的工作,掌握了理論基礎,我們要優化時也才有方向去優化。

效能對比

大神的開源庫中提供了三種高斯模糊方式,而我在 Blankj 的 AndroidUtilCode 開源庫中發現了另外一種,所以我將他們都整合起來,一共有四種:

  • Google 官方提供的 RenderScript 方式 (RSBlur)
  • C 編寫的高斯演算法 blur.c 方式 (NativeBlur)
  • Java 編寫的高斯演算法方式1(JavaBlur)
  • Java 編寫的高斯演算法方式2(StackBlur)

其實,大體上就三種:Google 官方提供的,大神用 C 寫的高斯模糊演算法,大神用 Java 寫的高斯模糊演算法。至於後面兩種,看了下,演算法的程式碼不一樣,我就把它們當做是兩種不同的演算法實現了。也許是一樣,但我沒深入去看,反正程式碼不一樣,我就這麼認為了。

下面我們來做個實驗,在如下相同條件下,不同高斯模糊方案的耗時比較:

  • radius=10, scale=1, bitmap=200*200(11.69KB)

ps: radius 表示高斯演算法計算過程中的半徑取值,scale=1表示對 bitmap 原圖進行高斯模糊。這些前提條件需要了解一下,不然你在看網上其他類似效能比對的文章時,發現它們動不動就優化到幾 ms 級別的,然而你自己嘗試卻始終達不到。這是因為也許所使用的這些前提都不一致,不一致的前提下,耗時根本無從比較。

private void testBlur() {
    int sum = 0;
    for (int i = 0; i < 100; i++) {
        long time = SystemClock.uptimeMillis();
        DBlur.source(this, R.drawable.image).modeRs().radius(10).sampling(1).build().doBlurSync();
        long end = SystemClock.uptimeMillis();
        sum += (end - time);
    }
    Log.e("DBlur", "RSBlur cast " + (sum/100) + "ms");
}

程式碼模板如上,分別執行 100 次後取平均值,四種不同方式的耗時如下表:

前提條件 RSBlur NativeBlur JavaBlur StackBlur
radius=10, scale=1, bitmap=200*200 51ms 13ms 162ms 384ms
radius=20, scale=1, bitmap=200*200 56ms 12ms 164ms 435ms
radius=10, scale=2, bitmap=200*200 48ms 11ms 75ms 110ms
radius=10, scale=8, bitmap=200*200 45ms 7ms 14ms 19ms
radius=10, scale=8, bitmap=1920*1180 183ms 143ms 346ms 460ms
radius=20, scale=8, bitmap=1920*1180 204ms 145ms 353ms 510ms
radius=20, scale=1, bitmap=1920*1180 474ms 444ms 8663ms 記憶體溢位

100 次樣本可能不多,但大體上我們也能比較出不同型別的高斯模糊之前的區別,及其適用場景:

  • 總體上,NativeBlur 和 RSBlur 的耗時會少於 JavaBlur 和 StackBlur
  • JavaBlur 和 StackBlur 方式,如果先對 Bitmap 進行縮小,再高斯模糊,最後再放大,耗時會大大縮短
  • radius 增大會增加耗時,但影響不大,但檢視呈現效果會越模糊
  • scale 對原圖縮小倍數越多,耗時越短,但檢視呈現效果同樣會越模糊
  • 解析度越高的圖片,高斯模糊的就越耗時
  • 對於大圖而言,如果要使用 JavaBlur 或 StackBlur,最好設定 scale 先縮小再模糊,否則將非常耗時且容易記憶體溢位
  • 如果已經通過 scale 方式進行優化,那麼最好 radius 值可以相對小一點,否則兩者的值都大會對圖片的模糊效果特別強烈,也許會過了頭

效能優化

高斯模糊的優化考慮點,其實就三個:

  • 選擇不同的高斯模糊方式
  • 通過 scale 對原圖先縮小,再模糊,最後再放大方式
  • 優化高斯模糊演算法

最後一點就不考慮了,畢竟難度太大。那麼,其實就剩下兩種,要麼是從高斯模糊的方案上選擇,要麼從待模糊的圖片上做手腳。

雖然有四種高斯模糊方案,但每種都有各自優缺點:

  • RSBlur 在低端機上可能無法相容
  • NativeBlur 需要生成對應 CPU 架構的 so 檔案支援
  • JavaBlur 和 StackBlur 耗時會較長

優化的考慮點大體上這幾種:

  • 大體上,使用 NativeBlur 或者 RSBlur 即可,如果出現一些問題,那麼此時可考慮切到 JavaBlur 或 StackBlur 方案,但記得結合 scale 方式優化處理。
  • 如果高斯模糊的圖片有實時性要求,要求模糊得同步進行處理,主執行緒後續的工作需要等待高斯模糊後才能夠處理的話,那麼儘量選擇 scale 方式進行優化,減少耗時。
  • 如果對實時性沒要求,但對圖片模糊程度有要求,那其實,只要後臺非同步去進行高斯模糊即可,此時 scale 可不用縮小太多,而利用 radius 來控制模糊效果,以達到理想的要求。
  • 如果兩者都有要求,那就自行嘗試尋找折中點吧。

最後說一點,因為已經封裝成元件庫了,RSBlur 也是引入的 support 包,so 檔案也打包好了,那麼使用這兩種方案足夠滿足絕大部分場景了,所以,沒有特意指定,元件預設的方案為 RSBlur。

二次封裝

需求場景

為什麼要二次封裝?那肯定是因為有自己的各種需求場景的,我的需求如下:

  • 要能夠對當前頁面(Activity)截圖後,進行模糊
  • 要能夠對 drawable 資源圖片進行模糊、或者對指定 View 的檢視進行模糊
  • 模糊完成後,要能夠自動以淡入的動畫方式顯示在指定的控制元件上
  • 存在這種需求場景:對當前介面截圖、並且模糊,模糊後的圖片展示的時機可能在其他介面,因此需要支援快取功能,可以根據指定 cacheKey 值獲取快取
  • 當然,可以根據各種配置使用高斯模糊,當不指定配置時,有預設配置

總結一下,其實封裝要做的事也就是要實現:

  • 截圖、快取、淡入動畫、預設配置
  • 可以的話,元件最好可以達到,其他人在不看文件,不看原始碼前提下,以最少的成本接入直接上手使用

實現

截圖、快取、動畫這些都屬於純功能程式碼的封裝了,具體就不說了。

這裡想來講講,如何設計,可以讓其他人以最少的成本接入直接上手使用。

我的想法是,利用 AndroidStudio 的程式碼提示功能,具體的說就是,你只需要瞭解元件的入口是 DBlur 即可,至於後續怎麼使用,全靠 AndroidStudio 來提示,跟著 AndroidStudio 走就行。

例如:

DBlur入口.png

當敲完 DBlur. 時,會彈出程式碼提示框,入口很少,是吧,就兩個,看命名也能猜到作用:getCacheBitmap() 明顯是用來取快取的,那麼要高斯模糊自然是另外一個入口 source(),這個方法有多個過載函式,看引數,其實也能知道,這就對應著要模糊的圖片的不同來源型別,如:

  • 直接傳入 Bitmap 對其進行模糊
  • 傳入 Activity/View,內部會對這個介面/控制元件進行截圖後再模糊
  • 傳入 resId,對 drawable 資源圖片進行模糊

那麼,可能想問了,哪裡進行高斯模糊配置,哪裡設定同步或非同步,哪裡註冊回撥等等。別急,既然只給你開了一個入口,那麼就跟著入口走下去,自然會一步步引導你走到最後。如:

BlurConfigBuilder入口.png

第一步、第二步該做什麼,我都給你規定好了,你也只能按照步驟一步步來。想要設定高斯模糊配置,你得先指定圖片來源,才能進入第二步,在這裡,可以進行的配置也都給你列出來了,想要哪個,直接設定即可。如:

  • mode()modeRs()modeNative() 等等類似 mode 開頭的方法,用於指定要使用哪種高斯模糊方案,一共四種,每種內部都有提供對應的常量標誌,但如果你不知道哪裡找,那麼直接呼叫 modeXXX 方法即可。
  • radius() 用於設定高斯模糊計算的半徑,內部預設為 4。
  • sampling() 用於設定對原圖的縮小比例,內部預設為 8,即預設先縮小 8 倍,再模糊,最後再放大。
  • cache() 用於設定快取此次模糊後的圖片,沒有呼叫預設不快取。
  • animAlpha() 用於設定使用淡入動畫,需要結合 intoTarget() 使用,否則不生效。
  • intoTarget() 用於設定模糊完成後,自動顯示到指定控制元件上。

另外,看每個方法返回的類名,其實這個過程都是在設定配置項,如果有對 Builder 模式瞭解的話,應該清楚,這個大多用來解決建構函式引數過多的場景,最後一般都會有一個 build() 或者 create() 型別的方法。參考的是 Android 原始碼中 AlertDialog。

也就是說,要進入下一個步驟,需要呼叫 build() 方法,如:

doblur.png

顯然,已經到最後一個步驟了,這裡就是發起高斯模糊工作的地方了。

  • doBlur() 會指定此次高斯模糊工作非同步進行,所以需要註冊回撥的在這裡傳入。
  • doBlurSync() 指定高斯模糊同步進行,模糊後的 Bitmap 直接返回。

至此,接入結束。

使用這個高斯模糊的元件,只需要知道 DBlur 入口,其他都跟隨著 AndroidStudio 程式碼提示一步步往下走即可,當然你也可以直接看原始碼,註釋裡也寫得蠻清楚的了。

這個就是我的想法,能力不足,只能想出這種方案,如果有哪裡需要改進,哪裡不合理,或者有其他思路,歡迎指點一下。

使用示例

compile 'com.dasu.image:blur:0.0.4'
//使用預設配置,最短呼叫鏈
Bitmap bitmap = DBlur.source(MainActivity.this).build().doBlurSync();

//同步模糊,將imageView控制的檢視進行模糊,完成後自動顯示到 imageView1 控制元件上,以淡入動畫方式
DBlur.source(imageView).intoTarget(imageView1).animAlpha().build().doBlurSync();

//非同步模糊,將drawable資原始檔中的圖片以 NATIVE 方式進行模糊,註冊回撥,完成時手動顯示到 imageView1 控制元件上
DBlur.source(this, R.drawable.background).mode(BlurConfig.MODE_NATIVE).build()
      .doBlur(new OnBlurListener() {
            @Override
            public void onBlurSuccess(Bitmap bitmap) {
                imageView1.setImageBitmap(bitmap);
            }

            @Override
            public void onBlurFailed() {
                //do something
            }});

大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支援~ dasuAndroidTv2.png