前言:
在我们的项目中有一个场景,类似系统相册中点击全屏预览,再次点击退出全屏,其中顶部toolbar要处理全屏时的对状态栏的计算,原本以外只要对状态栏的处理和计算即可,但是发现一个神奇的现象,在开启底部三个金刚键之后,toolbar的计算却出现问题。一、故事的开始:全屏的处理逻辑:
/**
* 沉浸式图片展示:内容铺满状态栏区域,但不隐藏状态栏
*/
fun enterImageImmersiveMode(activity: AppCompatActivity, lightStatusBar: Boolean = false) {
val window = activity.window
val controller = WindowInsetsControllerCompat(window, window.decorView)
// 使用post延迟执行以确保UI状态稳定
window.decorView.post {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
controller.show(WindowInsetsCompat.Type.systemBars())
controller.isAppearanceLightStatusBars = lightStatusBar
controller.isAppearanceLightNavigationBars = lightStatusBar
activity.supportActionBar?.hide()
}
}
toolbar的写法如下:
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentInsetStart="0dp"
android:fitsSystemWindows="true"
android:background="@color/black_90"
app:layout_constraintTop_toTopOf="parent">
// .....
</androidx.appcompat.widget.Toolbar>
按理来说,状态栏的高度和位置和底部导航栏的关系应该不大,才对,但是当启用底部导航栏(Navigation Bar)后,Toolbar 或顶部区域变得异常“很高”。
研究后发现,这个通常是由于系统窗口插入(WindowInsets)与 fitsSystemWindows 或 setDecorFitsSystemWindows(false) 混用不当导致布局偏移异常。
这个设置使得系统不会自动为状态栏、导航栏、刘海等区域留出空间。如果这时布局内还有 fitsSystemWindows="true",就可能导致以下问题:
1、系统将状态栏高度插入给 Toolbar,它又在 wrap_content 模式下,撑高了自身——导致Toolbar 异常变高
2、系统栏区域未正确手动适配——RecyclerView/List 被挤下去
二、关键原理分析:
- fitsSystemWindows 和 DecorView 的配合
- 在 Android 中,系统窗口(如状态栏、导航栏)会通过 WindowInsets 将“可用区域”通知到我的布局;
- fitsSystemWindows="true" 的作用是:自动将 view 的 padding 调整为 system window inset 大小(如顶部为状态栏高度,底部为导航栏高度);
- 这个属性作用的前提是:我的 Window 没有手动设置 setDecorFitsSystemWindows(window, false);
- 一旦设置 false,系统就不再自动处理这些 Insets,变成了需要我自己来处理。
- WindowInsets 与 InsetsController 行为
当使用 WindowInsetsControllerCompat.hide() 隐藏 systemBars 时:系统会设置导航栏为 IME_FLAG_FULLSCREEN;会触发 Insets 改变,从而使 DecorView 请求 layout;
如果我的布局中某些控件仍设置了 fitsSystemWindows=true,而又没有手动处理 Insets,这时:系统可能误以为你希望保留系统栏高度;
导致 Toolbar、ConstraintLayout、StatusBar 或 NavigationBar 区域的 padding/height 被重复计算或未清理。
- 系统源码追踪:DecorView 和 PhoneWindow
源码:com.android.internal.policy.DecorView:
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
...
if (mWindow.mDecorFitsSystemWindows) {
// 系统会将 inset 应用到 child view 的 padding
} else {
// 开启沉浸式:你自己处理 Insets,否则影响布局
}
}
继续看 PhoneWindow 的行为:
public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
...
mDecor.setFitsSystemWindows(decorFitsSystemWindows);
}
所以当我调用【WindowCompat.setDecorFitsSystemWindows(window, false)】时,就相当于通知系统“我来处理 system bars 区域”,系统将停止为你添加 padding。这时:
-
如果我没做额外处理,某些控件(如 Toolbar)使用了 fitsSystemWindows=true,系统可能会出错或行为不一致;
-
特别是在导航栏出现/隐藏时,布局高度会变动。
本质的核心就是:
在导航栏重新显示时,系统通过 Insets 通知 DecorView layout 更新。如果你的 Toolbar 或根布局没有正确响应 Insets,可能会出现布局误认为需要“腾出空间”显示导航栏高度,从而拉高布局。
四、解决问题
那既然有冲突的话,去掉【fitsSystemWindows="true"】,改为我们自己处理相关的逻辑是不是就可以解决问题
写了个方法,实现手动计算marginTop的值:
fun View.addStatusBarMargin() {
val statusBarHeight = context.resources.getIdentifier("status_bar_height", "dimen", "android")
.takeIf { it > 0 }
?.let { context.resources.getDimensionPixelSize(it) } ?: 0
post {
val lp = layoutParams as? ViewGroup.MarginLayoutParams
lp?.topMargin = statusBarHeight
layoutParams = lp
}
}
使用后,toolbar的位置和高度是正常了,但是又有新的问题,全屏状态下,我希望状态栏和我的toolbar是一个颜色,如果手动计算的话,相当于直接隔离开toolbar了,会出现带背景色的toolbar和原本颜色的状态栏这种违和的现象,需要继续优化问题,解决思路大致有两种
1、增加一个view,当作状态栏占位符,然后手动设置高度和位置,把它顶到状态栏:
ViewCompat.setOnApplyWindowInsetsListener(binding.statusBarPlaceholder) { view, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
view.updateLayoutParams {
height = statusBarHeight
}
insets
}
2、计算toolbar高度时加上状态栏的高度,然后将toolbar的高度设置成原本的高度+状态栏的高度,然后将布局设置为底部对齐
binding.toolbar.post {
val statusBarHeight = FullscreenHelper.getStatusBarHeight(this)
binding.toolbar.updateLayoutParams {
height = statusBarHeight + binding.toolbar.height
}
binding.toolbar.setPadding(
binding.toolbar.paddingLeft,
statusBarHeight,
binding.toolbar.paddingRight,
binding.toolbar.paddingBottom
)
}