Android 系统 Bar 沉浸式完美兼容方案(2)

window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)

window.navigationBarColor = Color.TRANSPARENT

但发现导航栏半透明背景依然无法去掉:

7bdc360643a2cb6dd88306e0bbf31562.png

问题三:亮色系统 bar 版本差异

对于大于等于 Android 6.0 版本的系统,如果背景是浅色的,可通过设置状态栏和导航栏文字颜色为深色,也就是导航栏和状态栏为浅色(只有 Android 8.0 及以上才支持导航栏文字颜色修改):

window.decorView.systemUiVisibility =

View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

window.decorView.systemUiVisibility =

window.decorView.systemUiVisibility or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0

效果如下:

3227d8427b37bca14f7d9e0e7f5c918c.png

Android 8.0 亮色状态栏

15ca6d0b5a6cf32888ecbf4c169593db.png

Android 8.0 亮色导航栏

但是在亮色系统 bar 基础上开启沉浸式后,在 8.0 至 9.0 系统中,导航栏深色导航 icon 不生效,而 10.0 以上版本能显示深色导航 icon:

9c7ef2429b1537110c26e93681b6a1f9.png

Android 8.0 亮色沉浸式亮色导航栏

4ff1a87f8d5fd6cf83ce97583199b422.png

Android 10.0 亮色沉浸式亮色导航栏

问题分析


问题一:沉浸式下无法设置背景色

查看源码发现设置状态栏和导航栏背景颜色时,是不能为沉浸式的:

5eefd3b1c06da1a5a33c28b49beeda8f.png

问题二:无法全透明导航栏

当设置导航栏为透明色(Color.TRANSPARENT)时,导航栏会变成半透明,当设置其他颜色,则是正常的,例如设置颜色为 0x700F7FFF,显示效果如下:

d366adfc240553ec6dbc0238a839b9d9.png

Android 10.0 沉浸式导航栏

为什么会出现这个情况呢,通过调试进入源码,发现 activity 的 onApplyThemeResource 方法中有一个逻辑:

// Get the primary color and update the TaskDescription for this activity

TypedArray a = theme.obtainStyledAttributes(

com.android.internal.R.styleable.ActivityTaskDescription);

if (mTaskDescription.getPrimaryColor() == 0) {

int colorPrimary = a.getColor(

com.android.internal.R.styleable.ActivityTaskDescription_colorPrimary, 0);

if (colorPrimary != 0 && Color.alpha(colorPrimary) == 0xFF) {

mTaskDescription.setPrimaryColor(colorPrimary);

}

}

也就是说如果设置的导航栏颜色为 0(纯透明)时,将会为其修改为内置的颜色:ActivityTaskDescription_colorPrimary,因此就会出现灰色蒙层效果。

问题三:亮色系统 bar 版本差异

通过查看源码发现,与设置状态栏和导航栏背景颜色类似,设置导航栏 icon 颜色也是不能为沉浸式:

14ad79371e92b35cfc92d3ff91138892.png

解决沉浸式兼容性问题


对于问题二无法全透明导航栏,由上述问题分析中的代码可以看出,当且仅当设置的导航栏颜色为纯透明时(0),才会置换为半透明的蒙层。那么,我们可以将纯透明这种情况修改颜色为 0x01000000,这样也能达到接近纯透明的效果:

65939505112f52fef3d4384e0a055e29.png

对于问题一,难以通过常规方式进行沉浸式下的系统 bar 背景颜色设置。而对于问题三,通过常规方式需要分别对各个版本进行适配,对于国内手机来说,适配难度更大。

为了解决兼容性问题,以及更好的管理状态栏和导航栏,我们是否能自己实现状态栏和导航栏的背景 View 呢?

通过 Layout Inspector 可以看出,导航栏和状态栏本质上也是一个 view:

134be87af323e8a4fe9288407b51ec7c.png

在 activity 创建的时候,会创建两个 view(navigationBarBackground 和 statusBarBackground),将其加到 decorView 中,从而可以控制状态栏的颜色。那么,是否能把系统的这两个 view 隐藏起来,替换成自定义的 view 呢?

