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 功能。以下是相关代码:

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 问题,无需手动进行适配。

相关推荐
Mr YiRan26 分钟前
Android Gradle多渠道打包
android
IvanCodes2 小时前
MySQL 视图
android·数据库·sql·mysql·oracle
KevinWang_2 小时前
Java 和 Kotlin 混编导致的 bug
android·kotlin
好学人2 小时前
Android动画系统全面解析
android
leverge20092 小时前
android studio 运行java main报错
android·ide·android studio
RichardLai882 小时前
Flutter 环境搭建
android·flutter
思想觉悟2 小时前
ubuntu编译android12源码
android·ubuntu·源码
好学人2 小时前
Android自定义控件事件传递机制
android
V少年3 小时前
深入浅出 C++ 标准库
android
V少年3 小时前
深入浅出 C++ 特有关键字
android