在 Android 15 中处理全面屏强制行为

1. 开始之前

SociaLite 展示了如何使用各种 Android 平台 API 来实现社交网络应用中常见的功能,并利用各种 Jetpack API 来实现更复杂的功能,这些功能可以在更多设备上可靠运行,并且所需的代码更少。

本 Codelab 将引导你完成使 SociaLite 应用兼容 Android 15 全面屏强制行为的过程,并以向后兼容的方式使应用实现全面屏效果。实现全面屏后,根据你的设备和导航模式,SociaLite 将如下所示

The SociaLite App in three-button navigation.

The SociaLite app in gesture navigation.

三键导航下的 SociaLite

手势导航下的 SociaLite

The SociaLite app on a large screen device.

大屏设备上的 SociaLite

前提条件

  • 基本的 Kotlin 知识。
  • 完成设置 Android Studio Codelab,或者熟悉如何在模拟器或运行 Android 15 的实体设备中使用 Android Studio 并测试应用。

你将学到什么

  • 如何处理 Android 15 全面屏变更。
  • 如何以向后兼容的方式使你的应用实现全面屏效果。

你需要准备什么

  • 最新版本的 Android Studio。
  • 运行 Android 15 Beta 1 或更高版本的测试设备或模拟器。
  • Android 15 Beta 1 或更高版本的 SDK。

2. 获取入门代码

  1. 从 GitHub 下载入门代码

或者,克隆仓库并检出 codelab_improve_android_experience_2024 分支。

$ git clone git@github.com:android/socialite.git
$ cd socialite
$ git checkout codelab_improve_android_experience_2024
  1. 在 Android Studio 中打开 SociaLite,并在你的 Android 15 设备或模拟器上运行该应用。你会看到如下所示的屏幕之一

SociaLite with three-button navigation.

SociaLite with gesture navigation.

三键导航

手势导航

SociaLite on a large screen device.

大屏

  1. 聊天页面,选择其中一个对话,例如与狗的对话。

Dog chat message with three-button navigation

Dog chat message with gesture navigation

三键导航下的狗聊天消息

手势导航下的狗聊天消息

3. 在 Android 15 上使你的应用实现全面屏效果

什么是全面屏?

应用可以在系统栏(如状态栏和导航栏)后面绘制内容,从而提供更精致的用户体验并充分利用显示空间。这被称为实现全面屏效果。

GIF of an app going edge to edge

如何处理 Android 15 全面屏变更

在 Android 15 之前,你的应用界面默认被限制在布局时避开系统栏区域,例如状态栏和导航栏。应用需要选择启用全面屏效果。根据应用的不同,选择启用可能非常简单,也可能很麻烦。

从 Android 15 开始,你的应用将默认实现全面屏效果。你将看到以下默认行为

  • 三键导航栏是半透明的。
  • 手势导航栏是透明的。
  • 状态栏是透明的。
  • 除非内容应用了内边距或填充,否则内容将在系统栏(如导航栏、状态栏和标题栏)后面绘制。

这确保了全面屏作为提升应用质量的一种手段不会被忽视,并减少了你的应用实现全面屏所需的工作。然而,此更改可能会对你的应用产生负面影响。将目标 SDK 升级到 Android 15 后,你将在 SociaLite 中看到两个负面影响示例。

将目标 SDK 值更改为 Android 15

  1. 在 SociaLite 应用的 build.gradle 文件中,将目标 SDK 和编译 SDK 版本更改为 Android 15 或 VanillaIceCream

如果你在 Android 15 正式发布之前学习本 Codelab,代码将如下所示

android {
    namespace = "com.google.android.samples.socialite"
    compileSdkPreview = "VanillaIceCream"

    defaultConfig {
        applicationId = "com.google.android.samples.socialite"
        minSdk = 21
        targetSdkPreview = "VanillaIceCream"
        ...
    }
...
}

如果你在 Android 15 正式发布之后学习本 Codelab,代码将如下所示

android {
    namespace = "com.google.android.samples.socialite"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.google.android.samples.socialite"
        minSdk = 21
        targetSdk = 35
        ...
    }
...
}
  1. 重新构建 SociaLite 并观察以下问题
  • 三键导航背景保护与导航栏不匹配。对于手势导航,聊天屏幕看起来是全面屏的,无需你进行任何干预。然而,三键导航存在背景保护,应将其移除。

Chats screen with three-button navigation.

Chats screen with gesture navigation.

三键导航下的聊天屏幕

手势导航下的聊天屏幕

  • 界面被遮挡。对话中的底部界面元素被导航栏遮挡。这在三键导航中最为明显。

Dog chat message with three-button navigation.

Dog chat message with gesture navigation.

三键导航下的狗聊天消息

手势导航下的狗聊天消息

修复 SociaLite

