操作Android窗口的幾種方式?WindowInsets與其兼容庫的使用與踩坑

語言: CN / TW / HK

theme: smartblue highlight: agate


持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第2天,點擊查看活動詳情

前言

首先在文章開始之前先拋出幾個問題,讓我們帶着疑問往下走:

什麼是窗口控制?

在Android手機中狀態欄,導航欄,輸入法等這些與app無關,但是需要配合app一起使用的窗口部件。

之前我們都是如何管理窗口的?

在window上添加各種flag,有些flag只適應於指定的版本,而某些flag在高版本不能生效,清除flag也相對麻煩。

WindowInsetsController 又能解決什麼問題?

WindowInsetsController 的推出是來取代之前複雜麻煩的窗口控制,之前添加各種Flag不容易理解,而使用Api的方式來管理窗口,更加的語義化,更加的方便理解,可以説看到Api方法就知道是什麼意思,使用起來倒是很方便。

WindowInsetsController 就真的沒有兼容性問題嗎?

雖然flag這不好那不好,那我們直接用 WindowInsetsController 就可以了嗎?可是 WindowInsetsController 需要Android 11 (R) API 30 才能使用。雖然谷歌又推出了 ViewCompat 的Api 向下兼容到5.0版本,但是5.0以下的版本怎麼辦?

可能現在的一些新應用都是5.0以上了,但是這個兼容到哪一個版本也並不是我們開發者説了算,萬一要兼容5.0一下怎麼辦?

就算我們的應用是支持5.0以上,那麼我們使用 WindowInsetsController 與 windowInsets 就可以了嗎?並不是!

就算是 WindowInsetsController 或它的兼容包 WindowInsetsControllerCompat 也並不是全部就能用的,也會有兼容性問題。部分設備不能用,部分版本不能用等等。

説了這麼多,到底如何使用?下面一起來看看吧!

一、WindowInsetsController 與 windowInsets 的使用

WindowInsetsController 能管理的東西不少,但是我們常用的就是狀態欄,導航欄,軟鍵盤的一些管理,下面我們就基於這幾點來看看到底如何控制

1.1 狀態欄

第一種方法: ```kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.decorView.setOnApplyWindowInsetsListener { view: View, windowInsets: WindowInsets ->

        //狀態欄
        val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())

        //狀態欄高度
        val statusBarHeight = Math.abs(statusBars.bottom - statusBars.top)

        windowInsets
    }
}

```

第二種方法 ```kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val windowInsets = window.decorView.rootWindowInsets //狀態欄 val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars()) //狀態欄高度 val statusBarHeight = Math.abs(statusBars.bottom - statusBars.top)

    YYLogUtils.w("statusBarHeight2:$statusBarHeight")
}

```

第三種方法 ```kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.decorView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(view: View?) { val windowInsets = window.decorView.rootWindowInsets //狀態欄 val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars()) //狀態欄高度 val statusBarHeight = Math.abs(statusBars.bottom - statusBars.top)

            YYLogUtils.w("statusBarHeight2:$statusBarHeight")
        }

        override fun onViewDetachedFromWindow(view: View?) {
        }
    })
}

```

第一種方法和第三種方法是使用監聽回調的方式獲取到狀態欄高度,第二種方式是使用同步的方式獲取狀態欄高度,但是第二種方式有坑,它無法在 onCreate 中使用,直接使用會空指針的。

為什麼?其實也能理解,onCreate 方法其實就是解析佈局添加布局,並沒有展示出來,所以我們第三種方式使用了監聽,當View已經 OnAttach 之後我們再調用方法才能使用。

1.2 導航欄

第一種方法: ```kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.decorView.setOnApplyWindowInsetsListener { view: View, windowInsets: WindowInsets ->

            //導航欄
            val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())

            //導航欄高度
            val navigationHeight = Math.abs(statusBars.bottom - statusBars.top)

            YYLogUtils.w("navigationHeight:$navigationHeight")

        windowInsets
    }
}

```

第二種方法 ```kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val windowInsets = window.decorView.rootWindowInsets //導航欄 val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())

            //導航欄高度
            val navigationHeight = Math.abs(statusBars.bottom - statusBars.top)

            YYLogUtils.w("navigationHeight:$navigationHeight")

    YYLogUtils.w("statusBarHeight2:$statusBarHeight")
}

```

