FitsSystemWindows=false导致虚拟导航栏遮挡界面的问题

2022年10月19日

发现问题

偶然在一部有虚拟导航栏的手机上,发现之前写的一个 App 有界面被虚拟导航栏遮挡的问题。

分析

因为使用了沉浸式状态栏,其中有一段这样的代码:

val window = activity.window
val decorView = window.decorView
// false 状态栏覆盖在 window 之上,true 不会覆盖
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
    WindowCompat.setDecorFitsSystemWindows(window, decorFitsSystemWindows)
} else {
    val decorFitsFlags = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
            or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
    val sysUiVis = decorView.systemUiVisibility
    decorView.systemUiVisibility =
        if (decorFitsSystemWindows) sysUiVis and decorFitsFlags.inv() else sysUiVis or decorFitsFlags
}

FitsSystemWindows 这个属性影响的不止 statusBar,还包括 navigationBar。

解决方法

思路是判断是否有虚拟导航栏,然后给根布局设置相应的 margin 值。

于是,在网上找到这样一段代码,用于判断是否有虚拟导航栏:

// 是否有虚拟导航栏
val hasNavBar: Boolean
    get() {
        var result = false
        val context = AppGlobals.application
        val resourceId =
            context.resources.getIdentifier("config_showNavigationBar", "bool", "android")
        if (resourceId > 0) {
            result = context.resources.getBoolean(resourceId)
        }
        return result
    }

经过测试,发现手上的真机 小米10Ultra 以及 虚拟机 Pixal 3a 出现了两种相反的结果

  • 在真机 小米10UltrahasNavBar 永远为 true
  • 在虚拟机 Pixal 3ahasNavBar 永远为 false

所以,这种方法并不适合。

然后,想到通过显示区域高度与根布局高度是否一致来判断。

// 判断显示区域与根布局高度是否一致
fun hasNavBar(activity: Activity): Boolean {
    var flag = false
    val content = activity.window.decorView.findViewById<View>(android.R.id.content)
    if (null != content) {
        val wm = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            activity.display
        } else {
            wm.defaultDisplay
        }
        display?.let {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                val bounds = wm.currentWindowMetrics.bounds
                if (!isLandscape()) {
                    if (content.bottom != bounds.bottom) {
                        flag = true
                    }
                } else {
                    if (content.right != bounds.bottom) {
                        flag = true
                    }
                }
            } else {
                val point = Point()
                display.getRealSize(point)
                if (!isLandscape()) {
                    if (content.bottom != point.y) {
                        flag = true
                    }
                } else {
                    if (content.right != point.y) {
                        flag = true
                    }
                }
            }

        }
    }
    return flag
}

// 判断是否是横屏
fun isLandscape(): Boolean {
    return AppGlobals.application.resources
        .configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}

// 虚拟导航栏高度
val navBarHeight: Int
    get() {
        var result = 0
        val context = AppGlobals.application
        val resourceId =
            context.resources.getIdentifier("navigation_bar_height", "dimen", "android")
        if (resourceId > 0) {
            result = context.resources.getDimensionPixelSize(resourceId)
        }
        return result
    }

发现,这种方法在 虚拟机 Pixal 3a 上是可行的。

但是在真机 小米10Ultra 上,却是依然永远返回 true。且在不显示虚拟导航栏的情况下,导航栏的高度返回为44px,即以下截图所示底部白边。

于是想到,44px的虚拟导航栏是没有意义的,所以为了适配这部机,多加个虚拟导航栏最小高度的判断。

fun hasNavBar(activity: Activity): Boolean {
    var flag = false
    val content = activity.window.decorView.findViewById<View>(android.R.id.content)
    if (null != content) {
        val wm = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            activity.display
        } else {
            wm.defaultDisplay
        }
        display?.let {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                val bounds = wm.currentWindowMetrics.bounds
                if (!isLandscape()) {
                    if (content.bottom != bounds.bottom) {
                        flag = true
                    }
                } else {
                    if (content.right != bounds.bottom) {
                        flag = true
                    }
                }
            } else {
                val point = Point()
                display.getRealSize(point)
                if (!isLandscape()) {
                    if (content.bottom != point.y) {
                        flag = true
                    }
                } else {
                    if (content.right != point.y) {
                        flag = true
                    }
                }
            }

        }
    }
    return flag && navBarHeight > 50 // 小米10至尊版 flag 永远为true,有个最小的 navBarHeight=44
}

至此,这个方法在2部机上都可以正常显示。

小鑫

写写代码, 掉掉头发。

文章评论