要移除默认的三键导航背景保护,请按照以下步骤操作

  1. MainActivity.kt 文件中,通过将 window.isNavigationBarContrastEnforced 属性设置为 false 来移除默认背景保护。
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()
        super.onCreate(savedInstanceState)
        setContent {
            // Add this block:
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                window.isNavigationBarContrastEnforced = false
            }
        }
    }
    ...
}

window.isNavigationBarContrastEnforced 确保在请求完全透明背景时,导航栏具有足够的对比度。通过将此属性设置为 false,你实际上是将三键导航背景设置为透明。window.isNavigationBarContrastEnforced 只会影响三键导航,对手势导航没有影响。

  1. 重新运行应用,并在你的 Android 15 设备上查看其中一个对话。时间轴聊天设置屏幕现在都显示为全面屏。应用的 NavigationBar(包含时间轴聊天设置按钮)绘制在系统透明的三键导航栏后面。

Chats screen with three-button navigation and banding removed.

Dog conversation in gesture navigation.

移除带状后效果的聊天屏幕

手势导航没有变化

然而,请注意对话的 InputBar 仍然被系统栏遮挡。你需要正确处理内边距来解决此问题。

Dog conversation in three-button navigation.

Dog conversation in gesture navigation.

三键导航下的狗对话。底部的输入字段被系统导航栏遮挡。

手势导航下的狗对话。底部的输入字段被系统导航栏遮挡。

在 SociaLite 中,InputBar 被遮挡。实际上,当你旋转到横屏模式或在大屏设备上时,你可能会发现顶部、底部、左侧和右侧的元素被遮挡。你需要考虑如何处理所有这些用例的内边距。对于 SociaLite,你应用填充来提升 InputBar 的可点击内容。

要应用内边距来修复被遮挡的界面,请按照以下步骤操作

  1. 导航到 ui/chat/ChatScreen.kt 文件,然后找到大约第 178 行的 ChatContent 可组合项,其中包含对话屏幕的界面。ChatContent 利用 Scaffold 轻松构建界面。默认情况下,Scaffold 提供有关系统界面的信息(例如系统栏的深度),作为内边距,你可以通过 Scaffold 的填充值(innerPadding 参数)来使用。使用 ScaffoldinnerPaddingInputBar 添加填充。
  2. ChatContent 中大约第 214 行找到 InputBar。这是一个自定义的可组合项,用于创建用户编写消息的界面。预览效果如下所示

The PreviewInputBar.

InputBar 接受一个 contentPadding 并将其作为填充应用于包含其余界面的 Row 可组合项。填充将应用于 Row 可组合项的所有侧边。你可以在大约第 432 行看到此代码。以下是 InputBar 可组合项以供参考(不要添加此代码)

// Don't add this code because it's only for reference.
@Composable
private fun InputBar(
    contentPadding: PaddingValues,
    ...,
) {
    Surface(...) {
        Row(
            modifier = Modifier
                .padding(contentPadding)
            ...
        ) {
            IconButton(...) { ... } // take picture
            IconButton(...) { ... } // attach picture
            TextField(...) // write message
            FilledIconButton(...){ ... } // send message
            }
        }
    }
}
  1. 返回 ChatContent 中的 InputBar,并更改 contentPadding 以使用系统栏内边距。这部分代码大约在第 220 行。
InputBar(
    ...
    contentPadding = innerPadding, //Add this line.
    // contentPadding = PaddingValues(0.dp), // Remove this line.
    ...
 )
  1. 在你的 Android 15 设备上重新运行应用。

Dog conversation in three-button navigation.

Dog conversation in gesture navigation.

三键导航下的狗对话,内边距应用不正确。

手势导航下的狗对话,内边距应用不正确。

底部填充已应用,因此按钮不再被系统栏遮挡;然而,顶部填充也已应用。顶部填充包含了 TopAppBar 和系统栏的深度。Scaffold 将填充值传递给其内容,以便其可以避开顶部应用栏和系统栏。

  1. 要修复顶部填充,创建一个 innerPadding PaddingValues 的副本,将顶部填充设置为 0.dp,然后将修改后的副本传递给 contentPadding
InputBar(
    ...
    contentPadding = innerPadding.copy(layoutDirection, top = 0.dp), //Add this line.
    // contentPadding = innerPadding, // Remove this line.
    ...
 )
  1. 在你的 Android 15 设备上重新运行应用。

Dog conversation in three-button navigation.

Dog conversation in gesture navigation.

三键导航下的狗对话,内边距应用正确。

手势导航下的狗对话,内边距应用正确。

恭喜!你已使 SociaLite 兼容 Android 15 全面屏平台变更。接下来,你将学习如何以向后兼容的方式使 SociaLite 实现全面屏效果。

4. 以向后兼容的方式使 SociaLite 实现全面屏效果