第三種方法 ```kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.decorView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(view: View?) { val windowInsets = window.decorView.rootWindowInsets //導航欄 val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())

            //導航欄高度
            val navigationHeight = Math.abs(statusBars.bottom - statusBars.top)

            YYLogUtils.w("navigationHeight:$navigationHeight")
        }

        override fun onViewDetachedFromWindow(view: View?) {
        }
    })
}

```

其實導航欄和狀態欄是一樣樣的,這裏打印Log如下:

可以看到其實也更推薦大家使用第三種方式,因為它是在 onAttach 中調用,而其他的方式需要在 onResume 之後調用,相對來説第三種方式更快一些。

1.3 軟鍵盤

同樣的我們可以操作軟鍵盤的打開,收起,還能監聽軟鍵盤彈起的動畫的Value,獲取當前的值,這個也是巨方便

```kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

    //打開鍵盤
    window?.insetsController?.show(WindowInsets.Type.ime())

// mBinding.llRoot.windowInsetsController?.show(WindowInsets.Type.ime())

    window.decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {

        override fun onProgress(insets: WindowInsets, runningAnimations: MutableList<WindowInsetsAnimation>): WindowInsets {

            val isVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
            val keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

            //當前是否展示
            YYLogUtils.w("isVisible = $isVisible")
            //當前的高度進度回調
            YYLogUtils.w("keyboardHeight = $keyboardHeight")

            return insets
        }
    })

}

```

我們可以通過 window?.insetsController 或者 window.decorView.windowInsetsController? 來獲取 WindowInsetsController 對象,通過 Controller 對象我們就能操作軟鍵盤了。

打印Log如下:

關閉軟鍵盤:

打開軟鍵盤:

1.4 其他

除了軟鍵盤的操作,我們還能進行其他的操作 ```kotlin window?.insetsController?.apply {

    show(WindowInsetsCompat.Type.ime())

    show(WindowInsetsCompat.Type.statusBars())

    show(WindowInsetsCompat.Type.navigationBars())

    show(WindowInsetsCompat.Type.systemBars())

}

```

不過都不是太常用。

除此之外我們還能設置狀態欄與導航欄的文本圖標顏色 ```kotlin window?.insetsController?.apply {

   setAppearanceLightNavigationBars(true)
   setAppearanceLightStatusBars(false)
}

```

不過也並不好用,內部有兼容性問題。

二、兼容庫 WindowInsetsControllerCompat 的使用

為了兼容低版本的Android,我們可以使用 implementation 'androidx.core:core:1.5.0' 以上的版本,內部即可使用 WindowInsetsControllerCompat 兼容庫,最多可以支持到5.0以上版本。

這裏我使用的是 implementation 'androidx.core:core:1.6.0'版本作為示例。

2.1 狀態欄

我們對於前面的版本,同樣的我們使用三種方式來獲取

方式一: ```java ViewCompat.setOnApplyWindowInsetsListener(view, new OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {

        Insets statusInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars());

        int top = statusInsets.top;
        int bottom = statusInsets.bottom;
        int height = Math.abs(bottom - top);

        return insets;
    }
});

```

方式二: java WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view); assert windowInsets != null; int top = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top; int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).bottom; int height = Math.abs(bottom - top);

方式三: ```java

view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {

            WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
             assert windowInsets != null;
            int top = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
            int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).bottom;
            int height = Math.abs(bottom - top);

            }

        @Override
        public void onViewDetachedFromWindow(View v) {
        }
    });

```

和R的版本一致,我更推薦使用第三種方式,當View已經 OnAttach 之後我們再調用方法,更快捷一點。

2.2 導航欄

方式一: ```java ViewCompat.setOnApplyWindowInsetsListener(view, new OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {

        Insets navInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars());

        int top = navInsets.top;
        int bottom = navInsets.bottom;
        int height = Math.abs(bottom - top);

        return insets;
    }
});

```

方式二: java WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view); assert windowInsets != null; int top = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).top; int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; int height = Math.abs(bottom - top);

方式三: ```java

view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
        @Override
        public void onViewAttachedToWindow(View v) {

            WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
             assert windowInsets != null;
            int top = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).top;
            int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
            int height = Math.abs(bottom - top);

            }

        @Override
        public void onViewDetachedFromWindow(View v) {
        }
    });

```

