天天看點

Android 彈窗毛玻璃背景實踐Android 彈窗毛玻璃背景實踐

Android 彈窗毛玻璃背景實踐

需求: 點選FloatingActionButton彈出彈層,彈層底部有多個圖示可選,每一個圖示都是一個功能入口,背景采用毛玻璃模糊效果。

記錄一下這個需求的思考和實作過程。

查找Android原生API做法

在Android API裡面,有個FLAG_BLUR_BEHIND用于模糊背景的FLAG,嘗試一下使用它來做Dialog的背景模糊。

代碼如下:

final Dialog dialog = new Dialog(MainActivity.this, R.style.SquareEntranceDialogStyle);
Window window = dialog.getWindow();
if (window != null) {
    window.setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND,
            WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
    window.setGravity(Gravity.BOTTOM);
}
View layout = View.inflate(MainActivity.this, R.layout.square_posting_entrance_dialog, null);
dialog.setContentView(layout);
dialog.show();
           
<style name="SquareEntranceDialogStyle" parent="@android:style/Theme.Dialog">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowIsFloating">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
</style>
           

最後出來的效果有點迷。在4.4的機器上,看不出任何模糊效果,而在7.0的機器上,dialog的彈窗背景出現了一片灰色,而遮罩層的效果沒有任何變化。

檢視了源碼,發現這個FLAG已經在4.1以後就廢棄了。

通過高斯模糊把處理後的圖檔作為彈層背景

換了一種思路,把遮罩模糊拆成了幾個步驟來實作

  1. 把dialog彈層鋪滿整個螢幕
  2. 通過應用内截圖的方式獲得彈層背後的圖檔bitmap
  3. 通過高斯模糊算法對截圖進行模糊處理
  4. 把模糊後的圖檔bitmap設定成dialog的background

把問題拆開以後,單獨實作每一個步驟就容易多了。

1.把dialog彈層鋪滿整個螢幕:

在拆分問題的一開始,我的思路是怎麼去更改dialog的遮罩層顔色,或者把遮罩層整體設定成圖檔,但嘗試了多種方法都無法實作,最後在stackoverflow查到了另一種思路,把dialog鋪滿螢幕,然後修改dialog的windowBackground來達到跟修改遮罩層相同的效果。

通過修改dialog style的windowIsFloating為false,可以使dialog鋪滿螢幕

<style name="SquareEntranceDialogStyle" parent="@android:style/Theme.Dialog">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowIsFloating">false</item>
    <item name="android:windowBackground">@android:color/transparent</item>
</style>
           

把dialog的根布局改成match_parent,并加上一點白色背景色和透明度

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tool="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ebebeb"
    android:alpha="0.8"
    android:gravity="center_horizontal|bottom">
    ...
</LinearLayout>
           
2.通過應用内截圖的方式獲得彈層背後的圖檔bitmap

通過activity的window.getDecorView()擷取到視窗最頂層的view,然後開啟Cache機制使View可以儲存成Bitmap,在每次擷取View截圖前通過destroyDrawingCache()先清除上一次的舊緩存,然後通過buildDrawingCache()重新生成一張最新截圖,最後通過getDrawingCache()擷取截圖Bitmap

View activityView = getWindow().getDecorView();
activityView.setDrawingCacheEnabled(true);
activityView.destroyDrawingCache();
activityView.buildDrawingCache();
Bitmap bmp = activityView.getDrawingCache();
           
3.通過高斯模糊算法對截圖進行模糊處理

高斯模糊算法直接搜尋了一個封裝好了的工具類,通過RenderScript進行模糊處理

public class BlurBitmap {
    /**
     * 圖檔縮放比例
     */
    private static final float BITMAP_SCALE = f;
    /**
     * 最大模糊度(在0.0到25.0之間)
     */
    private static final float BLUR_RADIUS = f;

    /**
     * 模糊圖檔的具體方法
     *
     * @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;
    }
}
           
4.把模糊後的圖檔bitmap設定成dialog的background

最後擷取到模糊處理後的bitmap,設定到dialog的windowBackground中,即可做出彈層模糊背景的效果,完整代碼如下:

final Dialog dialog = new Dialog(MainActivity.this, R.style.SquareEntranceDialogStyle);
Window window = dialog.getWindow();
Bitmap blurBg = null;
if (window != null) {
    long startMs = System.currentTimeMillis();
    // 擷取截圖
    View activityView = getWindow().getDecorView();
    activityView.setDrawingCacheEnabled(true);
    activityView.destroyDrawingCache();
    activityView.buildDrawingCache();
    Bitmap bmp = activityView.getDrawingCache();
    Log.d(TAG, "getDrawingCache take away:" + (System.currentTimeMillis() - startMs) + "ms");
    // 模糊處理并儲存
    blurBg = BlurBitmap.blur(MainActivity.this, bmp);
    Log.d(TAG, "blur take away:" + (System.currentTimeMillis() - startMs) + "ms");
    // 設定成dialog的背景
    window.setBackgroundDrawable(new BitmapDrawable(getResources(), blurBg));
    bmp.recycle();
}
final Bitmap finalBlurBg = blurBg;
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
    @Override
    public void onDismiss(DialogInterface dialog) {
        // 對話框取消時釋放背景圖bitmap
        if (finalBlurBg != null && !finalBlurBg.isRecycled()) {
            finalBlurBg.recycle();
        }
    }
});
View layout = View.inflate(MainActivity.this, R.layout.square_posting_entrance_dialog, null);
dialog.setContentView(layout);
dialog.show();
           

通過打Log檢測擷取截圖以及模糊處理所要消耗的時間,總處理時長大概在50~80ms,還算可以,沒有出現卡頓的情況。

最後補一張效果圖

Android 彈窗毛玻璃背景實踐Android 彈窗毛玻璃背景實踐

參考資料:

How can I change default black dim background “color” (not the amount of dim) of Dialog?

Android高斯模糊(RenderScript)