window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.navigationBarColor = Color.TRANSPARENT
但发现导航栏半透明背景依然无法去掉:
问题三:亮色系统 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
效果如下:
Android 8.0 亮色状态栏
Android 8.0 亮色导航栏
但是在亮色系统 bar 基础上开启沉浸式后,在 8.0 至 9.0 系统中,导航栏深色导航 icon 不生效,而 10.0 以上版本能显示深色导航 icon:
Android 8.0 亮色沉浸式亮色导航栏
Android 10.0 亮色沉浸式亮色导航栏
问题分析
问题一:沉浸式下无法设置背景色
查看源码发现设置状态栏和导航栏背景颜色时,是不能为沉浸式的:
问题二:无法全透明导航栏
当设置导航栏为透明色(Color.TRANSPARENT
)时,导航栏会变成半透明,当设置其他颜色,则是正常的,例如设置颜色为 0x700F7FFF,显示效果如下:
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 颜色也是不能为沉浸式:
解决沉浸式兼容性问题
对于问题二无法全透明导航栏,由上述问题分析中的代码可以看出,当且仅当设置的导航栏颜色为纯透明时(0),才会置换为半透明的蒙层。那么,我们可以将纯透明这种情况修改颜色为 0x01000000,这样也能达到接近纯透明的效果:
对于问题一,难以通过常规方式进行沉浸式下的系统 bar 背景颜色设置。而对于问题三,通过常规方式需要分别对各个版本进行适配,对于国内手机来说,适配难度更大。
为了解决兼容性问题,以及更好的管理状态栏和导航栏,我们是否能自己实现状态栏和导航栏的背景 View 呢?
通过 Layout Inspector 可以看出,导航栏和状态栏本质上也是一个 view:
在 activity 创建的时候,会创建两个 view(navigationBarBackground 和 statusBarBackground),将其加到 decorView 中,从而可以控制状态栏的颜色。那么,是否能把系统的这两个 view 隐藏起来,替换成自定义的 view 呢?
因此,为了提高兼容性,以及更好的管理状态栏和导航栏,我们可以将系统的 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) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!**
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
[外链图片转存中…(img-evCBIyT3-1713763574518)]
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
[外链图片转存中…(img-tVLvLXPv-1713763574519)]
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
[外链图片转存中…(img-WwayqSPD-1713763574520)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!