和R版本的一致,這樣即可正確的獲取到底部導航欄的高度

2.3 軟鍵盤

操作軟鍵盤的方式和R的版本差不多,只是調用的類變成了兼容類。

```kotlin ViewCompat.setWindowInsetsAnimationCallback(window.decorView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat {

            val isVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
            val keyboardHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

            //當前是否展示
            YYLogUtils.w("isVisible = $isVisible")
            //當前的高度進度回調
            YYLogUtils.w("keyboardHeight = $keyboardHeight")

            return insets
        }
    })

    ViewCompat.getWindowInsetsController(findViewById(android.R.id.content))?.apply {

        show(WindowInsetsCompat.Type.ime())

    }

```

這樣的兼容類,其實並沒有完全兼容,低版本的部分手機還是拿不到進度。

那麼我們可以在兼容類上再做一個版本的兼容

```kotlin

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

activity.window.decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
    override fun onProgress(insets: WindowInsets, animations: MutableList<WindowInsetsAnimation>): WindowInsets {

        val imeHeight = insets.getInsets(WindowInsets.Type.ime()).bottom

         listener.onKeyboardHeightChanged(imeHeight)

        return insets
    }
})

} else { ViewCompat.setOnApplyWindowInsetsListener(activity.window.decorView) { _, insets ->

    val posBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

    listener.onKeyboardHeightChanged(posBottom)

    insets
}

} ```

無賴,兼容類的軟鍵盤監聽效果並不好,只能使用以前的方式。

打印的Log如下:

2.4 其他

同樣的我們可以使用兼容類來操作狀態欄,導航欄,軟鍵盤等

```java View decorView = activity.findViewById(android.R.id.content); WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(decorView);

if (controller != null) {

    controller.show(WindowInsetsCompat.Type.navigationBars());

    controller.show(WindowInsetsCompat.Type.statusBars());

    controller.show(WindowInsetsCompat.Type.ime());    
}

```

注意坑點,如果使用的是Activity對象,這裏推薦使用 findViewById(android.R.id.content) 的方式來獲取View來操作,如果是通過window.decorView 來獲取 Controller 有可能為null。

控制導航欄,狀態欄的文本圖標顏色

``` WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(activity.findViewById(android.R.id.content)); if (controller != null) {

  controller.setAppearanceLightNavigationBars(false);

  controller.setAppearanceLightStatusBars(false);

}

```

注意坑點,看起來很美好,其實底部導航欄只有版本R以上才能控制,而頂部狀態欄的顏色控制則有很大的兼容性問題,幾乎不可用,我目前測試過的機型只有一款能生效。

三、實戰中兼容庫的兼容問題

在應用的開發中我們可以用 WindowInsetsControllerCompat 嗎?它能解決我們那些痛點呢?

當然可以用,在狀態欄高度,導航欄高度,判斷狀態欄導航欄是否顯示,監聽軟鍵盤的高度等一系列場景中確實能起到很好的作用。

為什麼要用 WindowInsetsControllerCompat ?

看之前的狀態欄高度,導航欄高度獲取,都是監聽的方式獲取啊,如果想使用我還需要加個回調才行,這裏就引入一個問題,一定要異步使用嗎?使用同步行不行?

博主,你這個太複雜了,我們之前的方式都是直接一個靜態方法就行了。

```java

/**
 * 老的方法獲取狀態欄高度
 */
private static int getStatusBarHeight(Context context) {
    int result = 0;
    int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = context.getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}

/**
 * 老的方法獲取導航欄的高度
 */
private static int getNavigationBarHeight(Context context) {
    int result = 0;
    int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = context.getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}

```

相信大家包括我都是這麼用的,確實簡單好用有快速又便捷,搞得這些監聽啊回調啊有個diao用?

但是但是,這些值只是預設的值,部分手機廠商會修改不使用這些值,而我們使用 WindowInsets 的方式來獲取的話,是其真正展示的值。

例如狀態欄的高度,早前一些劉海屏的手機,如果劉海做的比較大,比較高,狀態欄的高度都顯示不下,那麼就會加大狀態欄高度,那麼使用預設值就會有問題,顯得比較小。

