Android模煳圖像
在Android中,我們可以實現很多很酷的處理圖片的效果。在2014年某次會議的講演《圖像的魔力》中,我介紹了其中的一部分。其中的一項技術是如何模煳圖像,示例代碼是使用RenderScript實現的,因為在Android中沒有內置的可使用的簡單的API。在這個係列中,我們將著眼於RenderScript模煳技術和JAVA實現模煳功能。我們還將進行一些基準測試,以了解每種方案的運行情況,並探討獲取最佳性能的可行方法。
讓我們先從實現一個簡單的可以運行的例子開始,使用RenderScript!對於沒有使用過RenderScript的開發者來說,這是一個讓人心生恐懼的設想,因為RenderScript真的是很難,是不是?是的,的確是。不過它也有一些事情真的很簡單,而模煳圖像就是其中之一。
對於不熟悉RenderScript的人來說,他是在API11中引入的,並且有一個compat庫,提供RenderScript給API8及以後的版本。它本質上是一個麵向圖形的本地計算框架。RenderScript引擎在運行期會選擇最合適的處理器(CPU或者GPU核心,多核處理器間可以分解原子操作)來執行請求的操作。本地語法基於C99,與OpenCL, CUDA, and GLSL的API相似。
如果這聽起來很可怕,請稍等一下,因為我們正要簡化整個過程。因為它是一個框架,允許我們創建自定義的內核來實現過濾與處理,它內置了不少可以使用的內核,其中的一個允許我們模煳圖像。
後麵的實例代碼基於API17及更高版本開發,我已經決定不使用compat庫,因為在本文寫作的時候,Android Studio還不支持。此外,我們使用的模煳核心在API17後才引入,所以有最小SDK版本為17的需求。
讓我們深入到一個簡單的例子,這裏有一個非常簡單的RelativeLayout,包含了一個ImageView,上層還有個TextView:
- <?xml version="1.0" encoding="utf-8"?>
- <RelativeLayout
- xmlns:andro
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:gravity="center"
- android:orientation="vertical" >
-
- <ImageView
- android:
- android:src="@drawable/broadstairs"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="matrix"
- android:layout_centerInParent="true"/>
- <TextView
- android:
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/hello"
- android:layout_centerHorizontal="true"
- android:textColor="<a href="https://www.jobbole.com/members/android/" rel="nofollow">@android</a>:color/white"
- android:layout_marginTop="300dp"
- android:textStyle="bold"
- android:textSize="48sp"/>
- </RelativeLayout>

