1. 程式人生 > >一分鐘實現動態模糊效果(毛玻璃)

一分鐘實現動態模糊效果(毛玻璃)

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

本文轉載自 http://wl9739.github.io/   湫水長天的部落格

現在,越來越多的App裡面使用了模糊效果,我尤其喜歡雅虎天氣的介面,上滑的時候背景圖片會跟著移動,最重要的是背景圖片會根據手指上下移動的距離來進行不同程度的模糊,感覺甚為驚奇,畢竟大家都知道,在Android平臺上進行模糊渲染是一個相當耗CPU也相當耗時的操作,一旦處理不好,卡頓是在所難免的。雖然我並不知道雅虎天氣是怎麼做出這種效果的,但是簡單的模仿一下的話,還是能做到的。

一般來說,考慮到效率,渲染一張圖片最好的方法是使用OpenGL,其次是使用C++/C,使用Java程式碼是最慢的。但是Android推出RenderScript之後,我們就有了新的選擇,測試表明,使用RenderScript的渲染效率和使用C/C++不相上下,但是使用RenderScript卻比使用JNI簡單地多!同時,Android團隊提供了RenderScript

的支援庫,使得在低版本的Android平臺上也能使用。

不過在使用RenderScript之前,對於模糊一張圖片,需要注意的是,我們應該儘量不要使用原尺寸解析度的圖片,最好將圖片縮小比例,這小渲染的效率要高一些。

動態模糊的實現

如何使用RenderScript來模糊一張圖片呢?廢話不多說,先上核心程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public
class BlurBitmap {
/** * 圖片縮放比例 */ private static final float BITMAP_SCALE = 0.4f; /** * 最大模糊度(在0.0到25.0之間) */ private static final float BLUR_RADIUS = 25f; /** * 模糊圖片的具體方法 * * @param context 上下文物件 * @param image 需要模糊的圖片 * @return 模糊處理後的圖片
*/
public static Bitmap blur(Context context, Bitmap image) { // 計算圖片縮小後的長寬 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(BLUR_RADIUS); // 設定blurScript物件的輸入記憶體 blurScript.setInput(tmpIn); // 將輸出資料儲存到輸出記憶體中 blurScript.forEach(tmpOut); // 將資料填充到Allocation中 tmpOut.copyTo(outputBitmap); return outputBitmap; } }

完成上面的程式碼後,需要在app的gradle檔案中新增如下的支援:

1
2
3
4
5
 defaultConfig {
    ......
    renderscriptTargetApi 19
    renderscriptSupportModeEnabled true
}

程式碼做了簡單的註釋以幫助理解,如果需要詳細瞭解,可以查閱官方文件:RenderScript

然後,我們可以看一下模糊前和模糊後的效果對比:

duibi.jpg-94kB

將圖片模糊後,接下來要考慮的是怎麼實現動態模糊效,有一點需要注意的是,即使我們使用了RenderScript這種高效的渲染方式,但是在實際測試中,渲染一張500*700解析度的PNG格式圖片,在我的Pro 6手機上,仍然需要50ms左右的時間,顯然如果使用上面的程式碼進行實時渲染的話,會造成介面嚴重的卡頓。

既然實時渲染這條路走不通,那麼就需要我們另闢蹊徑了,我這裡可以提供一種方法:先將圖片進行最大程度的模糊處理,再將原圖放置在模糊後的圖片上面,通過不斷改變原圖的透明度(Alpha值)來實現動態模糊效果。

簡單的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class MainActivity extends AppCompatActivity {

    /**
     * 原始圖片控制元件
     */
    private ImageView mOriginImg;

    /**
     * 模糊後的圖片控制元件
     */
    private ImageView mBluredImage;

    /**
     * 進度條SeekBar
     */
    private SeekBar mSeekBar;

    /**
     * 顯示進度的文字
     */
    private TextView mProgressTv;

    /**
     * 透明度
     */
    private int mAlpha;

    /**
     * 原始圖片
     */
    private Bitmap mTempBitmap;

    /**
     * 模糊後的圖片
     */
    private Bitmap mFinalBitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 初始化檢視
        initViews();

        // 獲取圖片
        mTempBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dayu);
        mFinalBitmap = BlurBitmap.blur(this, mTempBitmap);

        // 填充模糊後的影象和原圖
        mBluredImage.setImageBitmap(mFinalBitmap);
        mOriginImg.setImageBitmap(mTempBitmap);

        // 處理seekbar滑動事件
        setSeekBar();
    }

    /**
     * 初始化檢視
     */
    private void initViews() {
        mBluredImage = (ImageView) findViewById(R.id.activity_main_blured_img);
        mOriginImg = (ImageView) findViewById(R.id.activity_main_origin_img);
        mSeekBar = (SeekBar) findViewById(R.id.activity_main_seekbar);
        mProgressTv = (TextView) findViewById(R.id.activity_main_progress_tv);
    }

    /**
     * 處理seekbar滑動事件
     */
    private void setSeekBar() {
        mSeekBar.setMax(100);
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                mAlpha = progress;
                mOriginImg.setAlpha((int) (255 - mAlpha * 2.55));
                mProgressTv.setText(String.valueOf(mAlpha));
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
    }
}

xml佈局檔案程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="0dp">

        <ImageView
            android:id="@+id/activity_main_blured_img"
            android:scaleType="centerCrop"
            android:src="@drawable/dayu"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

        <ImageView
            android:id="@+id/activity_main_origin_img"
            android:scaleType="centerCrop"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </FrameLayout>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="80dp">

        <SeekBar
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:id="@+id/activity_main_seekbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginRight="16dp"/>

        <TextView
            android:id="@+id/activity_main_progress_tv"
            android:text="0"
            android:textSize="24sp"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>

</LinearLayout>

效果如下:

blurdemo.gif-3200.7kB

怎麼樣?是不是很簡單的樣子?只需要呼叫模糊處理方法,並在SeekBar的滑動監聽裡面呼叫原影象的setAlpha()方法,來實現動態模糊效果。

你以為這樣就完了?不不不,我們的目的並不是這麼單純,哦,不對,並不是這麼簡單。還記得文章開頭的時候說了嗎?我們的終極目的是要簡單地模仿一下雅虎天氣的介面效果。

仿雅虎天氣介面

有了上面的基礎,就可以很容易地模仿雅虎天氣的介面效果。簡單來說,在上面製作出的效果基礎上,有以下兩點需要注意的地方:

  • 需要要監聽滑動事件,然後再將背景圖片呼叫setTop()方法,將圖片向上平移一段距離。
  • 要向上平移圖片,還需要手動增加圖片的高度,不然圖片向上平移後,底部就會有留白。設定圖片高度的核心程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point point = new Point();
display.getSize(point);
// 獲取到ImageView的高度
int height = point.y;
ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
// 將ImageView的高度增加100
params.height = height + 100;
// 應用更改設定
imageView.requestLayout();

完成上面兩點的內容後,基本就可以模仿出雅虎天氣的首頁了。

結合第一個例子的demo,效果如下:

demo.gif-4725.3kB

相關程式碼已上傳至Github:BlurredView,歡迎Star,Fork。

本著不重複造輪子的原則,我將模糊影象的程式碼上傳至JCenter,大家只需要在專案中新增Gradle引用就行了,具體使用方法請參考上面Github倉庫的ReadMe。