ITPub博客

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

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

原创 ANDROID 作者:山有木xi 时间:2025-06-12 22:24:20 0 删除 编辑
前言:

在我们的项目中有一个场景,类似系统相册中点击全屏预览,再次点击退出全屏,其中顶部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,变成了需要我自己来处理。

    2. WindowInsets 与 InsetsController 行为

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

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

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

    3. 系统源码追踪: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,当作状态栏占位符,然后手动设置高度和位置,把它顶到状态栏:

        fun View.addTopInsetMargin() {
            ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
                val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
                view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
                    topMargin = 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
                    )
                }


        来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69917874/viewspace-3087516/ ,如需转载,请注明出处,否则将追究法律责任。

        请登录后发表评论 登录
        全部评论
        Oracle OCA(Java),IBM高级工程师认证,中国软件行业人才(高级c语言),高级Android工程师,对数据库并发与性能调优也有一定了解

        注册时间:2019-04-25

        • 博文量
          147
        • 访问量
          390750