我們想要做的效果是模煳ImageView內位於TextView顯示區域的圖像,有效的模煳ImageView後的區域。我們最終使用的技術是拿到位於TextView區域的圖像副本進行模煳,然後再將模煳後的副本設定為TextView的背景。
我刻意的設計了這種布局,使圖像顯示實際大小,並從顯示屏的左上方開始。這樣讓隨後的位置計算簡單些,而且這裏討論的是模煳技術而不是圖像定位的數學算法。嚐試設定ImageView的屬性android:scaleType=”center”,就會發現定位出現錯亂。
此處有一個方法,它有3個參數,一個位圖(已在ImageView裏獲得),一個視圖(這是我們的TextView,但是該技術適用於任意的視圖類型,所以我們將在方法中進行支持),以及一個半徑用來控製模煳的程度。
- private void blur(Bitmap bkg, View view, float radius) {
- Bitmap overlay = Bitmap.createBitmap(
- view.getMeasuredWidth(),
- view.getMeasuredHeight(),
- Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(overlay);
- canvas.drawBitmap(bkg, -view.getLeft(),
- -view.getTop(), null);
- RenderScript rs = RenderScript.create(this);
- 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();
- }
我們要做的第一件事是創建一個新的位圖對象,用來保存想要模煳的圖像區域的副本。它的大小將會是我們視圖的尺寸(2-5行)。
現在我們將空的位圖包裝到畫布中,我們可以在上麵繪圖(7行)。
接下來的步驟我稱為”緩存剪切”,我們拷貝位圖中的處於視圖顯示區域內的那部分圖像(9-10行)。
接下來我們必須做的事情是構建一個RenderScript對象上下文,在其中我們執行模煳操作(12行)。
RenderScript是一個本地環境,它在自己的內存空間內運行,所以我們不能簡單的傳遞位圖引用,我們需要在JAVA和RenderScript內存區域進行序列化。這使用了一個分配實例來完成,這是在RenderScript內存區域中創建和引用對象的方式,為我們的位圖創建一個分配會將位圖的內容拷貝的分配區域中(14-15行)。
現在我們創建一個 ScriptIntrinsicBlur實例,它創建合適的RenderScript模煳腳本,並且是腳本對象的Java接口,它讓我們能夠控製它。(17-18行)。
腳本的輸入定義了我們需要執行模煳的位圖源(20行)。
現在我們設置半徑來控製模煳強度(22行)。
forEach方法執行模煳操作,參數代表了結果的輸出位置。我們將它寫回源分配地址以減少創建的對象數量(24行)。
現在模煳完成了,但是我們必須把模煳後的圖像拷貝回JAVA內存區域(26行)。
最後將模煳後的位圖包裝到BitmapDrawable內,並設置為視圖的背景(28-29行)。
我們已經使用完RenderScript上下文,清理掉。這樣做也將自動刪除我們的分配(31行)。
這就是我們完整的基本模煳邏輯,但目前還不清楚什麼地方和什麼時候我們需要調用此方法。不幸的是關於這個問題不能簡單的進行回答,所以我們將在接下來的文章中討論。
平時我喜歡每篇文章都發布相關工作代碼,但是在本例中,我們還沒達到有一個可運行的完整的端到端的示例的程度。我保證在接下來的文章中進行調整。
via:
我們介紹了使用RenderScript使另一個視圖範圍內的圖片部分模煳。但是實際上,我們並沒有深入地調用這個方法來研究圖像模煳行為。原因是我們需要在性能方麵進行仔細考慮,這篇文章我們會進行更進一步地的探索。
調用這個方法最直白的方法是父布局的onDraw()。有經驗的開發者讀到這裏可能會開始搖頭,我們應該保持onDraw方法的實現盡可能有效。以前的文章中的代碼包括創建對象、位圖操作和切換到renderScript上下文。其中,OnDraw會降低幀速率。你可以不相信我的做法,但是可以通過測量並證明它是有效的。在後麵的係列中,我們就會這樣做。
如果布局是靜態的(即我們的布局不包含任何動畫),在布局時就不會改變待模煳的位置和範圍。隻有在布局改變時執行該操作才有意義,但前提是布局的所有視圖大小和位置根據布局變化測量和計算過。這裏有一個非常實用的技巧,可以注冊一個OnGlobalLayoutListener監聽函數。當布局發生改變的時候會調用onGlobalLayout()方法。當我們收到布局已經改變的通知時,注冊的OnPreDrawListener監聽函數的onPreDraw()方法會被調用每當執行onDraw方法。我們要做的第一件事情就是取消注冊onPreDraw()方法,這樣隻有在布局改變的時候才會被調用,而不是每次onDraw方法觸發時都調用。下麵可以執行模煳方法,從這個方法的返回值很重要,使用它可以讓我們放棄onDraw操作,重複之前的布局。這對在回調函數中修改布局非常有幫助,但是這裏不需要這麼做。所以返回true,繼續繪製。
我們的Activity代碼如下:
- public class MainActivity extends Activity {
- private ImageView mImage;
- private TextView mText;
-
- private OnPreDrawListener mPreDrawListener =
- new OnPreDrawListener() {
-
- @Override
- public boolean onPreDraw() {
- ViewTreeObserver observer = mText.getViewTreeObserver();
- if(observer != null) {
- observer.removeOnPreDrawListener(this);
- }
- Drawable drawable = mImage.getDrawable();
- if (drawable != null &&
- drawable instanceof BitmapDrawable) {
- Bitmap bitmap =
- ((BitmapDrawable) drawable).getBitmap();
- if (bitmap != null) {
- blur(bitmap, mText, 25);
- }
- }
- return true;
- }
- };
-
- private OnGlobalLayoutListener mLayoutListener =
- new OnGlobalLayoutListener() {
-
- @Override
- public void onGlobalLayout() {
- ViewTreeObserver observer = mText.getViewTreeObserver();
- if(observer != null) {
- observer.addOnPreDrawListener(
- mPreDrawListener);
- }
- }
- };
-
- /**
- * Called when the activity is first created.
- */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- mImage = (ImageView) findViewById(R.id.image);
- mText = (TextView)findViewById(R.id.text);
- if (mImage != null && mText != null) {
- ViewTreeObserver observer =
- mText.getViewTreeObserver();
- if (observer != null) {
- observer.addOnGlobalLayoutListener(
- mLayoutListener);
- }
- }
- }
-
- private void blur(Bitmap bkg, View view, float radius) {
- ....
- }
- }
在父布局覆蓋onDraw方法的優點是,可以在布局層次上任意附加OnPreDrawListener方法。因此需要自定義一個布局,這個布局是標準布局的子類,這樣就可以覆蓋onDraw方法。使用predrawlistener意味著可以非常容易在任意布局中添加。
最後,模煳圖片實現如下:

在下一篇文章中,我會更深入地回答為什麼要避免在onDraw中執行模煳操作,並且還會介紹有用的性能測量工具。
這篇文章的代碼在這裏。
via:
原文鏈接:
stylingandroid 翻譯:
伯樂在線-
fhdis
最後更新:2017-04-03 12:56:25