再比如現在流行的全面屏手機,全面屏手勢,由於要兼容各種操作模式,底部的導航欄高度就完全不是預設值,如果還是用老方法就會踩大坑了。

如下圖,非常典型的例子,真正的導航欄是黑色,使用老方法獲取到的導航欄高度為深灰色。

再比如判斷導航欄是否存在,因為部分手機可以手動隱藏導航欄,還能在設置中動態改變交互模式,全面屏手勢,底部三大金剛鍵等。

大家使用的老的方式,大概都是這樣判斷:

```java /* * 老方法,並不好用 / public static boolean isNavBarVisible(Context context) { boolean isVisible = false; if (!(context instanceof Activity)) { return false; } Activity activity = (Activity) context; Window window = activity.getWindow(); ViewGroup decorView = (ViewGroup) window.getDecorView(); for (int i = 0, count = decorView.getChildCount(); i < count; i++) { final View child = decorView.getChildAt(i); final int id = child.getId(); if (id != View.NO_ID) { String resourceEntryName = context.getResources().getResourceEntryName(id); if ("navigationBarBackground".equals(resourceEntryName) && child.getVisibility() == View.VISIBLE) { isVisible = true; break; } } } if (isVisible) { // 對於三星手機,android10以下做單獨的判斷 if (isSamsung() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { try { return Settings.Global.getInt(activity.getContentResolver(), "navigationbar_hide_bar_enabled") == 0; } catch (Exception ignore) { } }

        int visibility = decorView.getSystemUiVisibility();
        isVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
    }

    return isVisible;
}

private static final String[] ROM_SAMSUNG = {"samsung"};

private static boolean isSamsung() {
    final String brand = getBrand();
    final String manufacturer = getManufacturer();
    return isRightRom(brand, manufacturer, ROM_SAMSUNG);
}

private static String getBrand() {
    try {
        String brand = Build.BRAND;
        if (!TextUtils.isEmpty(brand)) {
            return brand.toLowerCase();
        }
    } catch (Throwable ignore) {/**/}
    return "UNKNOWN";

}

private static String getManufacturer() {
    try {
        String manufacturer = Build.MANUFACTURER;
        if (!TextUtils.isEmpty(manufacturer)) {
            return manufacturer.toLowerCase();
        }
    } catch (Throwable ignore) {/**/}
    return "UNKNOWN";
}

private static boolean isRightRom(final String brand, final String manufacturer, final String... names) {
    for (String name : names) {
        if (brand.contains(name) || manufacturer.contains(name)) {
            return true;
        }
    }
    return false;
}

```

核心思路是直接遍歷 decorView 找到導航欄的控件,去判斷它是否隱藏還是顯示。。。

其實不説全面屏手機了,就是我的老華為 7.0系統的手機都判斷的不準確,巨坑!

比如,全面屏手機的導航欄判斷:

看到我全面屏手勢的小橫槓槓的了嗎?我明明沒有底部導航欄了,居然判斷我存在導航欄,還給一個完全不合理的狀態欄高度。

我醉了,真的是夠了!

而以上方法都是可以通過 WindowInsets 來解決的,也就是為什麼推薦部分場景下的一些效果還是使用 WindowInsets 來做為好。

那麼我們真的在實戰中使用了 WindowInsetsControllerCompat 就完美了嗎?就沒坑了嗎?

no no no, 答案是否定的。你根本不知道會發生什麼兼容性的問題。(兼容性可用説是我們安卓人的一生之敵)

WindowInsetsController 的兼容性問題

我們知道 WindowInsetsController 是安卓11以上用的,而 WindowInsetsControllerCompat 是安卓5以上可用的兼容包,那麼 WindowInsetsControllerCompat 的兼容包就沒有兼容性問題了嗎?一樣有!

例如一些 WindowInsetsControllerCompat 的獲取方式,設置狀態欄文本圖標的顏色方式,設置導航欄的圖標顏色方式。設置狀態欄導航欄的背景顏色等。

如果 WindowInsetsController / WindowInsets的方式在某些效果上並沒有那麼好用,那麼我們是不是還是要用flag的方式來實現這些效果,在一些兼容性好的方式上,那麼我們就可以用 WindowInsetsController / WindowInsets的方式的方式來實現,這樣是不是就能相對完美的實現我們想要的效果了。

