操作Android窗口的幾種方式?WindowInsets與其兼容庫的使用與踩坑
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
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,這一期就此完結。
- Android操作文件也太難了趴,File vs DocumentFile 以及 DocumentsProvider vs FileProvider 的異同
- findViewById不香嗎?為什麼要把簡單的問題複雜化?為什麼要用DataBinding?
- Android自定義View繪製進階-水波浪温度刻度表
- Android自定義ViewGroup佈局進階,完整的九宮格實現
- 記錄仿抖音的視頻播放並緩存預加載視頻的效果實現
- Kotlin對象的懶加載方式?by lazy 與 lateinit 的異同
- 定位都得集成第三方?Android原生定位服務LocationManager不行嗎?
- 還用第三方庫管理狀態欄嗎?Android關於狀態欄管理的幾種方案實現!
- 下載需要集成第三方?Android原生下載服務DownloadManager不行嗎?
- Android陰影實現的幾種方案-自定義圓角ViewGroup加入陰影效果
- 操作Android窗口的幾種方式?WindowInsets與其兼容庫的使用與踩坑
- Android軟鍵盤與佈局的協調-不同的效果與實現方案的探討
- ViewPager2:ViewPager都能自動嵌套滾動了,我不行?我麻了!該怎麼做?
- Android軟鍵盤的監聽與高度控制的幾種方案及常用效果
- 圓角升級啦,來手把手一起實現自定義ViewGroup的各種圓角與背景
- Android導航欄的處理-HostStatusLayout加入底部的導航欄適配
- 一次搞懂怎麼設置圓角圖片,ImageView的各種圓角設置
- 一看就會 Android框架DataBinding的使用與封裝
- 別濫用FileProvider了,Android中FileProvider的各種場景應用
- Android登錄攔截的場景-基於攔截器模式實現