Android 状态栏和导航栏的终极解决方案 最终版

·  阅读 5063

缘起

我对 Android 的状态栏和导航栏一直有种情结,在我做 Android 开发之前,我就喜欢通过一些 Xposed 插件来让状态栏和导航栏变色或者透明,以消除那丑丑的两个黑条。

做 Android 开发之后,我更是写了两篇文章(透明状态栏和导航栏的终极解决方案 Android 状态栏和导航栏的真终极解决方案 )来分析 Android 4.4 以上的状态栏和导航栏的各种效果的实现,还开源了一个库 UltimateBar

但是随着自己技术的成长,我越发觉得这个库设计的不好,存在很多缺陷,以至于到了维护不动的境地。比如,经常有人给我提这样的 issues:

这两个问题,其实是同一个问题,主要就是因为状态栏和导航栏的设置耦合到了一起,导致修改了状态栏,导航栏也会收到影响,当然,我不是故意要把他们耦合到一起的,只是要实现很多复杂的功能不得已而为之,不过现在我已经彻底解决这个问题了,这个后面会说到。

基于这些原因,我最近终于又开发了一个新的库 UltimateBarX,这个名字借鉴了 Google 爸爸的「AndroidX」,Google 之前的 Support 库太混乱了,为了统一,新开了一个「AndroidX」,我起这个名字,多少有点相似的意味。

接下拉就言归正传,看看这个库的用法以及它的实现原理。

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 的核心原理

其实方法很简单,当第一次设置状态栏或导航栏的时候,不管需要什么效果,都让布局侵入到状态栏和导航栏,然后根据要不要侵入来设置 decorViewtopPaddingbottomPadding,比如上面提到的需求,就可以在刚进入 Activity 的时候,就让布局同时侵入到状态栏和导航栏,然后给 decorView 设置一个状态栏高度的 topPadding,看起来效果就是布局没有侵入到状态栏了,当点击按钮需要让状态栏被侵入的时候,只需再把 decorViewtopPadding 设为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 模式也需要调用 decorViewsetSystemUiVisibility 方法,这就意味着 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,也欢迎跟我交流

github.com/Zackratos/U…

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改