所以我封裝了這樣的工具類。

四、推薦的工具類

此工具類5.0以上可用,記錄了一些狀態欄與導航欄操作的常用的方法。

```java

public class StatusBarHostUtils {

// =======================  StatusBar begin ↓ =========================

/**
 * 5.0以上設置沉浸式狀態
 */
public static void immersiveStatusBar(Activity activity) {
    //方式一
    //false 表示沉浸,true表示不沉浸

// WindowCompat.setDecorFitsSystemWindows(activity.getWindow(), false);

    //方式二:添加Flag,兩種方式都可以,都是5.0以上使用
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = activity.getWindow();
        View decorView = window.getDecorView();
        decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        window.setStatusBarColor(Color.TRANSPARENT);
    }
}

/**
 * 設置當前頁面的狀態欄顏色,使用宿主方案一般不用這個修改顏色,只是用於沉浸式之後修改狀態欄顏色為透明
 */
public static void setStatusBarColor(Activity activity, int statusBarColor) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = activity.getWindow();
        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        window.setStatusBarColor(statusBarColor);
    }
}

/**
 * 6.0版本及以上可以設置黑色的狀態欄文本
 *
 * @param activity
 * @param dark     是否需要黑色文本
 */
public static void setStatusBarDarkFont(Activity activity, boolean dark) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        Window window = activity.getWindow();
        View decorView = window.getDecorView();
        if (dark) {
            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        } else {
            decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        }
    }

}

/**
 * 老的方法獲取狀態欄高度
 */
private static int getStatusBarHeight(Context context) {
    int result = 0;
    int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = context.getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}

/**
 * 新方法獲取狀態欄高度
 */
public static void getStatusBarHeight(Activity activity, HeightValueCallback callback) {
    getStatusBarHeight(activity.findViewById(android.R.id.content), callback);
}

/**
 * 新方法獲取狀態欄高度
 */
public static void getStatusBarHeight(View view, HeightValueCallback callback) {

    boolean attachedToWindow = view.isAttachedToWindow();

    if (attachedToWindow) {

        WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view);
        assert windowInsets != null;
        int top = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
        int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).bottom;
        int height = Math.abs(bottom - top);
        if (height > 0) {
            callback.onHeight(height);
        } else {
            callback.onHeight(getStatusBarHeight(view.getContext()));
        }

    } else {

        view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View v) {

                WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
                assert windowInsets != null;
                int top = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
                int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).bottom;
                int height = Math.abs(bottom - top);
                if (height > 0) {
                    callback.onHeight(height);
                } else {
                    callback.onHeight(getStatusBarHeight(view.getContext()));
                }
            }

            @Override
            public void onViewDetachedFromWindow(View v) {
            }
        });
    }
}

// =======================  NavigationBar begin ↓ =========================

/**
 * 5.0以上-設置NavigationBar底部導航欄的沉浸式
 */
public static void immersiveNavigationBar(Activity activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = activity.getWindow();
        View decorView = window.getDecorView();
        decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);

        window.setNavigationBarColor(Color.TRANSPARENT);
    }
}

/**
 * 設置底部導航欄的顏色
 */
public static void setNavigationBarColor(Activity activity, int navigationBarColor) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = activity.getWindow();
        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        window.setNavigationBarColor(navigationBarColor);
    }
}

/**
 * 底部導航欄的Icon顏色白色和灰色切換,高版本系統才會生效
 */
public static void setNavigationBarDrak(Activity activity, boolean isDarkFont) {
    WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(activity.findViewById(android.R.id.content));
    if (controller != null) {
        if (!isDarkFont) {
            controller.setAppearanceLightNavigationBars(false);
        } else {
            controller.setAppearanceLightNavigationBars(true);
        }
    }
}

/**
 * 老的方法獲取導航欄的高度
 */
private static int getNavigationBarHeight(Context context) {
    int result = 0;
    int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = context.getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}

/**
 * 獲取底部導航欄的高度
 */
public static void getNavigationBarHeight(Activity activity, HeightValueCallback callback) {
    getNavigationBarHeight(activity.findViewById(android.R.id.content), callback);
}

/**
 * 獲取底部導航欄的高度
 */
public static void getNavigationBarHeight(View view, HeightValueCallback callback) {

    boolean attachedToWindow = view.isAttachedToWindow();

    if (attachedToWindow) {

        WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(view);
        assert windowInsets != null;
        int top = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).top;
        int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
        int height = Math.abs(bottom - top);
        if (height > 0) {
            callback.onHeight(height);
        } else {
            callback.onHeight(getNavigationBarHeight(view.getContext()));
        }

    } else {

        view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View v) {

                WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);
                assert windowInsets != null;
                int top = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).top;
                int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
                int height = Math.abs(bottom - top);
                if (height > 0) {
                    callback.onHeight(height);
                } else {
                    callback.onHeight(getNavigationBarHeight(view.getContext()));
                }
            }

            @Override
            public void onViewDetachedFromWindow(View v) {
            }
        });
    }
}

// =======================  NavigationBar StatusBar Hide Show begin ↓ =========================

/**
 * 顯示隱藏底部導航欄(注意不是沉浸式效果)
 */
public static void showHideNavigationBar(Activity activity, boolean isShow) {

    View decorView = activity.findViewById(android.R.id.content);
    WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(decorView);

    if (controller != null) {
        if (isShow) {
            controller.show(WindowInsetsCompat.Type.navigationBars());
            controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH);
        } else {
            controller.hide(WindowInsetsCompat.Type.navigationBars());
            controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
        }
    }
}

/**
 * 顯示隱藏頂部的狀態欄(注意不是沉浸式效果)
 */
public static void showHideStatusBar(Activity activity, boolean isShow) {

    View decorView = activity.findViewById(android.R.id.content);
    WindowInsetsControllerCompat controller = ViewCompat.getWindowInsetsController(decorView);

    if (controller != null) {
        if (isShow) {
            controller.show(WindowInsetsCompat.Type.statusBars());
        } else {
            controller.hide(WindowInsetsCompat.Type.statusBars());
        }
    }

}

/**
 * 當前是否顯示了底部導航欄
 */
public static void hasNavigationBars(Activity activity, BooleanValueCallback callback) {

    View decorView = activity.findViewById(android.R.id.content);
    boolean attachedToWindow = decorView.isAttachedToWindow();

    if (attachedToWindow) {

        WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(decorView);

        if (windowInsets != null) {

            boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
                    windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;

            callback.onBoolean(hasNavigationBar);
        }

    } else {

        decorView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View v) {

                WindowInsetsCompat windowInsets = ViewCompat.getRootWindowInsets(v);

                if (windowInsets != null) {

                    boolean hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
                            windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0;

                    callback.onBoolean(hasNavigationBar);
                }
            }

            @Override
            public void onViewDetachedFromWindow(View v) {
            }
        });
    }
}

}

```

