因此,为了提高兼容性,以及更好的管理状态栏和导航栏,我们可以将系统的 navigationBarBackground 和 statusBarBackground 隐藏起来,替换成自定义的 view,而不再通过 FLAG_TRANSLUCENT_STATUS
和 FLAG_TRANSLUCENT_NAVIGATION
来设置。
实现沉浸式状态栏
- 添加自定义的状态栏。通过创建一个 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)
}
- 隐藏系统的状态栏。由于 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 即可将其隐藏起来,那么为什么不能设置 visibility
为 GONE
呢?这是因为后续在应用主题时(onApplyThemeResource
),系统会将 visibility
又重新设置为 VISIBLE
。
隐藏之后,半透明的状态栏不显示,但是顶部会出现空白:
通过 Layout Inspector 发现,decorView 的第一个元素(内容 view )会存在一个 padding:
因此,可以通过设置 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:
从图中可以看出,导航栏切换之后高度没有发生变化。为了解决这个问题,需要通过对 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?) {
}
})
}
通过上面方式,可以解决切换导航栏样式后自定义的导航栏高度问题:
完整代码
@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) {
navigationBarView?.setBackgroundColor(STATUS_BAR_MASK_COLOR)
} else {
navigationBarView?.setBackgroundColor(color)
}
}
@Suppress(“UNCHECKED_CAST”)
val Activity.navigationBarHeightLiveData: LiveData
get() {
var liveData = window.decorView.getTag(R.id.navigation_height_live_data) as? LiveData
if (liveData == null) {
liveData = MutableLiveData()
window.decorView.setTag(R.id.navigation_height_live_data, liveData)
}
return liveData
}
val Activity.screenWidth: Int get() = screenSize.width
val Activity.screenHeight: Int get() = screenSize.height
private const val STATUS_BAR_MASK_COLOR = 0x7F000000
扩展
–
对话框适配
有时候需要通过 Dialog 来显示一个提示对话框、loading 对话框等,当显示一个对话框时,即使设置了 activity 为深色状态栏和导航栏文字颜色,这时候状态栏和导航栏的文字颜色又变成白色,如下所示:
这是因为对 activity 设置的状态栏和导航栏颜色是作用 于 activity 的 window,而 dialog 和 activity 不是同一个 window,因此 dialog 也需要单独设置。
完整代码
@file:Suppress( DEPRECATION )
package com.bytedance.heycan.systembar.dialog
import android.app.Dialog
import android.os.Build
import android.view.View
import android.view.ViewGroup
/**
* Created by dengchunguo on 2021/4/25
*/
fun Dialog.setLightStatusBar(isLightingColor: Boolean) {
val window = this.window ?: return
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 Dialog.setLightNavigationBar(isLightingColor: Boolean) {
val window = this.window ?: return
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
}
}
fun Dialog.immersiveStatusBar() {
val window = this.window ?: return
(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?) {
}
})
}
fun Dialog.immersiveNavigationBar() {
val window = this.window ?: return
(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
}
}
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
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
}
}
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-smuNs1Cc-1715364882820)]
[外链图片转存中…(img-Q5vmOLPC-1715364882822)]
[外链图片转存中…(img-etcSQRnD-1715364882823)]
[外链图片转存中…(img-fNM9uod6-1715364882824)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!