SociaLite 现在在 Android 15 上是全面屏效果,但在较旧的 Android 设备上仍然不是全面屏效果。要使 SociaLite 在较旧的 Android 设备上实现全面屏效果,请在 MainActivity.kt 文件中设置内容之前调用 enableEdgeToEdge

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()
        enableEdgeToEdge() // Add this line.
        window.isNavigationBarContrastEnforced = false
        super.onCreate(savedInstanceState)
        setContent {... }
    }
}

enableEdgeToEdge 的导入语句是 import androidx.activity.enableEdgeToEdge。依赖项是 AndroidX Activity 1.8.0 或更高版本。

有关如何以向后兼容的方式使你的应用实现全面屏效果以及如何处理内边距的深入概述,请参阅以下指南

全面屏部分的学习路径到此结束。下一部分是可选的,讨论了可能适用于你的应用的其他全面屏考量。

5. 可选:其他全面屏考量

跨架构处理内边距

组件

你可能已经注意到,更改目标 SDK 值后,SociaLite 中的许多组件没有发生位移。SociaLite 采用最佳实践进行架构设计,因此处理此平台更改非常容易。最佳实践包括以下内容

可滚动内容

你的应用可能包含列表,并且随着 Android 15 的更改,列表的最后一个项目可能会被系统的导航栏遮挡。

App with last list item occluded by three-button navigation.

显示列表中的最后一个项目被三键导航遮挡。

使用 Compose 实现可滚动内容

在 Compose 中,使用 LazyColumncontentPadding 为最后一个项目添加空间,除非你使用 TextField

Scaffold { innerPadding ->
    LazyColumn(
        contentPadding = innerPadding
    ) {
        // Content that does not contain TextField
    }
}

App with last list item is not occluded by three-button navigation.

显示列表中的最后一个项目未被三键导航遮挡。

对于 TextField,请使用 SpacerLazyColumn 中绘制最后一个 TextField。更多信息请参阅内边距使用

LazyColumn(
    Modifier.imePadding()
) {
    // Content with TextField
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

使用 Views 实现可滚动内容

对于 RecyclerViewNestedScrollView,添加 android:clipToPadding="false"

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    app:layoutManager="LinearLayoutManager" />

使用 setOnApplyWindowInsetsListener 从窗口内边距提供左、右和底部填充

ViewCompat.setOnApplyWindowInsetsListener(binding.recycler) { v, insets ->
    val i = insets.getInsets(
        WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.displayCutout()
    )
    v.updatePadding(
        left = i.left,
        right = i.right,
        bottom = i.bottom + bottomPadding,
    )
    WindowInsetsCompat.CONSUMED
}

使用 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS

在目标 SDK 为 35 之前,SocialLite 在横屏模式下是这样显示的,左边缘有一个大的白色框来容纳相机刘海。在三键导航中,按钮位于右侧。

The SociaLite app in landscape.

在目标 SDK 为 35 之后,SociaLite 将如下所示,左边缘不再有一个大的白色框来容纳相机刘海。要实现此效果,Android 会自动设置 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS横屏模式下的 SociaLite 应用。

根据你的应用,你可能希望在此处处理内边距。

要在 SociaLite 中执行此操作,请按照以下步骤操作

  1. ui/ContactRow.kt 文件中,找到 Row 可组合项。
  2. 修改填充以考虑显示屏刘海。
@Composable
fun ChatRow(
   chat: ChatDetail,
   onClick: (() -> Unit)?,
   modifier: Modifier = Modifier,
) {
   // Add layoutDirection, displayCutout, startPadding, and endPadding.
   val layoutDirection = LocalLayoutDirection.current
   val displayCutout = WindowInsets.displayCutout.asPaddingValues()
   val startPadding = displayCutout.calculateStartPadding(layoutDirection)
   val endPadding = displayCutout.calculateEndPadding(layoutDirection)
   Row(
       modifier = modifier
           ...
           // .padding(16.dp) // Remove this line.
           // Add this block:
           .padding(
               PaddingValues(
                   top = 16.dp,
                   bottom = 16.dp,
                   // Ensure content is not occluded by display cutouts
                   // when rotating the device.
                   start = startPadding.coerceAtLeast(16.dp),
                   end = endPadding.coerceAtLeast(16.dp)
               )
           ),
       ...
   ) { ... }

处理显示屏刘海后,SociaLite 如下所示

The SociaLite app in landscape.

你可以在开发者选项屏幕的显示屏刘海下测试各种显示屏刘海配置。

如果你的应用有一个浮动窗口(例如 Activity),并且使用了 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULTLAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVERLAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,从 Android 15 Beta 2 开始,Android 会将这些刘海模式解释为 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS。此前,在 Android 15 Beta 1 中,你的应用会崩溃。

标题栏也是系统栏

标题栏也是系统栏,因为它描述了自由形式窗口的系统界面窗口装饰,例如顶部的标题栏。你可以在 Android Studio 的桌面模拟器中查看标题栏。在以下屏幕截图中,标题栏位于应用的顶部。

Emulator showing a caption bar.

在 Compose 中,如果你使用 ScaffoldPaddingValuessafeContentsafeDrawing 或内置的 WindowInsets.systemBars,你的应用将按预期显示。但是,如果你使用 statusBar 处理内边距,你的应用内容可能不会按预期显示,因为状态栏未考虑标题栏。

在 Views 中,如果你手动使用 WindowInsetsCompat.systemBars 处理内边距,你的应用将按预期显示。如果你手动使用 WindowInsetsCompat.statusBars 处理内边距,你的应用可能不会按预期显示,因为状态栏不是标题栏。

处于沉浸式模式的应用基本不受 Android 15 全面屏强制行为的影响,因为沉浸式应用本身就已实现全面屏效果。

保护系统栏

你可能希望你的应用在手势导航下拥有透明导航栏,但在三键导航下拥有半透明或不透明导航栏。

在 Android 15 中,半透明的三键导航是默认行为,因为平台将 window.isNavigationBarContrastEnforced 属性设置为 true。手势导航仍然是透明的。

三键导航默认为半透明。

An app in three-button navigation.

通常情况下,半透明的三键导航就足够了。然而,在某些情况下,你的应用可能需要不透明的三键导航。首先,将 window.isNavigationBarContrastEnforced 属性设置为 false。然后,对于 Views,使用 WindowInsetsCompat.tappableElement;对于 Compose,使用 WindowInsets.tappableElement。如果这些值为 0,则用户正在使用手势导航;否则,用户正在使用三键导航。如果用户处于三键导航模式,则在导航栏后面绘制一个视图或框。一个 Compose 示例如下所示

通常情况下,半透明的三按钮导航栏应该足够使用。然而,在某些情况下,您的应用可能需要一个不透明的三按钮导航栏。首先,将 window.isNavigationBarContrastEnforced 属性设置为 false。然后,对于 View,使用 WindowInsetsCompat.tappableElement;对于 Compose,使用 WindowInsets.tappableElement。如果这些值为 0,则用户正在使用手势导航。否则,用户正在使用三按钮导航。如果用户正在使用三按钮导航,请在导航栏后面绘制一个视图或框。一个 Compose 示例如下所示:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            window.isNavigationBarContrastEnforced = false
            MyTheme {
                Surface(...) {
                    MyContent(...)
                    ProtectNavigationBar()
                }
            }
        }
    }
}


