一、背景
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 功能。以下是相关代码:
ini
<?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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android: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:
bash
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文件中添加如下库的依赖:
dart
dependencies {
// Java language implementation
implementation 'androidx.activity:activity:$activity_version'
// Kotlin
implementation 'androidx.activity:activity-ktx:$activity_version'
}
第二步,在Activity的onCreate函数中添加如下代码:
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
...
}
}
enableEdgeToEdge 应在 setContentView 之前调用此方法,默认情况下,enableEdgeToEdge 会使系统栏透明,但在三按钮导航模式下,状态栏会获得半透明的遮罩。系统图标和遮罩的颜色会根据系统的浅色或深色主题进行调整。
2.2 系统条
适配的代码其实还是比较简单的,主要就是借助 ViewCompat.setOnApplyWindowInsetsListener 这个函数,来对某些指定的 View 进行偏移,保证其不会被系统的状态栏或导航栏遮挡住就可以了。
在第二节的例子中,若要避免图片被状态栏和导航栏遮挡,只需对代码进行如下修改:
kotlin
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 提供的信息来设置视图的外边距,从而避免界面元素被截断。这里以获取屏幕左上角的圆角信息为例:
kotlin
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 库来实现。示例代码如下:
dart
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
// 隐藏系统栏
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
// 显示系统栏
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
当采用无边框设计时,可能需要手动调整系统栏图标颜色,确保其与应用背景形成鲜明对比,以提升视觉辨识度。例如,若要创建浅色状态栏图标,可按以下步骤操作:
ini
WindowCompat.getInsetsController(window, window.decorView)
.isAppearanceLightStatusBars = false
四、Compose中如何适配
下面是将第二节的代码转换为 Jetpack Compose 实现的内容。转换后的代码如下:
kotlin
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 函数,对指定的可组合项进行偏移,从而避免其被系统的状态栏或导航栏遮挡。修改后的代码如下:
less
@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 函数,例如曲面屏手机、输入法弹出等场景。由于函数数量较多,这里不再逐一介绍。若你想深入了解,可以参考官方文档
developer.android.com/reference/k...
另外,如果你使用了一些 Compose Material 3 的控件,像 TopAppBar、BottomAppBar、NavigationBar 等,它们会自动处理 Insets 问题,无需手动进行适配。