Android 启动页面gif动态图添加(gif图启动一次)

从Android 4.4 开始,Android 支持了状态栏和导航栏的透明效果,并在 Android 5.0 上加强了这种效果,但是实现方法却和 Android 4.4 完全不同,之后在 Android 6.0、Android 8.0 以及 Android 10.0 上都增加了一些新的特性,使得在不同 Android 版本上,要实现状态栏和导航栏同样的效果异常困难,为此,我很久以前写了一个库 UltimateBar。但是随着时间的推移以及本人的成长,我越发觉得这个库设计的不好,存在太多不合理的地方,有较多的bug 无法解决,后来我决定设计一个更完美更强大更好用的库,于是便有了今天的主角。


UltimateBarX

https://github.com/Zackratos/UltimateBarX

关于命名Ultimate

翻译过来是「终极」的意思,在设计第一个库的时候,就命名为了「UltimateBar」,现在命名为「UltimateBarX」,是借鉴了 Google 爸爸的「AndroidX」,Google 也是嫌弃 support 库太乱,弄了 AndroidX 来统一,这个命名倒是有异曲同工之妙。

/   实现方案   /

首先说说什么是「沉浸式状态栏」,什么是「透明状态栏」,关于这一点,郭神在很久以前有一篇文章已经说的很清楚Android状态栏微技巧,带你真正理解沉浸式模式

https://blog.csdn.net/guolin_blog/article/details/51763825

我简单总结一下,如下图所示,很多人称这种效果为「沉浸式」,其实这并不是真正的沉浸式,而只能叫「透明」,真正的沉浸式是状态栏完全不可见,这个暂不讨论,先说说这里的「透明」,可以看到,它效果就是状态栏和导航栏本身是透明的,然后布局内容侵入到状态栏和导航栏内部


那么这种效果要怎么实现呢,在Android 4.4 上是这样的

private fun test() { window ?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window ?. addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)}


Android 5.0 以上则是

private fun test() { val flag = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE) window?.decorView?.systemUiVisibility = flag window?.statusBarColor = Color.TRANSPARENT window?.navigationBarColor = Color.TRANSPARENT}

再看一下另外一种常见的效果:


用语言描述一下就是,状态栏和导航栏的颜色都是红色,状态栏下面的 Toolbar 的颜色也是红色,并且布局内容没有侵入到状态栏和导航栏内部。

这种效果在 Android 5.0 以上非常好实现,两行代码就可以.

private fun test() {    window?.statusBarColor = Color.RED    window?.navigationBarColor = Color.RED}


但是在 Android 4.4 上就比较麻烦了,因为 Android 4.4 是无法直接给状态栏和导航栏设置颜色的,要实现这种效果,比较常见的解决方案就是在 Activity 的 DecroView 中,在状态栏和导航栏的位置分别添加一个 View,根据需要给 View 设置背景色,然后让布局内容不侵入到状态栏和导航栏内部,就可以造成这种视觉效果了,代码如下:

private fun test() { window ?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window ?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) val decorView = window ?.decorView as FrameLayout ? val contentView = decorView ?.findViewById<ViewGroup> (android.R.id.content) ?. getChildAt(0) contentView ?.fitsSystemWindows = true val statusBarView = View(this) statusBarView.setBackgroundColor(Color.RED) val statusBarLP = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight()) statusBarLP.gravity = Gravity.TOP decorView ?.addView(statusBarView, statusBarLP) val navigationBarView = View(this) navigationBarView.setBackgroundColor(Color.RED) val navigationBarLP = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getNavigationBarHeight()) navigationBarLP.gravity = Gravity.BOTTOM decorView ?.addView(navigationBarView, navigationBarLP)}

上面一直有提到布局内容侵入到状态栏和导航栏,那么怎么设置让布局内容侵入或者不侵入呢,其实很简单,调用布局根 View 的 setFitsSystemWindows 方法即可(true 表示不侵入,默认 false),但是这个方法有个缺陷,它对状态栏和导航栏是同时生效的,也就是说,要么都侵入,要么都不侵入,那如果现在的需求是状态栏侵入,但导航栏不侵入该怎么办呢,显然就实现不了了。

为了解决这个问题,我在设计 UltimateBarX 的时候,就用了很极端的方法,先让状态栏和导航栏都侵入,当遇到不需要侵入的情况时,给 DecroView 增加 paddingTop 和 paddingBottom 就可以了。

到这里,思路已经很明显了,不管是 Android 4.4 还是 Android 5.0 以上,都给状态栏和导航栏设置透明效果并侵入,获取状态栏的高度为 statusBarHeight, 然后如果需要状态栏不透明,就在状态栏的位置给 DecroView 增加一个有背景色高度为 statusBarHeight 的 View,姑且称它为 StatusBarView,如果状态栏需要不侵入,就设置 DecroView 的 paddingTop 为 statusBarHeight。

