Android 15强制edge-to-edge全面屏体验
一、背景
Edge-to-edge 全面屏体验并非 Android 15 才有的新功能,早在 Android 15 之前系统就已支持。然而,该功能推出多年来,众多应用程序依旧未针对全面屏体验进行适配。因此,在 Android 15 的更新中,Google 终于决定强推这一功能,力求让所有应用程序都能带来更出色的使用体验。
需要注意的是,在 Android 15 系统下,仅当应用程序将 targetSdkVersion 指定为 35 或更高版本时,系统才会强制启用 edge-to-edge 功能。所以,若开发者不想进行适配,只要不升级 targetSdkVersion 版本即可。
二、什么是 edge-to-edge 全面屏体验
我们的 App 将以边到边的方式显示,窗口会在系统栏后面绘制,从而跨越整个显示屏的宽度和高度。系统栏包括状态栏、标题栏和导航栏。
下面通过一个具体示例,来深入探究 edge-to-edge 全面屏效果。在项目里,当把 targetSdkVersion 指定为 34 时,默认不会强制开启 edge-to-edge 功能。以下是相关代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><ImageViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:adjustViewBounds="true"android:contentDescription="@null"android:scaleType="fitXY"android:src="@drawable/test" /></LinearLayout>
</androidx.core.widget.NestedScrollView>
将该项目运行于 Android 15 设备上,效果如下图所示:
当把 targetSdkVersion 指定为 35 或者更高版本时,App 会自动切换至 edge-to-edge 全面屏效果,如下图所示:
为确保在开启 edge-to-edge 全面屏体验后,导航条不会因背景因素而难以辨认,Android 系统做了相应优化。当在屏幕上进行滚动操作时,导航条的颜色会随之改变。
若手机底部采用的是传统的 Back、Home、Task 三按键导航栏,而非手势导航栏,edge-to-edge 全面屏体验会有所不同。此时,导航栏会呈现半透明效果,默认不透明度为 80%,效果如下:
从这种显示效果能够看出,三按键导航栏在 edge-to-edge 全面屏体验方面存在明显不足,未来很可能会逐渐被 Android 系统边缘化。
随着 edge-to-edge 全面屏体验的普及,一些与状态栏、导航栏颜色设置相关的 API 也逐渐被边缘化。这是因为这些 API 与 edge-to-edge 全面屏体验存在冲突,部分 API 当下已无法使用,部分则不再被推荐使用,例如以下这些 API:
Window#setStatusBarColor
Window#setStatusBarContrastEnforced
Window#setNavigationBarColor
Window#setNavigationBarContrastEnforced
三、如何适配
是否需要针对 edge-to-edge 全面屏进行额外的适配工作,很大程度上取决于应用界面的具体设计。就像前文第二节所举的例子,即便不做任何适配,用户体验依旧良好。然而,换作其他界面,情况可能就大不相同了。
接下来,我们以腾讯 QMUI_Android 的主界面为例,看看它在 edge-to-edge 全面屏体验下的实际效果,结果如下:
可以看到,这次的显示效果并不理想。在主界面,底部的 tab 栏陷入了导航栏区域,这会对 tab 按钮的操作产生干扰。而在其他页面,页面内容延伸到了状态栏区域,使得页面内容与状态栏相互重叠,严重影响了内容的可读性。
这这些问题正是 edge-to-edge 全面屏体验可能带来的典型状况,同时也是我们在开发过程中需要进行适配优化的重点方向。
2.1 启用无边框显示
前面说过,edge-to-edge 全面屏体验其实并不是全新的功能,在 Android 15 之前也是支持的,Android 15 只是将这个功能强制开启了而已。要在 Android 15 之前的设备上启用 edge-to-edge 全面屏体验,只需要额外两步就可以完成。
第一步,在项目的build.gradle文件中添加如下库的依赖:
dependencies {// Java language implementationimplementation 'androidx.activity:activity:$activity_version'// Kotlinimplementation 'androidx.activity:activity-ktx:$activity_version'
}
第二步,在Activity的onCreate函数中添加如下代码:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {enableEdgeToEdge()super.onCreate(savedInstanceState)...}
}
enableEdgeToEdge 应在 setContentView 之前调用此方法,默认情况下,enableEdgeToEdge 会使系统栏透明,但在三按钮导航模式下,状态栏会获得半透明的遮罩。系统图标和遮罩的颜色会根据系统的浅色或深色主题进行调整。
2.2 系统条
适配的代码其实还是比较简单的,主要就是借助 ViewCompat.setOnApplyWindowInsetsListener 这个函数,来对某些指定的 View 进行偏移,保证其不会被系统的状态栏或导航栏遮挡住就可以了。
在第二节的例子中,若要避免图片被状态栏和导航栏遮挡,只需对代码进行如下修改:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)insets}}
}
由于我们不希望图片延伸到状态栏和导航栏区域,所以调用 WindowInsetsCompat.Type.systemBars 来获取所有系统条(包含状态栏和导航栏)的 Insets。借助这个 Insets,我们能够获取状态栏和导航栏的高度,然后为 NestedScrollView 设置内边距(padding),这样就能确保图片内容不会进入状态栏和导航栏。
添加这段代码后,重新运行程序,便可得到较为理想的显示效果,具体效果如下:
除了 WindowInsetsCompat.Type.systemBars,还有多种其他类型的 Insets 可供选择:
-
若希望某个 View 不进入状态栏区域,可使用 WindowInsetsCompat.Type.statusBars。
-
若希望某个 View 不进入导航栏区域,可使用 WindowInsetsCompat.Type.navigationBars。
-
若希望某个 View 不进入 Cutout 区域,可使用 WindowInsetsCompat.Type.displayCutout。
Cutout 这一概念是在 Android 9 系统中引入的。当时,手机市场刚兴起刘海屏,为了适配可能出现的各种不同样式的刘海设计,Google 推出了 Cutout API。不过,后来手机厂商并未设计出各种奇形怪状的刘海,大多选择将刘海区域整合到状态栏中。因此,如今 displayCutout 这个 API 的实际效果与 statusBars 已无太大差异。
2.3 应用圆角
在 Android 设备的屏幕设计中,圆角屏幕逐渐成为一种流行趋势。从 Android 12(API 级别 31)开始,系统提供了 RoundedCorner 和 WindowInsets.getRoundedCorner 相关 API,利用这些 API 可以获取设备屏幕圆角的半径和中心点。其主要目的在于避免应用的界面元素在圆角屏幕上被截断,从而保证应用在不同屏幕形状的设备上都能有良好的显示效果。
当在应用中实现这些 API 时,无需担心对非圆角屏幕设备产生影响,因为这些 API 仅会在支持圆角屏幕的设备上生效,对于非圆角屏幕设备不会有任何额外的处理。
以下示例代码展示了如何依据 RoundedCorner 提供的信息来设置视图的外边距,从而避免界面元素被截断。这里以获取屏幕左上角的圆角信息为例:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)val main = findViewById<View>(R.id.main)ViewCompat.setOnApplyWindowInsetsListener(main) { _, insets ->if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {val rootWindowInsets = main.rootWindowInsets// 获取屏幕左上角的圆角信息 val topLeft = rootWindowInsets?.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)Log.d("lx-test", " topLeft: $topLeft")// 根据topLeft的信息设置外边距}insets}}
}
运行日志如下:
在获取到屏幕圆角信息后,后续我们可进一步根据 RoundedCorner 对界面进行外边距的设置,以此有效避免界面元素被屏幕圆角截断,提升应用在圆角屏幕设备上的视觉体验。
2.4 手势导航
自 Android 10 起,Google 引入了手势导航功能。在此模式下,手机屏幕的左右两侧可用于触发 Back 键操作,屏幕底部则用于触发 Home 键操作,其触发区域如下面的图示中的橙色部分所示:
这意味着,若我们设计的应用界面在这些区域也存在相似的手势操作,就会引发手势冲突问题,导致用户操作无法正常执行。
如同处理系统栏内边距问题一样,我们可以借助 WindowInsetsCompat.Type.systemGestures 来获取橙色区域的 Insets。随后,通过设置内边距(padding)的方式,让存在事件冲突的 View 避开这个区域,从而避免与系统手势内边距重叠,确保用户操作的流畅性。
2.5 Material 组件
许多基于 View 的 Android Material 组件 (com.google.android.material)具备自动处理边衬区的能力,像 BottomAppBar、BottomNavigationView、NavigationRailView 以及 NavigationView这些组件都能自行处理边衬区的相关问题。
然而,AppBarLayout 它不会自动处理内边距。可以添加 android:fitsSystemWindows="true" 以处理顶部边衬区。
所以,当项目中使用了 Material 组件时,开发者需要依据具体的应用场景,有针对性地对这些组件进行适配操作,以确保界面在不同的系统环境下都能有良好的显示和交互效果。
2.6 沉浸模式
在某些场景下,将内容以全屏模式呈现,能为用户带来绝佳体验,使其更具身临其境之感。为了在沉浸模式下隐藏系统栏,可以借助 WindowInsetsController 和 WindowInsetsControllerCompat 库来实现。示例代码如下:
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
// 隐藏系统栏
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
// 显示系统栏
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
当采用无边框设计时,可能需要手动调整系统栏图标颜色,确保其与应用背景形成鲜明对比,以提升视觉辨识度。例如,若要创建浅色状态栏图标,可按以下步骤操作:
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = false
四、Compose中如何适配
下面是将第二节的代码转换为 Jetpack Compose 实现的内容。转换后的代码如下:
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContent {EdgeToEdgeTheme {MainPage()}}}
}@Composable
fun MainPage() {Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {Image(painter = painterResource(id = R.drawable.test),contentDescription = null,contentScale = ContentScale.FillWidth,modifier = Modifier.fillMaxWidth())}
}
若不希望图片延伸到状态栏和导航栏区域,可借助 Modifier 的 systemBarsPadding 函数,对指定的可组合项进行偏移,从而避免其被系统的状态栏或导航栏遮挡。修改后的代码如下:
@Composable
fun MainPage() {Column(modifier = Modifier.fillMaxSize().systemBarsPadding() .verticalScroll(rememberScrollState())) {// ...}
}
systemBarsPadding 是 Compose 内置的专门用于处理 Insets 问题的函数。此外,Compose 还提供了许多其他实用的函数:
-
statusBarsPadding:可防止 Compose 控件的内容绘制到状态栏区域。
-
navigationBarsPadding:能避免 Compose 控件的内容绘制到导航栏区域。
-
displayCutoutPadding:可保护 Compose 控件的内容不进入 Cutout 区域。
-
safeDrawingPadding:该函数可确保 Compose 控件的内容不会绘制到任何系统 UI 区域,涵盖状态栏、导航栏、刘海区域等,是最常用的 Insets 处理函数之一。
-
safeGesturesPadding:能避免与系统手势发生冲突。
-
safeContentPadding:它是 safeDrawingPadding 和 safeGesturesPadding 的结合,可保证界面和手势都不会与系统 UI 发生冲突或覆盖。
除了上述这些常用函数外,Compose 还提供了众多用于解决其他场景问题的 Insets 函数,例如曲面屏手机、输入法弹出等场景。由于函数数量较多,这里不再逐一介绍。若你想深入了解,可以参考官方文档
https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier
另外,如果你使用了一些 Compose Material 3 的控件,像 TopAppBar、BottomAppBar、NavigationBar 等,它们会自动处理 Insets 问题,无需手动进行适配。