// Use only if required.
@Composable
fun ProtectNavigationBar(modifier: Modifier = Modifier) {
   val density = LocalDensity.current
   val tappableElement = WindowInsets.tappableElement
   val bottomPixels = tappableElement.getBottom(density)
   val usingTappableBars = remember(bottomPixels) {
       bottomPixels != 0
   }
   val barHeight = remember(bottomPixels) {
       tappableElement.asPaddingValues(density).calculateBottomPadding()
   }

   Column(
       modifier = modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Bottom
   ) {
       if (usingTappableBars) {
           Box(
               modifier = Modifier
                   .background(MaterialTheme.colorScheme.background)
                   .fillMaxWidth()
                   .height(barHeight)
           )
       }
   }
}

An app in three-button navigation.

不透明的三键导航

6. 查看解决方案代码

MainActivity.kt 文件的 onCreate 方法应如下所示

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       installSplashScreen()
       enableEdgeToEdge()
       window.isNavigationBarContrastEnforced = false
       super.onCreate(savedInstanceState)
       setContent {
           Main(
               shortcutParams = extractShortcutParams(intent),
           )
       }
   }
}

ChatScreen.kt 文件中的 ChatContent 可组合项应处理内边距

private fun ChatContent(...) {
   ...
   Scaffold(...) { innerPadding ->
       Column {
           ...
           InputBar(
               input = input,
               onInputChanged = onInputChanged,
               onSendClick = onSendClick,
               onCameraClick = onCameraClick,
               onPhotoPickerClick = onPhotoPickerClick,
               contentPadding = innerPadding.copy(
                    layoutDirection, top = 0.dp
                ),
               sendEnabled = sendEnabled,
               modifier = Modifier
                   .fillMaxWidth()
                   .windowInsetsPadding(
                       WindowInsets.ime.exclude(WindowInsets.navigationBars)
                    ),
            )
       }
   }
}

解决方案代码位于主分支中。如果你已经下载了 SociaLite

git checkout main

或者,你可以再次下载代码以查看主分支,可以直接下载或通过 git 下载

git clone git@github.com:android/socialite.git