因此,为了提高兼容性,以及更好的管理状态栏和导航栏,我们可以将系统的 navigationBarBackground 和 statusBarBackground 隐藏起来,替换成自定义的 view,而不再通过 FLAG_TRANSLUCENT_STATUSFLAG_TRANSLUCENT_NAVIGATION 来设置。

实现沉浸式状态栏

  1. 添加自定义的状态栏。通过创建一个 view ,让其高度等于状态栏的高度,并将其添加到 decorView 中:

View(window.context).apply {

id = R.id.status_bar_view

val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, statusHeight)

params.gravity = Gravity.TOP

layoutParams = params

(window.decorView as ViewGroup).addView(this)

}

  1. 隐藏系统的状态栏。由于 activity 在 onCreate 时,并没有创建状态栏的 view(statusBarBackground),因此无法直接将其隐藏。这里可以通过对 decorView 添加 OnHierarchyChangeListener 监听来捕获到 statusBarBackground:

(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {

override fun onChildViewAdded(parent: View?, child: View?) {

if (child?.id == android.R.id.statusBarBackground) {

child.scaleX = 0f

}

}

override fun onChildViewRemoved(parent: View?, child: View?) {

}

})

注意:这里将 child 的 scaleX 设为 0 即可将其隐藏起来,那么为什么不能设置 visibilityGONE 呢?这是因为后续在应用主题时(onApplyThemeResource),系统会将 visibility 又重新设置为 VISIBLE

隐藏之后,半透明的状态栏不显示,但是顶部会出现空白:

034ba175849ef707544f63cc04d4ddf7.png

通过 Layout Inspector 发现,decorView 的第一个元素(内容 view )会存在一个 padding:

f44e6bbc5da143fcc1549fc4cb3fada7.png

因此,可以通过设置 paddingTop 为 0 将其去除:

val view = (window.decorView as ViewGroup).getChildAt(0)

view.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->

if (view.paddingTop > 0) {

view.setPadding(0, 0, 0, view.paddingBottom)

val content = findViewById(android.R.id.content)

content.requestLayout()

}

}

注意:这里需要监听 view 的 layout 变化,否则只有一开始设置则后面又被修改了。

实现沉浸式导航栏

导航栏的自定义与状态栏类似,不过会存在一些差异。先创建一个自定义 view 将其添加到 decorView 中,然后把原来系统的 navigationBarBackground 隐藏:

window.decorView.findViewById(R.id.navigation_bar_view) ?: View(window.context).apply {

id = R.id.navigation_bar_view

val resourceId = resources.getIdentifier( navigation_bar_height ,  dimen ,  android )

val navigationBarHeight = if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0

val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, navigationBarHeight)

params.gravity = Gravity.BOTTOM

layoutParams = params

(window.decorView as ViewGroup).addView(this)

(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {

override fun onChildViewAdded(parent: View?, child: View?) {

if (child?.id == android.R.id.navigationBarBackground) {

child.scaleX = 0f

} else if (child?.id == android.R.id.statusBarBackground) {

child.scaleX = 0f

}

}

override fun onChildViewRemoved(parent: View?, child: View?) {

}

})

}

注意:这里 onChildViewAdded 方法中,因为只能设置一次 OnHierarchyChangeListener ,需要同时考虑状态栏和导航栏。

通过这个方式,能将导航栏替换为自定义的 view ,但是存在一个问题,由于 navigationBarHeight 是固定的,如果用户切换了导航栏的样式,再回到 app 时,导航栏的高度不会重新调整。为了让导航栏看的清楚,设置其颜色为 0x7F00FF7F:

86d7b26c48ced5170683f7f29df46ab9.gif

从图中可以看出,导航栏切换之后高度没有发生变化。为了解决这个问题,需要通过对 navigationBarBackground 设置 OnLayoutChangeListener 来监听导航栏高度的变化,并通过 liveData 关联到 view 中,代码实现如下:

