持续创作,加速成长!这是我参与「掘金日新计划 · 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 状态栏
第一种方法:
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
}
}
第二种方法
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")
}
第三种方法
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 导航栏
第一种方法:
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
}
}
第二种方法
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")
}
第三种方法
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,获取当前的值,这个也是巨方便
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 其他
除了软键盘的操作,我们还能进行其他的操作
window?.insetsController?.apply {
show(WindowInsetsCompat.Type.ime())
show(WindowInsetsCompat.Type.statusBars())
show(WindowInsetsCompat.Type.navigationBars())
show(WindowInsetsCompat.Type.systemBars())
}
不过都不是太常用。
除此之外我们还能设置状态栏与导航栏的文本图标颜色
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 状态栏
我们对于前面的版本,同样的我们使用三种方式来获取
方式一:
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;
}
});
方式二:
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);
方式三:
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 导航栏
方式一:
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;
}
});
方式二:
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);
方式三:
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的版本差不多,只是调用的类变成了兼容类。
ViewCompat.setWindowInsetsAnimationCallback(window.decorView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): 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())
}
这样的兼容类,其实并没有完全兼容,低版本的部分手机还是拿不到进度。
那么我们可以在兼容类上再做一个版本的兼容
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 其他
同样的我们可以使用兼容类来操作状态栏,导航栏,软键盘等
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 ?
看之前的状态栏高度,导航栏高度获取,都是监听的方式获取啊,如果想使用我还需要加个回调才行,这里就引入一个问题,一定要异步使用吗?使用同步行不行?
博主,你这个太复杂了,我们之前的方式都是直接一个静态方法就行了。
/**
* 老的方法获取状态栏高度
*/
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 的方式来获取的话,是其真正展示的值。
例如状态栏的高度,早前一些刘海屏的手机,如果刘海做的比较大,比较高,状态栏的高度都显示不下,那么就会加大状态栏高度,那么使用预设值就会有问题,显得比较小。
再比如现在流行的全面屏手机,全面屏手势,由于要兼容各种操作模式,底部的导航栏高度就完全不是预设值,如果还是用老方法就会踩大坑了。
如下图,非常典型的例子,真正的导航栏是黑色,使用老方法获取到的导航栏高度为深灰色。
再比如判断导航栏是否存在,因为部分手机可以手动隐藏导航栏,还能在设置中动态改变交互模式,全面屏手势,底部三大金刚键等。
大家使用的老的方式,大概都是这样判断:
/**
* 老方法,并不好用
*/
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以上可用,记录了一些状态栏与导航栏操作的常用的方法。
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,这一期就此完结。