这个系列是我在Android开发过程中遇到的坑和爬坑历程,并且到网上搜一下发现可能坑比解决方法多,因此我打算对每个坑记录下来,辅以部分源码做解析,争取能够简单易懂。
沉浸式状态栏是几乎每个APP都必须的,可是实现起来比较多坑,不同版本实现方法不同,也比较难适配。除了用别人造的轮子之外,我们可以看看具体是怎么实现才比较优雅呢?
效果
先上代码
fun setTransparentStyle(
view: View,
window: Window,
isLightTheme: Boolean = true
) {
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
view.doOnAttach {
setInsertContentTheme(window, view, isLightTheme)
setInsertPadding(window, view)
}
}
private fun setInsertContentTheme(
window: Window,
view: View,
isLightTheme: Boolean
) {
WindowCompat.getInsetsController(window, view).apply {
isAppearanceLightStatusBars = isLightTheme
isAppearanceLightNavigationBars = isLightTheme
}
}
private fun setInsertPadding(window: Window, view: View) {
val rootWindowInsert = ViewCompat.getRootWindowInsets(window.decorView) ?: return
val statusInsert = rootWindowInsert.getInsets(WindowInsetsCompat.Type.statusBars())
val paddingTop = abs(statusInsert.top - statusInsert.bottom)
val navInsert = rootWindowInsert.getInsets(WindowInsetsCompat.Type.navigationBars())
val paddingBottom = abs(navInsert.top - navInsert.bottom)
if (paddingTop != 0 || paddingBottom != 0) {
view.setPadding(0, paddingTop, 0, paddingBottom)
}
}
使用
class MyActivity: AppCompatActivity() {
private lateinit var binding: ActivityMyBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMyBinding.inflate(layoutInflater)
// 将 root view 传进去
setTransparentStyle(binding.root, window)
setContentView(binding.root)
}
}
解析
以Android 5.0 以上为例。代码中的API尽量都使用Compat API,官方提供的兼容不同版本的API,确保能在多个版本中效果正常。
- 设置状态栏和导航栏背景为透明
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
当设置状态栏背景为透明时,背景一般显示为灰色。
然而真正的内容并没有填充状态栏,因此并不是真正意义上的沉浸式。
- 将decor view适配状态栏和导航栏去除掉(默认开启)
WindowCompat.setDecorFitsSystemWindows(window, false)
API注释是这么说的
Sets whether the decor view should fit root-level content views for WindowInsetsCompat. If set to false, the framework will not fit the content view to the insets and will just pass through the WindowInsetsCompat to the content view.
简单来说就是decor view会将根布局调整适配WindowsInserts(在这里也就是状态栏和导航栏),如果设置为false时就不会适配。在实际应用中内容就会顶到状态栏上面
- 将实际内容往下挪一个状态栏的位置,往上挪一个导航栏的位置,要实现这个有多种方法,这里就用比较简单的Padding来实现。
// 此处传入的View为root view
private fun setInsertPadding(window: Window, view: View) {
val rootWindowInsert = ViewCompat.getRootWindowInsets(window.decorView) ?: return
val statusInsert = rootWindowInsert.getInsets(WindowInsetsCompat.Type.statusBars())
val paddingTop = abs(statusInsert.top - statusInsert.bottom)
val navInsert = rootWindowInsert.getInsets(WindowInsetsCompat.Type.navigationBars())
val paddingBottom = abs(navInsert.top - navInsert.bottom)
if (paddingTop != 0 || paddingBottom != 0) {
view.setPadding(0, paddingTop, 0, paddingBottom)
}
}
通过getRootWindowInserts
API获取到WindowsInserts,而WindowsInserts的定义为。
Describes a set of insets for window content.
也就是插入到windows中的除了content视图的其他界面,例如:状态栏,导航栏,软键盘。在此处只需要获取状态栏和导航栏。再进行setPadding
即可。
- 可选:如果觉得白色不好看,在Android 6.0以上可以设置导航栏和状态栏内容颜色。
WindowCompat.getInsetsController(window, view).apply {
isAppearanceLightStatusBars = isLightTheme
isAppearanceLightNavigationBars = isLightTheme
}
爬坑
如果按照以上三步走会发现,效果只能走到第二步的效果。
fun Activity.setTransparentStyle(view: View) {
window.statusBarColor = Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
// 错误用法
setInsertPadding(window, view)
}
fun setInsertPadding(window: Window, view: View) {
...
}
class MyActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
setTransparentStyle(binding.root)
setContentView(binding.root)
}
}
分析一下发现是第三步没有生效,首先我们看一下第三步的API:
/**
* Provide original {@link WindowInsetsCompat} that are dispatched to the view hierarchy.
* The insets are only available if the view is attached.
* <p>
* On devices running API 20 and below, this method always returns null.
*
* @return WindowInsetsCompat from the top of the view hierarchy or null if View is detached
*/
@Nullable
public static WindowInsetsCompat getRootWindowInsets(@NonNull View view) {
...
}
仅在View被attached的时候才可用,不然就会返回空,于是我们就不能设置Padding了。
View那么什么时候才attached呢?onCreate()
?onResume()
?很遗憾都不是。
当Activity的onResume()
在第一次被调用之后,View.dispatchAttachedToWindow
才会被执行,也就是attached操作。
View#post
这个时候就会想到,使用View.post()
方法来执行第三步,这个方法是可以的。当执行View.post()
方法之后,要是View没被attached,这个Runnable
就会被暂时保存起来,等到执行完View.dispatchAttachedToWindow
,就会将这个Runnable
放到消息队列中等待执行。
fun Activity.setTransparentStyle(view: View) {
window.statusBarColor = Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
view.post { setInsertPadding(window, view) }
}
这种方法是可行也是可用的。
View#doOnAttach
官方也提供了一个方法用于在Attached之后执行传入的逻辑。
fun Activity.setTransparentStyle(view: View) {
window.statusBarColor = Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
view.doOnAttach { setInsertPadding(window, view) }
}
这个方法来源于ktx扩展库。
/**
* Performs the given action when this view is attached to a window. If the view is already
* attached to a window the action will be performed immediately, otherwise the
* action will be performed after the view is next attached.
*
* The action will only be invoked once, and any listeners will then be removed.
*
* @see doOnDetach
*/
public inline fun View.doOnAttach(crossinline action: (view: View) -> Unit) {
if (ViewCompat.isAttachedToWindow(this)) {
action(this)
} else {
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {
removeOnAttachStateChangeListener(this)
action(view)
}
override fun onViewDetachedFromWindow(view: View) {}
})
}
}
传入的action将会放在View被attached到window上的时候执行。
如果已经被attached到window上了就立即执行,否则调用addOnAttachStateChangeListener
添加一个listener,在onViewAttachedToWindow
中执行传入的action。
拓展
在Compose如何使用呢?
setContent {
...
val view = LocalView.current
setTransparentStyle(view)
Column(Modifier.fillMaxSize()) {
Text(text = "123123123123")
Text(text = "123123123123")
Text(text = "123123123123")
}
}
效果不能达到预期,设置Padding的方法并不能生效。
可以使用Spacer占据状态栏和导航栏。以下为参考实现。
@Composable
fun WithTransparentStyle(
modifier: Modifier = Modifier,
isLightTheme: Boolean = true,
content: @Composable ColumnScope.() -> Unit
) {
val view = LocalView.current
val density: Density = LocalDensity.current
var statusHeight by remember { mutableStateOf(0.dp) }
var navHeight by remember { mutableStateOf(0.dp) }
DisposableEffect(view, density) {
val window = (view.context as Activity).window
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
view.doOnAttach {
ViewCompat.getRootWindowInsets(window.decorView)?.let { rootWindowInsert ->
statusHeight = rootWindowInsert.getInsets(WindowInsetsCompat.Type.statusBars())
.let { statusInsert ->
density.run { (abs(statusInsert.top - statusInsert.bottom)).toDp() }
}
navHeight = rootWindowInsert.getInsets(WindowInsetsCompat.Type.navigationBars())
.let { navInsert ->
density.run { (abs(navInsert.top - navInsert.bottom)).toDp() }
}
}
}
onDispose { }
}
DisposableEffect(isLightTheme, view) {
val window = (view.context as Activity).window
view.doOnAttach {
WindowCompat.getInsetsController(window, view).apply {
isAppearanceLightStatusBars = isLightTheme
isAppearanceLightNavigationBars = isLightTheme
}
}
onDispose { }
}
Surface(modifier = modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(statusHeight))
content()
Spacer(modifier = Modifier.height(navHeight))
}
}
}
使用方式
WithTransparentStyle {
Text(text = "123123123123")
Spacer(modifier = Modifier.weight(1f))
Text(text = "123123123123")
}
效果
参考
一文读懂 View 事件分发机制:juejin.cn/post/693191…