处理 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 之前,您的应用 UI 默认情况下会受到限制,以避免系统栏区域(例如状态栏和导航栏)。应用选择加入边缘到边缘。根据应用的不同,选择加入可能从微不足道到繁琐不等。

从 Android 15 开始,您的应用将默认采用边缘到边缘。您将看到以下默认设置

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

这确保不会忽视边缘到边缘作为提高应用质量的一种方法,并减少了应用采用边缘到边缘所需的工作量。但是,此更改可能会对您的应用产生负面影响。在将目标 SDK 升级到 Android 15 后,您将在 SociaLite 中看到两个负面影响的示例。

将目标 SDK 值更改为 Android 15

  1. 在 SociaLite 应用的 build.gradle 文件中,将目标和编译 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.

具有三键导航的“聊天”屏幕

具有手势导航的“聊天”屏幕

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

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 的可点击内容。

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

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

The PreviewInputBar.

InputBar 获取 contentPadding 并将其作为填充应用于包含其余 UI 的 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 设备上仍然不是边缘到边缘的。要在较旧的 Android 设备上使 SociaLite 成为边缘到边缘的,请在 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。更多信息,请参阅Inset consumption

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或更高之后,SocialLite将是这样的:左侧不再有大的白色方块来适应摄像头开孔。为了实现此效果,Android会自动设置LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYSSocialLite应用的横向截图。

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

要在SocialLite中执行此操作,请遵循以下步骤

  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)
               )
           ),
       ...
   ) { ... }

处理显示开孔后,SocialLite看起来像这样

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中,你的应用会崩溃。

标题栏也是系统栏

标题栏也是系统栏,因为它描述了自由形式窗口的系统UI窗口装饰,例如顶部的标题栏。你可以在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示例可能如下所示

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)
                    ),
            )
       }
   }
}

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

git checkout main

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

git clone [email protected]:android/socialite.git