缘起
我对 Android 的状态栏和导航栏一直有种情结,在我做 Android 开发之前,我就喜欢通过一些 Xposed 插件来让状态栏和导航栏变色或者透明,以消除那丑丑的两个黑条。
从 fitsSystemWindows 方法说起,View 里面有个方法,叫setFitsSystemWindows,这个方法有什么用呢,当给 Activity 设置全屏的时候,如果给 contentView 的最外层设置 setFitsSystemWindows(true),那么 contentView 就不会侵入状态栏和导航栏,否则,就会侵入。什么意思呢?举个例子,在 Android 5.0 以上,如果在 Activity 中做以下操作
int flag = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
getWindow().getDecorView().setSystemUiVisibility(flag);
getWindow().setStatusBarColor(Color.parseColor("#66000000"));
getWindow().setNavigationBarColor(Color.parseColor("#66000000"));
就会看到这样的效果
如果再在后面加上
findViewById(R.id.contentView).setFitsSystemWindows(true);
效果就会变成这样
以上两个图已经很形象的说明了侵入和非侵入的区别。现在来解释一下为什么状态栏和导航栏设置会耦合,从上面的两个图可以看出 setFitsSystemWindows(true)
方法是对状态栏和导航栏同时生效的,就是说要么都侵入,要么都不侵入,那么问题来了,如果我现在要求布局侵入到状态栏而不侵入到导航栏要怎么办呢,其实也是可以实现的,只需要在上面的代码中把 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
去掉,即
int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
getWindow().getDecorView().setSystemUiVisibility(flag);
getWindow().setStatusBarColor(Color.parseColor("#66000000"));
findViewById(R.id.contentView).setFitsSystemWindows(false);
这样就能达到只设置状态栏,而导航栏不受影响的效果,同样的,如果需要让布局只侵入导航栏而状态栏不受影响,只需要
int flag = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
getWindow().getDecorView().setSystemUiVisibility(flag);
getWindow().setNavigationBarColor(Color.parseColor("#66000000"));
findViewById(R.id.contentView).setFitsSystemWindows(false);
这样看来好像并没有什么问题,各种情况都能解决,但是,如果现在需求是这样的,首先刚进入 Activity 的时候,让布局只侵入到导航栏,然后当点击某个按钮的时候,导航栏保持被侵入不变,同时,让布局侵入到状态栏,这时候如果还用上面的方法,就会导致导航栏被还原成初始状态,如图所示
这是因为在多次调用 setSystemUiVisibility
方法时,只有最后一次才是有效的,前面的调用都会被最后一次覆盖掉,那要怎么解决这个问题呢,有种方法就是保存当前 Activity 的状态栏和导航栏的状态,当每次需要设置的时候,都根据保存的状态重新设置 setSystemUiVisibility 的参数,保证之前设置过的效果不受影响,但是这样操作起来太过繁琐,我选择了另一种很简单的方法。
UltimateBarX 的核心原理
其实方法很简单,当第一次设置状态栏或导航栏的时候,不管需要什么效果,都让布局侵入到状态栏和导航栏,然后根据要不要侵入来设置 decorView
的 topPadding
和 bottomPadding
,比如上面提到的需求,就可以在刚进入 Activity 的时候,就让布局同时侵入到状态栏和导航栏,然后给 decorView
设置一个状态栏高度的 topPadding
,看起来效果就是布局没有侵入到状态栏了,当点击按钮需要让状态栏被侵入的时候,只需再把 decorView
的 topPadding
设为0,而不用再管导航栏,也不用保存导航栏之前设置的状态了,简单粗暴。
UltimateBarX 的用法
UltimateBarX 的用法也很简单,这次我花了很长的时间思考怎样让方法调用起来更简单,同时日后维护起来更方便,首先在 build.gradle 中添加
dependencies {
implementation 'com.zackratos.ultimatebarx:ultimatebarx:0.1.1'
}
如果需要设置状态了,可以在 Activity 中
UltimateBarX.create(UltimateBarX.STATUS_BAR) // 设置状态栏
.fitWindow(true) // 布局是否侵入状态栏(true 不侵入,false 侵入)
.bgColor(Color.BLACK) // 状态栏背景颜色(色值)
.bgColorRes(R.color.deepSkyBlue) // 状态栏背景颜色(资源id)
.bgRes(R.drawable.bg_gradient) // 状态栏背景 drawable
.light(false) // light模式(状态栏字体灰色 Android 6.0 以上支持)
.apply(this);
方法非常简单,注释也写的很清楚了,这里有三个设置背景的方法,写一个就行了,多写也只有一个会生效,优先级 bgRes
> bgColor
> bgColorRes
,如果需要设置导航栏,在 create
里面传入 UltimateBarX.NAVIGATION_BAR
即可,其他不变,状态栏和导航栏完全独立设置,互不影响,做到了真正的解耦。
如果要实现上面所说的需求,只需要在刚进入 Activity 的时候
UltimateBarX.create(UltimateBarX.NAVIGATION_BAR)
.fitWindow(false)
.bgColor(Color.parseColor("#66000000"))
.apply(this);
设置布局侵入到导航栏,然后点击按钮,让布局侵入到状态栏,只需
UltimateBarX.create(UltimateBarX.STATUS_BAR)
.fitWindow(false)
.bgColor(Color.parseColor("#66000000"))
.apply(this);
关于 light 方法
这里面有个 light
方法,用来设置状态栏或者导航栏的 light 模式,当 light 模式为 true
时,状态栏的字体会变灰,对应的导航栏的按钮会变灰,这里有个问题,就是设置 light 模式也需要调用 decorView
的 setSystemUiVisibility
方法,这就意味着 light 模式是需要保存状态的,如果不保存,第一次设置状态栏为 light 模式,第二次再设置导航栏的时候,状态栏的 light 模式就会被清除,这个前面已经解释过了。
那如何保存状态呢,为了使侵入性更低,可以用一个单例对象来保存每个 Activity 的状态栏和导航栏的 light 状态,这样又会有一个问题,在 Activity 退出的时候,必须把当前 Activity 对象从单例里面移除,否则会造成内存泄漏,那么怎样监听 Activity 退出呢,常用的做法就是在 Activity 里面添加一个看不见的 Fragment,当 Fragment 的 onDestroy
方法被调用的时候,说明 Activity 的 onDestroy
被调用了,即 Activity 已退出。
不过现在已经不用这么麻烦了,Google 爸爸在 JetPack 组件里面,给我们提供了非常好用的工具「Lifecycle」,可以无侵入的监听 Activity 和 Fragment 的生命周期,这里就使用 Lifecycle 来实现的,非常方便,其实 Lifecycle 的实现原理也是在内部添加一个 Fragment 来监听的。
最后
原理和用法都讲的差不多了,最后再贴一遍 UltimateBarX 地址,希望大家多多关注,多多 star、fork,提 issues、提 pr,也欢迎跟我交流
https://github.com/Zackratos/UltimateBarX
作者:Zackratos
链接:https://juejin.im/post/5f03e8786fb9a07e5e58c623
关注我获取更多知识或者投稿