另外需要提一点的是,由于 StatusBarView 也是 Decorview 的子 View,而 DecorView 设置了 paddingTop,这时候 StatusBarView 的实际位置会跑到状态栏的下方,所以需要给它设置 marginTop 为 -statusBarHeight,同时需要调用 DecroView 的 setClipToPadding(false) 方法,保证 StatusBarView 可见,导航栏的设置方法也是类似,这样就可以实现状态栏和导航栏完全分开设置,不再耦合了,最终代码如下

private fun test( statusBarFitWindow:Boolean, @ColorInt statusBarColor:Int, navigationBarFitWindow:Boolean, @ColorInt navigationBarColor:Int) { transparentBar() setStatusBarView(statusBarFitWindow, statusBarColor) setNavigationBarView(navigationBarFitWindow, navigationBarColor)}private fun transparentBar() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val flag = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE) window ?.decorView ?.systemUiVisibility = flag window ?.statusBarColor = Color.TRANSPARENT window ?.navigationBarColor = Color.TRANSPARENT } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { window ?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window ?.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) } val decorView = window ?.decorView as FrameLayout ? val contentView = decorView ?.findViewById<ViewGroup> (android.R.id.content) ?. getChildAt(0) contentView ?.fitsSystemWindows = false decorView ?.clipToPadding = false}private fun setStatusBarView(statusBarFitWindow:Boolean, @ColorInt statusBarColor:Int) { val decorView = window ?.decorView as FrameLayout ? var statusBarView = decorView ?.findViewWithTag<View> ("status_bar") if (statusBarView == null) { statusBarView = View(this) statusBarView.tag = "status_bar" } statusBarView.setBackgroundColor(statusBarColor) val statusBarLP = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight()) if (statusBarFitWindow) { statusBarLP.topMargin = -getStatusBarHeight() decorView ?.setPadding(0, getStatusBarHeight(), 0, decorView.paddingBottom) } else { statusBarLP.topMargin = 0 decorView ?.setPadding(0, 0, 0, decorView.paddingBottom) } statusBarLP.gravity = Gravity.TOP decorView ?.addView(statusBarView, statusBarLP)}private fun setNavigationBarView(navigationBarFitWindow:Boolean, @ColorInt navigationBarColor:Int) { val decorView = window ?.decorView as FrameLayout ? var navigationBarView = decorView ?.findViewWithTag<View> ("navigation_bar") if (navigationBarView == null) { navigationBarView = View(this) navigationBarView.tag = "navigation_bar" } navigationBarView.setBackgroundColor(navigationBarColor) val navigationBarLP = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getNavigationBarHeight()) if (navigationBarFitWindow) { navigationBarLP.bottomMargin = -getNavigationBarHeight() decorView ?.setPadding(0, decorView.paddingTop, 0, getNavigationBarHeight()) } else { navigationBarLP.bottomMargin = 0 decorView ?.setPadding(0, decorView.paddingTop, 0, 0) } navigationBarLP.gravity = Gravity.BOTTOM decorView ?.addView(navigationBarView, navigationBarLP)}

到此,UltimateBarX 的最基本的功能已经实现了。

light 模式

在 Android 6.0 的以上,状态栏支持字体变灰色,Android 8.0 以上,导航栏支持导航按钮变灰色,效果如下所示:


我们可以称它为「light 模式」,调用 DecroView 的 setSystemUiVisibility(int visibility) 方法给它设置一些 flag 即可实现,代码如下:

private fun test() { val flag = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) window ?.decorView ?.systemUiVisibility = flag window ?.statusBarColor = Color.TRANSPARENT window ?.navigationBarColor = Color.TRANSPARENT}

而前面的状态栏导航栏透明效果也是依赖这些 flag,并且状态栏和导航栏的 light 模式也是耦合在一起的,假设一种场景,如果一开始使用 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 给状态栏设置了 light 模式,然后需要设置导航栏的 light 模式,需要重新调用 setSystemUiVisibility(int visibility) 方法并设置 SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,这时候按理说状态栏的灰色字体应该保持不变,所以要同时加上 SYSTEM_UI_FLAG_LIGHT_STATUS_BAR,设置 flag 的代码如下:

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)private fun systemUiFlag(statusBarLight:Boolean, navigationBarLight:Boolean):Int{ var flag = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE) when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ->{ if (statusBarLight) flag = flag or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR if (navigationBarLight) flag = flag or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR } Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ->{ if (statusBarLight) flag = flag or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR }} return flag}

但是在 Activity 的外部,并不知道上次设置了状态栏的 light 模式,因此,在每一次设置完状态栏或者导航栏的 light 模式时,都需要把它们的 light 状态记下来。

那么这个状态记在哪里呢?UltimateBarX 中使用了单例类来保存,为了可以记录多个 Activity 的状态,在单例类中创建了两个 Map,分别用于保存状态栏和导航栏的 light 模式状态,Map 的 key 就是 Activity 对象,那么问题来了,我们知道,单例的生命周期是贯穿整个应用的生命周期的,在单例中持有 Activity 对象会导致 Activity 不能被回收,造成内存泄漏,所以必须要 Activity 关闭的时候把 Map 中的对应数据移除掉,那么怎么监听 Activity 的关闭呢?

