1. 开始之前
SociaLite 演示了如何使用各种 Android 平台 API 来实现社交网络应用中常见的特性,利用各种 Jetpack API 来实现可在更多设备上可靠运行且代码更少的复杂功能。
本 Codelab 将引导您完成使 SociaLite 应用与 Android 15 边缘到边缘强制执行兼容以及以向后兼容的方式使应用边缘到边缘的过程。在实现边缘到边缘之后,SociaLite 的外观将如下所示,具体取决于您的设备和导航模式
具有三键导航的 SociaLite | 具有手势导航的 SociaLite |
在大屏幕设备上的 SociaLite |
先决条件
- 基本的 Kotlin 知识。
- 完成 设置 Android Studio codelab,或熟悉如何在运行 Android 15 的模拟器或物理设备上使用 Android Studio 和测试应用。
您将学习什么
- 如何处理 Android 15 边缘到边缘的更改。
- 如何以向后兼容的方式使您的应用边缘到边缘。
您需要什么
- 最新版本的 Android Studio。
- 运行 Android 15 Beta 1 或更高版本的测试设备或模拟器。
- Android 15 Beta 1 SDK 或更高版本。
2. 获取起始代码
- 从 GitHub 下载 起始代码。
或者,克隆存储库并检出 codelab_improve_android_experience_2024
分支。
$ git clone git@github.com:android/socialite.git
$ cd socialite
$ git checkout codelab_improve_android_experience_2024
- 在 Android Studio 中打开 SociaLite,并在您的 Android 15 设备或模拟器上运行该应用。您将看到如下所示的屏幕之一
三键导航 | 手势导航 |
大屏幕 |
- 在“聊天”页面上,选择其中一个对话,例如与狗狗的对话。
具有三键导航的狗狗聊天消息 | 具有手势导航的狗狗聊天消息 |
3. 在 Android 15 上使您的应用边缘到边缘
什么是边缘到边缘?
应用可以在系统栏后面绘制,从而提供精致的用户体验并充分利用显示空间。这称为边缘到边缘。
如何处理 Android 15 边缘到边缘的更改
在 Android 15 之前,您的应用 UI 默认情况下会受到限制,以避免系统栏区域(例如状态栏和导航栏)。应用选择加入边缘到边缘。根据应用的不同,选择加入可能从微不足道到繁琐不等。
从 Android 15 开始,您的应用将默认采用边缘到边缘。您将看到以下默认设置
- 三键导航栏是半透明的。
- 手势导航栏是透明的。
- 状态栏是透明的。
- 除非内容应用内边距或填充,否则内容将绘制在系统栏(如导航栏、状态栏和标题栏)后面。
这确保不会忽视边缘到边缘作为提高应用质量的一种方法,并减少了应用采用边缘到边缘所需的工作量。但是,此更改可能会对您的应用产生负面影响。在将目标 SDK 升级到 Android 15 后,您将在 SociaLite 中看到两个负面影响的示例。
将目标 SDK 值更改为 Android 15
- 在 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
...
}
...
}
- 重新构建 SociaLite 并观察以下问题
- 三键导航背景保护与导航栏不匹配。“聊天”屏幕在您无需任何干预的情况下即可实现手势导航的边缘到边缘显示。但是,存在应该移除的三键导航背景保护。
具有三键导航的“聊天”屏幕 | 具有手势导航的“聊天”屏幕 |
- UI 被遮挡。对话的底部 UI 元素被导航栏遮挡。这在三键导航中最明显。
具有三键导航的狗狗聊天消息 | 具有手势导航的狗狗聊天消息 |
修复 SociaLite
要移除默认的三键导航背景保护,请执行以下步骤
- 在
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
仅会影响三按钮导航,对姿态导航没有影响。
- 重新运行应用程序并在您的 Android 15 设备上查看其中一个对话。时间线、聊天和设置屏幕现在都显示为边缘到边缘。应用程序的
NavigationBar
(带有时间线、聊天和设置按钮)绘制在系统透明的三按钮导航栏后面。
聊天屏幕已去除条带 | 姿态导航无变化 |
但是,请注意,对话的 InputBar
仍然被系统栏遮挡。您需要正确处理内边距才能解决此问题。
三按钮导航中的狗狗对话。底部的输入字段被系统的导航栏遮挡。 | 姿态导航中的狗狗对话。底部的输入字段被系统的导航栏遮挡。 |
在 SociaLite 中,InputBar
被遮挡。实际上,当您旋转到横向模式或在大型屏幕设备上时,您可能会发现顶部、底部、左侧和右侧的元素被遮挡。您需要考虑如何处理所有这些用例的内边距。对于 SociaLite,您可以应用填充以增加 InputBar
的可点击内容。
要应用内边距以修复被遮挡的 UI,请按照以下步骤操作
- 导航到
ui/chat/ChatScreen.kt
文件,然后找到大约第 178 行附近的ChatContent
可组合项,其中包含对话屏幕的 UI。ChatContent
利用Scaffold
来轻松构建 UI。默认情况下,Scaffold
提供有关系统 UI 的信息,例如系统栏的深度,作为您可以使用Scaffold
的填充值(innerPadding
参数)使用 的内边距。使用Scaffold
的innerPadding
将填充添加到InputBar
。 - 在
ChatContent
中大约第 214 行附近找到InputBar
。这是一个自定义的可组合项,用于创建用户编写消息的 UI。预览如下所示
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
}
}
}
}
- 返回
ChatContent
中的InputBar
并更改contentPadding
,以便您使用系统栏内边距。这大约在第 220 行。
InputBar(
...
contentPadding = innerPadding, //Add this line.
// contentPadding = PaddingValues(0.dp), // Remove this line.
...
)
- 在您的 Android 15 设备上重新运行应用程序。
错误应用内边距的三按钮导航中的狗狗对话。 | 错误应用内边距的姿态导航中的狗狗对话。 |
应用底部填充后,按钮不再被系统栏遮挡;但是,也应用了顶部填充。顶部填充包含 TopAppBar
和系统栏的深度。Scaffold 将填充值传递给其内容,以便它可以避免顶部应用栏以及系统栏。
- 要修复顶部填充,请创建
innerPadding
PaddingValues
的副本,将顶部填充设置为0.dp
,并将修改后的副本传递到contentPadding
。
InputBar(
...
contentPadding = innerPadding.copy(layoutDirection, top = 0.dp), //Add this line.
// contentPadding = innerPadding, // Remove this line.
...
)
- 在您的 Android 15 设备上重新运行应用程序。
正确应用内边距的三按钮导航中的狗狗对话。 | 正确应用内边距的姿态导航中的狗狗对话。 |
恭喜!您使 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 采用最佳实践构建,因此处理此平台更改很容易。最佳实践包括以下内容
- 使用 Material Design 3 组件(
androidx.compose.material3
),例如TopAppBar
、BottomAppBar
和NavigationBar
,因为它们自动应用内边距。 - 如果您的应用程序改为在 Compose 中使用 Material 2 组件(
androidx.compose.material
),则这些组件本身不会自动处理内边距。但是,您可以访问内边距并手动应用它们。在androidx.compose.material 1.6.0
和更高版本中,使用windowInsets
参数手动为BottomAppBar
、TopAppBar
、BottomNavigation
和NavigationRail
应用内边距。同样,对Scaffold
使用contentWindowInsets
参数。否则,请手动将内边距应用为填充。 - 如果您的应用程序使用视图和 Material 组件(
com.google.android.material
),大多数基于视图的 Material 组件(例如BottomNavigationView
、BottomAppBar
、NavigationRailView
和NavigationView
)处理内边距,因此可能不需要额外的工作。但是,如果使用AppBarLayout
,则需要添加android:fitsSystemWindows="true"
。 - 如果您的应用程序使用视图和
BottomSheet
、SideSheet
或自定义容器,请使用ViewCompat.setOnApplyWindowInsetsListener
应用填充。对于RecyclerView
,请使用此侦听器应用填充,并添加clipToPadding="false"
。 - 使用
Scaffold
(或NavigationSuiteScaffold
或ListDetailPaneScaffold
),而不是Surface
,来构建复杂的UI。Scaffold
可以让你轻松地放置TopAppBar
、BottomAppBar
、NavigationBar
和NavigationRail
。
滚动内容
你的应用可能包含列表,并且在Android 15的更改后,列表的最后一项可能被系统的导航栏遮挡。
显示列表中的最后一项被三键式导航遮挡。
使用Compose进行滚动内容
在Compose中,使用LazyColumn
的contentPadding为最后一项添加空间,除非你使用的是TextField
。
Scaffold { innerPadding ->
LazyColumn(
contentPadding = innerPadding
) {
// Content that does not contain TextField
}
}
显示列表中的最后一项没有被三键式导航遮挡。
对于TextField
,使用Spacer
在LazyColumn
中绘制最后一个TextField
。更多信息,请参阅Inset consumption。
LazyColumn(
Modifier.imePadding()
) {
// Content with TextField
item {
Spacer(
Modifier.windowInsetsBottomHeight(
WindowInsets.systemBars
)
)
}
}
使用Views进行滚动内容
对于RecyclerView
或NestedScrollView
,添加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在横向模式下是这样的:左侧有一个大的白色方块来适应摄像头开孔。在三键式导航中,按钮位于右侧。
在目标SDK版本为35或更高之后,SocialLite将是这样的:左侧不再有大的白色方块来适应摄像头开孔。为了实现此效果,Android会自动设置LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS。
根据你的应用,你可能希望在此处处理内嵌。
要在SocialLite中执行此操作,请遵循以下步骤
- 在
ui/ContactRow.kt
文件中,找到Row组合。 - 修改填充以适应显示开孔。
@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看起来像这样
你可以在开发者选项屏幕上的显示开孔下测试各种显示开孔配置。
如果你的应用有一个非悬浮窗口(例如,Activity)正在使用LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
、LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
或LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
,从Android 15 Beta 2开始,Android会将这些开孔模式解释为LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
。之前,在Android 15 Beta 1中,你的应用会崩溃。
标题栏也是系统栏
标题栏也是系统栏,因为它描述了自由形式窗口的系统UI窗口装饰,例如顶部的标题栏。你可以在Android Studio的桌面模拟器中查看标题栏。在下图中,标题栏位于应用的顶部。
在Compose中,如果你使用的是Scaffold的PaddingValues
、safeContent
、safeDrawing
或内置的WindowInsets.systemBars
,你的应用将按预期显示。但是,如果你使用statusBar
处理内嵌,你的应用内容可能无法按预期显示,因为状态栏不包含标题栏。
在Views中,如果你使用WindowInsetsCompat.systemBars
手动处理内嵌,你的应用将按预期显示。如果你使用WindowInsetsCompat.statusBars
手动处理内嵌,你的应用可能无法按预期显示,因为状态栏不是标题栏。
沉浸模式下的应用
处于沉浸模式的屏幕很大程度上不受Android 15边缘到边缘强制执行的影响,因为沉浸式应用已经是边缘到边缘的。
保护系统栏
你可能希望你的应用为手势导航使用透明栏,但为三键式导航使用半透明或不透明栏。
在Android 15中,半透明的三键式导航是默认设置,因为平台将window.isNavigationBarContrastEnforced
属性设置为true
。手势导航保持透明。
三键式导航默认情况下是半透明的。 |
通常,半透明的三键式导航就足够了。但是,在某些情况下,你的应用可能需要不透明的三键式导航。首先,将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)
)
}
}
}
不透明的三键式导航 |
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