为什么开启底部导航栏(三大金刚键)后,全屏或沉浸式模式会出现布局异常

217 阅读4分钟

前言:

在我们的项目中有一个场景,类似系统相册中点击全屏预览,再次点击退出全屏,其中顶部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 被挤下去

二、关键原理分析:

  1. fitsSystemWindows 和 DecorView 的配合
  • 在 Android 中,系统窗口(如状态栏、导航栏)会通过 WindowInsets 将“可用区域”通知到我的布局;
  • fitsSystemWindows="true" 的作用是:自动将 view 的 padding 调整为 system window inset 大小(如顶部为状态栏高度,底部为导航栏高度);
  • 这个属性作用的前提是:我的 Window 没有手动设置 setDecorFitsSystemWindows(window, false);
  • 一旦设置 false,系统就不再自动处理这些 Insets,变成了需要我自己来处理。
  1. WindowInsets 与 InsetsController 行为

当使用 WindowInsetsControllerCompat.hide() 隐藏 systemBars 时:系统会设置导航栏为 IME_FLAG_FULLSCREEN;会触发 Insets 改变,从而使 DecorView 请求 layout;

如果我的布局中某些控件仍设置了 fitsSystemWindows=true,而又没有手动处理 Insets,这时:系统可能误以为你希望保留系统栏高度;

导致 Toolbar、ConstraintLayout、StatusBar 或 NavigationBar 区域的 padding/height 被重复计算或未清理。

  1. 系统源码追踪: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
            )
        }

原文地址:mp.weixin.qq.com/s/MLqsLaNRU…