比较常规的一个方法就是在 Activity 中添加一个看不见的 Fragment,只要监听 Fragment 的 onDestroy 方法即可,大名鼎鼎的图片加载库「Glide」就是用的这种套路,不过 Google 在「JetPack」中增加了很多好用的组件,Lifecycle 就是其中一种,通过Lifecycle 就可以非常方便的监听 Activity 的各个生命周期方法,而不需要繁琐的添加 Fragment 了,UltimateBarX 就是采用 Lifecycle 来监听的,代码如下:

internal class UltimateBarXObserver: LifecycleObserver {    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)    fun onDestroy(owner: LifecycleOwner) {        UltimateBarXManager.getInstance().removeAllData(owner)    }}private fun test() {    addObserver(UltimateBarXObserver())}

适配全面屏导航栏

在非全面屏手机上,如果不对导航栏做任何设置,那么它的背景就是一个黑条,上面有三个白色的导航按钮,如下图所示:


前面提到,UltimateBarX 的基本原理就是先让状态栏和导航栏全部透明并侵入,然后再添加 View,设置 padding 和 margin

如果现在只需要设置状态栏而不设置导航栏,那么根据前面讲的原理,这时候导航栏也要透明并且侵入的,但是要让视觉效果上没有被设置过,怎么办?

方法就是增加一种默认设置效果,在导航栏的位置上增加一个黑色背景的 View,并设置它的 marginBottom 和 DecroView 的 paddingBottom,但是全面屏的手机,它的导航栏默认是白色的,并且导航按钮是灰色的,如下图所示:


显然,对于全面屏的手机,就不能用这种默认的方法了,否则就会在设置状态栏的时候导致导航栏变黑色


这个问题该如何解决呢?首先,全面屏手机不好判断,另外,即使可以判断,也不见得所有的全面屏手机默认导航栏都是白色的,所以直接在设置默认效果的地方判断是不是全面屏并设置不同的效果显然不合理。

UltimateBarX 使用的方法是在第一次给 Activity 设置透明效果之前,先调用 Window 的 getNavigationBarColor 方法拿到当前 Activity 的导航栏颜色,并根据导航栏颜色判断是否是 light 模式,然后把导航栏的初始颜色和 light 模式状态也保存在单例中,后面如果需要给导航栏设置默认效果时,直接从单例里面取数据设置即可

private fun putOriginColor() { val navigationBarColor = window?.navigationBarColor ?: Color.TRANSPARENT originColorMap[this] = navigationBarColor val navConfig = getNavigationBarConfig(this) navConfig.light = calculateLight(navigationBarColor) putNavigationBarConfig(this, navConfig)}private fun calculateLight(@ColorInt color: Int) = color > (Color.BLACK + Color.WHITE / 2)

这样在设置状态栏的时候,在视觉效果上就不会对导航栏造成影响了


到此为止,UltimateBarX 在 Activity 中的功能已经基本实现了,接下来看看它在 Fragment 中的实现

  /   在Fragment中使用   /

其实我一开始是拒绝适配 Fragment 的,因为状态栏和导航栏本来就是 Activity 自带的属性,Fragment 中并没有状态栏和导航栏的概念,所谓的在 Fragment 中使用,其实只是让 Fragment 看起来有相同的视觉效果,要实现这种效果,可以先让其所在的 Activity 的透明状态栏和导航栏透明,然后在 Fragment 的根 View 的顶部和底部分别添加子 View,其实前面提到,UltimateBarX 的 Activity 的实现也是采用了同样的方法,虽然原理相同,但是 Fragment 中实现起来有更多的坑。

View的父布局

第一个坑就是 StatsBarView 和 NavigationBarView 的父布局,由于 Fragment 不像 Activity 那样布局的最外层有个 DecorView,所以把 StatsBarView 和 NavigationBarView 添加到哪个 View 中就成了一个难题。

一开始我的想法是添加到 Activity 中的 Fragment 所在的容器布局中,但是这显然不合理,首先,我们并不能确定 Fragment 的布局容器的 ViewGroup 的具体类型,所以无法用统一的方法把 StatsBarView 和 NavigationBarView 固定在布局的顶部和底部。

另外,如果使用这种方法,那就无法在在 google 新出的 ViewPager2 中使用,因为 ViewPager2 中的Fragment 是没有父布局的,最后,也是最重要的一点,就是这种方法本质还是在 Activity 中添加 StatsBarView 和 NavigationBarView,在切换 Fragment 会有状态栏延迟改变的情况,如下图所示


看起来非常奇怪,我们需要的是下图这种效果:


所以要把 StatsBarView 和 NavigationBarView 添加到 Fragment 本身的布局中,假设他们添加到 Fragment 的根布局中,这时候就要考虑根布局的类型了,如果它是一个 FrameLayout,那就跟在 Activity 中起来是一样的,没有难度,如果是 RelativeLayout 也很简单,分别设置跟父布局的顶部和底部对齐即可,代码如下:


推荐阅读更多精彩内容