返回

Android应用内容全屏绘制并保持系统栏不透明

Android

如何在 Android 中实现应用内容绘制到系统栏区域并保持系统栏完全不透明?

开发 Android 应用时,你可能会遇到这样一个需求:想让应用的内容扩展到状态栏和导航栏下方,同时保持系统栏(状态栏和导航栏)完全不透明,而且所有区域都要可以接收到 AMotionEvent (触摸事件)。 简单的 AWINDOW_FLAG_* 系列标志位似乎解决不了。别急,这篇博客就来好好聊聊这个问题。

一、问题根源:默认的窗口行为

默认情况下,Android 系统会把应用窗口限制在系统栏之间。这样做有它的道理:

  1. 保证了系统栏可见,方便用户查看时间、电量、通知等信息。
  2. 防止应用界面元素被系统栏遮挡,影响用户操作。

但是,这种行为不满足所有需求,特别是在一些需要沉浸式体验或者全屏展示的场景。

二、解决方案:边缘到边缘 (Edge-to-Edge) 布局

要让应用内容绘制到系统栏下方,核心思路是采用"边缘到边缘" (Edge-to-Edge) 布局,同时控制系统栏的可见性和外观。以下是几种可行的方案,以及它们分别在非全屏模式和全屏模式下的具体实现:

方案 1:WindowInsetsControllerCompat + setAppearanceLightNavigationBars (API 级别 27+)

这个方案通过 WindowInsetsControllerCompat 来精确控制系统栏外观。兼容性更好,能向后支持到 API Level 27 及以上。

原理:

  • WindowInsetsControllerCompat: 提供控制窗口内边距(insets)行为和外观的能力,比如系统栏的显示/隐藏、行为以及外观。
  • setSystemBarsBehavior(): 设置系统栏行为,BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 允许通过边缘滑动临时显示系统栏。
  • setAppearanceLightStatusBars()/setAppearanceLightNavigationBars():设置状态栏/导航栏的前景色是否为浅色(背景通常为深色)。

非全屏模式:

// 在 Activity 的 onCreate() 方法中调用
fun setupEdgeToEdgeNonFullscreen() {
    val decorView = window.decorView
    val windowInsetsController = WindowCompat.getInsetsController(window, decorView)

     // 将内容延伸到系统栏
    WindowCompat.setDecorFitsSystemWindows(window, false)
    
    // 设置状态栏背景透明
    window.statusBarColor = Color.TRANSPARENT 
    
    //设置导航栏背景为你需要的纯色,以保证“不透明”的需求,假设用黑色
    window.navigationBarColor = Color.BLACK

     // 使状态栏内容具有可读性,当statusBarColor是透明色时,通过设置成true或false来将系统状态栏图标的着色变为白色或黑色.
     windowInsetsController.isAppearanceLightStatusBars = false  

     //使导航栏上的图标/文字 可读
      windowInsetsController.isAppearanceLightNavigationBars = false
}

全屏模式:

fun setupEdgeToEdgeFullscreen() {
    val decorView = window.decorView
    val windowInsetsController = WindowCompat.getInsetsController(window, decorView)
    
    // 将内容延伸到系统栏下方
     WindowCompat.setDecorFitsSystemWindows(window, false)

    // 隐藏系统栏, 同时让内容全屏显示.
    windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())

    // 设置系统栏在滑动边缘时出现.
    windowInsetsController.systemBarsBehavior =
            WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
    
     // 设置状态栏和导航栏背景完全透明
    window.statusBarColor = Color.TRANSPARENT 
    window.navigationBarColor = Color.TRANSPARENT

     // 使状态栏图标可读
    windowInsetsController.isAppearanceLightStatusBars = false

     //使导航栏上的图标/文字 可读.
    windowInsetsController.isAppearanceLightNavigationBars = false

}

注意: 非全屏模式,如果为了确保“不透明” 需求,你或许需要一个纯色 navigationBarColor 值,来代替Color.TRANSPARENT.
全屏模式则可以直接把 statusBarColornavigationBarColor 设置成Color.TRANSPARENT, 滑出系统栏,就会应用原本设置好的颜色.

安全性建议:

  • 隐藏系统栏时,要提供明确的退出全屏的方式(比如手势或按钮)。
  • 在进行 Edge-to-Edge 布局的时候,应妥善处理内边距(比如通过 padding)来避免你的 UI 元素被系统栏覆盖。

进阶使用技巧:
可以使用setOnApplyWindowInsetsListener监听WindowInsets的变化,以便动态的调整你UI的位置和大小。

方案 2: 旧版 Flags + View.SYSTEM_UI_FLAG_* (适用于旧版系统, 较复杂)

如果需要支持更旧的 Android 版本 (API Level 27 以下),可以结合使用 window.decorView.systemUiVisibility 属性和旧版的标志位,这个方式更加复杂,处理兼容性也更麻烦。

原理:

  • View.SYSTEM_UI_FLAG_LAYOUT_STABLE: 保证布局稳定,避免内容因系统栏的显示/隐藏而跳动。
  • View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION: 将内容布局延伸到导航栏下方。
  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN: 将内容布局延伸到状态栏下方。
  • View.SYSTEM_UI_FLAG_HIDE_NAVIGATION: 隐藏导航栏。
  • View.SYSTEM_UI_FLAG_FULLSCREEN: 隐藏状态栏。
  • View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY: 沉浸模式,用户从边缘滑动可以临时显示系统栏,并自动再次隐藏.

非全屏模式:

fun setupLegacyEdgeToEdgeNonFullscreen() {
     val decorView = window.decorView
     val flags = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)

        decorView.systemUiVisibility = flags

    // 设置状态栏颜色. 保证可见性和内容不被遮挡.
     window.statusBarColor = Color.TRANSPARENT 
      //假设黑色导航栏. 实际情况根据你应用配色而定
    window.navigationBarColor =  Color.BLACK

}

全屏模式:

 fun setupLegacyEdgeToEdgeFullscreen() {

        val decorView = window.decorView
        val flags = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                or View.SYSTEM_UI_FLAG_FULLSCREEN
                or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
         decorView.systemUiVisibility = flags

     //设置透明以便在滑出状态栏时显示已经设置的系统栏颜色
        window.statusBarColor =  Color.TRANSPARENT
        window.navigationBarColor = Color.TRANSPARENT

}

安全性建议:

  • 和方案一相同。

进阶技巧:
由于不同版本的行为有所不同,要充分测试。可以组合设置 decorView.setOnSystemUiVisibilityChangeListener来响应系统 UI 可见性的变化。

接收 AMotionEvent

不论你选择哪一种方法去绘制你的内容,让所有的区域都能接收 AMotionEvent 的重点是保证两点:

  1. 确保 Window 开启 FLAG_LAYOUT_IN_SCREEN 标志, 这个 flag 让 window 的 layout 会占据整个屏幕,即使系统栏存在,系统栏会绘制在你的内容之上, 但内容会“透出”到这些区域中:
    window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
  2. 你的布局必须填充满整个屏幕。这通常意味你的 root view 需要使用 match_parent for both width and height:
      <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
        <!-- 你的内容 -->
       </FrameLayout>
    

三、小结

实现应用内容绘制到系统栏区域并保持系统栏完全不透明,需要综合考虑 Android 系统版本、用户体验和开发成本。优先推荐使用 WindowInsetsControllerCompat 方案,因为它更简洁、易用,并且具有更好的兼容性。如果必须支持旧版本,再考虑使用旧版 Flags 方案。记得要妥善处理布局,避免 UI 元素与系统栏冲突,同时要让全屏模式可退出,提供流畅的用户体验。