關於狀態欄的沉浸式兩種方式都可以,而導航欄的沉浸式使用的Flag,修改狀態欄與導航欄的背景顏色使用flag,修改狀態欄文本顏色使用flag,修改導航欄的圖片顏色使用的 controller,獲取導航欄狀態欄的高度使用的 controller ,判斷導航欄是否存在使用的 controller。

一些效果如圖:

總結

由於使用了 WindowInsetsController與其兼容庫,所以我們定義的工具類在5.0版本以上。

如果使用flag的方式,那麼我們可以兼容到更低的版本,這一點還請知悉。

在5.0版本以上使用工具類,我們有些兼容性不好的使用的是flag方案,而有些效果比較好的我們使用的是 indowInsetsController 方案。

此方案並非什麼權威方案,只是我個人在開發過程中踩坑踩出來的,對我個人來説相對完善的一個方案,在實戰開發中我個人覺得還算能用。

當然由於各種原因受限,個人水平也有限,難免有閉門造車的情況,如果你有更好的方案或者覺得有錯漏的地方,還望指出來大家一起交流學習進步。

後期我也會針對本文進行一些擴展,會出一些相關的細節文章與一些效果的實現。

好了,本文的全部代碼與Demo都已經開源。有興趣可以看這裏。項目會持續更新,大家可以關注一下。

如果感覺本文對你有一點點的啟發,還望你能點贊支持一下,你的支持是我最大的動力。

Ok,這一期就此完結。

「其他文章」