val heightLiveData = MutableLiveData()

heightLiveData.value = 0

window.decorView.setTag(R.id.navigation_height_live_data, heightLiveData)

val navigationBarView = window.decorView.findViewById(R.id.navigation_bar_view) ?: View(window.context).apply {

id = R.id.navigation_bar_view

val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, heightLiveData.value ?: 0)

params.gravity = Gravity.BOTTOM

layoutParams = params

(window.decorView as ViewGroup).addView(this)

if (this@immersiveNavigationBar is FragmentActivity) {

heightLiveData.observe(this@immersiveNavigationBar) {

val lp = layoutParams

lp.height = heightLiveData.value ?: 0

layoutParams = lp

}

}

(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {

override fun onChildViewAdded(parent: View?, child: View?) {

if (child?.id == android.R.id.navigationBarBackground) {

child.scaleX = 0f

child.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->

heightLiveData.value = bottom - top

}

} else if (child?.id == android.R.id.statusBarBackground) {

child.scaleX = 0f

}

}

override fun onChildViewRemoved(parent: View?, child: View?) {

}

})

}

通过上面方式,可以解决切换导航栏样式后自定义的导航栏高度问题:

3544bad11a3f031a24d058192eb3ac3c.gif

完整代码

@file:Suppress(“DEPRECATION”)

package com.bytedance.heycan.systembar.activity

import android.app.Activity

import android.graphics.Color

import android.os.Build

import android.util.Size

import android.view.Gravity

import android.view.View

import android.view.ViewGroup

import android.view.WindowManager

import android.widget.FrameLayout

import androidx.fragment.app.FragmentActivity

import androidx.lifecycle.LiveData

import androidx.lifecycle.MutableLiveData

import com.bytedance.heycan.systembar.R

/**

* Created by dengchunguo on 2021/4/25

*/

fun Activity.setLightStatusBar(isLightingColor: Boolean) {

val window = this.window

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

if (isLightingColor) {

window.decorView.systemUiVisibility =

View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

} else {

window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE

}

}

}

fun Activity.setLightNavigationBar(isLightingColor: Boolean) {

val window = this.window

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isLightingColor) {

window.decorView.systemUiVisibility =

window.decorView.systemUiVisibility or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0

}

}

/**

* 必须在Activity的onCreate时调用

*/

fun Activity.immersiveStatusBar() {

val view = (window.decorView as ViewGroup).getChildAt(0)

view.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->

val lp = view.layoutParams as FrameLayout.LayoutParams

if (lp.topMargin > 0) {

lp.topMargin = 0

v.layoutParams = lp

}

if (view.paddingTop > 0) {

view.setPadding(0, 0, 0, view.paddingBottom)

val content = findViewById(android.R.id.content)

content.requestLayout()

}

}

val content = findViewById(android.R.id.content)

content.setPadding(0, 0, 0, content.paddingBottom)

window.decorView.findViewById(R.id.status_bar_view) ?: View(window.context).apply {

id = R.id.status_bar_view

val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, statusHeight)

params.gravity = Gravity.TOP

layoutParams = params

(window.decorView as ViewGroup).addView(this)

(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {

override fun onChildViewAdded(parent: View?, child: View?) {

if (child?.id == android.R.id.statusBarBackground) {

child.scaleX = 0f

}

}

override fun onChildViewRemoved(parent: View?, child: View?) {

}

})

}

setStatusBarColor(Color.TRANSPARENT)

}

/**

* 必须在Activity的onCreate时调用

*/

fun Activity.immersiveNavigationBar(callback: (() -> Unit)? = null) {

val view = (window.decorView as ViewGroup).getChildAt(0)

view.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->

val lp = view.layoutParams as FrameLayout.LayoutParams

if (lp.bottomMargin > 0) {

lp.bottomMargin = 0

v.layoutParams = lp

}

if (view.paddingBottom > 0) {

view.setPadding(0, view.paddingTop, 0, 0)

val content = findViewById(android.R.id.content)

content.requestLayout()

}

}

val content = findViewById(android.R.id.content)

content.setPadding(0, content.paddingTop, 0, -1)

val heightLiveData = MutableLiveData()

heightLiveData.value = 0

window.decorView.setTag(R.id.navigation_height_live_data, heightLiveData)

callback?.invoke()

window.decorView.findViewById(R.id.navigation_bar_view) ?: View(window.context).apply {

id = R.id.navigation_bar_view

val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, heightLiveData.value ?: 0)

params.gravity = Gravity.BOTTOM

layoutParams = params

(window.decorView as ViewGroup).addView(this)

if (this@immersiveNavigationBar is FragmentActivity) {

heightLiveData.observe(this@immersiveNavigationBar) {

val lp = layoutParams

lp.height = heightLiveData.value ?: 0

layoutParams = lp

}

}

(window.decorView as ViewGroup).setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {

override fun onChildViewAdded(parent: View?, child: View?) {

if (child?.id == android.R.id.navigationBarBackground) {

child.scaleX = 0f

bringToFront()

child.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->

heightLiveData.value = bottom - top

}

} else if (child?.id == android.R.id.statusBarBackground) {

child.scaleX = 0f

}

}

override fun onChildViewRemoved(parent: View?, child: View?) {

}

})

}

setNavigationBarColor(Color.TRANSPARENT)

}

/**

* 当设置了immersiveStatusBar时,如需使用状态栏,可调佣该函数

*/

fun Activity.fitStatusBar(fit: Boolean) {

val content = findViewById(android.R.id.content)

if (fit) {

content.setPadding(0, statusHeight, 0, content.paddingBottom)

} else {

content.setPadding(0, 0, 0, content.paddingBottom)

}

}

fun Activity.fitNavigationBar(fit: Boolean) {

val content = findViewById(android.R.id.content)

if (fit) {

content.setPadding(0, content.paddingTop, 0, navigationBarHeightLiveData.value ?: 0)

} else {

content.setPadding(0, content.paddingTop, 0, -1)

}

if (this is FragmentActivity) {

navigationBarHeightLiveData.observe(this) {

if (content.paddingBottom != -1) {

content.setPadding(0, content.paddingTop, 0, it)

}

}

}

}

val Activity.isImmersiveNavigationBar: Boolean

get() = window.attributes.flags and WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION != 0

val Activity.statusHeight: Int

get() {

val resourceId =

resources.getIdentifier(“status_bar_height”, “dimen”, “android”)

if (resourceId > 0) {

return resources.getDimensionPixelSize(resourceId)

}

return 0

}

val Activity.navigationHeight: Int

get() {

return navigationBarHeightLiveData.value ?: 0

}

val Activity.screenSize: Size

get() {

return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

Size(windowManager.currentWindowMetrics.bounds.width(), windowManager.currentWindowMetrics.bounds.height())

} else {

Size(windowManager.defaultDisplay.width, windowManager.defaultDisplay.height)

}

}

fun Activity.setStatusBarColor(color: Int) {

val statusBarView = window.decorView.findViewById<View?>(R.id.status_bar_view)

if (color == 0 && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {

statusBarView?.setBackgroundColor(STATUS_BAR_MASK_COLOR)

} else {

statusBarView?.setBackgroundColor(color)

}

}

fun Activity.setNavigationBarColor(color: Int) {

val navigationBarView = window.decorView.findViewById<View?>(R.id.navigation_bar_view)

if (color == 0 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {

最后

答应大伙的备战金三银四,大厂面试真题来啦!

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
VERSION_CODES.M) {

最后

答应大伙的备战金三银四,大厂面试真题来啦!

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

[外链图片转存中…(img-ofmRC5Tc-1714689851957)]

《379页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

[外链图片转存中…(img-oTL1EAJt-1714689851958)]

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

[外链图片转存中…(img-xgCe30M8-1714689851958)]

腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析

[外链图片转存中…(img-ZGUtaRBD-1714689851958)]

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值