技巧干货分享 | Android 15 默认采用无边框设计后,边衬区如何处理?

科技   2024-10-21 18:31   北京  

作者 / Ash Nohe


这篇文章是我们 "Android 15 Spotlight Week" 系列的内容。在该系列中,我们会提供各种资源,包括文章、视频、示例代码等,以帮助您为应用做好准备,并充分利用 Android 15 的最新功能。您可以查阅更多关于 Spotlight Week: Android 15 的概述


  • 查阅更多关于 "Spotlight Week: Android 15" 的概述
    https://android-developers.googleblog.com/2024/09/android-15-spotlight-week.html


Google 的一项内部用户研究显示,无论是手势导航还是 "三按钮" 导航,用户都更偏好 "无边框" 的设计而不是非无边框屏幕。
图 1. 左图: 无边框应用。该应用的背景绘制于顶部的状态栏和底部的导航栏之下。右图: 非无边框应用。该应用的背景和内容会避开状态栏和导航栏。


Android 15 默认采用无边框设计


若应用的目标 SDK 版本低于 35 (Android 15),且没有明确的代码更改以实现无边框绘制,则应用默认不会进行无边框显示。在将 SDK 设置为 targetSdk=35 或更高版本后,系统会在 Android 15 及更高版本的设备上默认将应用绘制为无边框。虽然此更改可以让许多应用更轻松地实现无边框显示,但也可能导致用户不可访问一些关键的界面元素。因此,您的应用必须进行边衬区处理,以确保关键的界面元素仍然可访问。

图 2. 以 SDK 34 为目标的应用在 Android 15 设备上运行。应用并不是无边框的。

图 3. 以 SDK 35 为目标的应用在 Android 15 设备上运行。应用实现了无边框显示,但没有处理边衬区。状态栏、导航栏和刘海屏会遮挡界面。

图 4. 以 SDK 35 为目标的应用在 Android 15 设备上运行。应用实现了无边框显示,并且处理了边衬区,以确保关键的界面元素不被遮挡。

  • 将应用绘制为无边框
    https://developer.android.google.cn/about/versions/15/behavior-changes-15#edge-to-edge

如果您的应用已经显示为无边框或处于沉浸模式,则不会受到影响。但您可以对应用进行测试,以检查是否存在任何问题。



边衬区处理技巧


您可以使用多种 API 和属性来处理边衬区,以避免系统界面和刘海屏遮挡。

本文将介绍以下适用于 Compose 和 View 的技巧:

1. 使用 Material 组件来简化边衬区的处理
2. 绘制无边框背景,并内嵌关键界面元素
3. 处理刘海屏和标题栏的边衬区
4. 切勿遗漏最后一个列表项
5. 切勿忽略 IME
6. 为了向后兼容,使用 enableEdgeToEdge 而不是 setDecorFitsSystemWindows 
7. 仅在必要时使用后台保护系统栏

我们还将介绍以下 Compose 专门技巧:

8. 使用 Scaffold 的 PaddingValues
9. 使用高阶 WindowInset API

最后,我们将介绍以下针对 View 的专门技巧:

10. 优先使用 ViewCompat.setOnApplyWindowInsetsListener 而不是 fitsSystemWindows=true
11. 根据应用栏的高度在栏布局上应用边衬区

1. 使用 Material 组件简化处理边衬区


许多 Material 组件会自动处理边衬区,这意味着组件的背景会在系统栏区域内绘制,并且会对关键界面元素进行内嵌处理 (详见技巧 #2)。但还有一些 Material 组件并不会自动处理边衬区,而是提供了方法来简化处理边衬区的过程。

自动处理边衬区的 Material 3 组件 (androidx.compose.material3) 包括:

  • BottomAppBar
  • CenterAlignedTopAppBar
  • DismissibleDrawerSheet
  • LargeTopAppBar
  • MediumTopAppBar
  • ModalBottomSheet
  • ModalDrawerSheet
  • NavigationBar
  • NavigationRail
  • NavigationSuiteScaffold
  • PermanentDrawerSheet
  • TopAppBar
  • SmallTopAppBar

Material 2 组件 (androidx.compose.material) 与 Material 3 组件不同,默认情况下不会自动处理边衬区本身。我们建议您更新到 Material 3 组件。如无法更新,许多 Material 2 组件亦支持使用 windowInsets 参数来指定要应用的窗口边衬区,如 BottomAppBarTopAppBarBottomNavigationNavigationRail。同样地,对于 Scaffold,您可以使用 contentWindowInsets 参数。如无法使用,请将边衬区应用为内边距。


  • Material 3 组件
    https://m3.material.io/blog/migrating-material-3
  • BottomAppBar
    https://link.gevents.cn/MPUUrH
  • TopAppBar
    https://link.gevents.cn/vnxAz5
  • BottomNavigation
    https://link.gevents.cn/e6tCpe
  • NavigationRail
    https://link.gevents.cn/Y3mCpa
  • Scaffold
    https://link.gevents.cn/xfTJvw


自动处理边衬区的 Material View 组件 (com.google.android.material) 包括:

  • BottomAppBar
  • BottomNavigationView
  • NavigationRailView
  • NavigationView


2. 绘制无边框背景,并内嵌关键界面元素


通常,应用会将系统栏设置为与应用栏相同的颜色,以营造一种无边框的效果,这是一种反面模式。

图 5. 以 SDK 34 为目标的应用在 Android 15 设备上运行。应用设置了状态栏和导航栏颜色,这种做法在以 SDK 35 为目标进行开发后将不再受支持。状态栏和导航栏的颜色特意设置为略有不同的色调,以便区分系统栏和应用栏,但通常应用会为系统栏和应用栏使用相同的颜色,以实现无边框的视觉效果。

使用这种反面模式的应用并不会一直显示为无边框效果,尤其是在大屏设备上。

为了在默认采用无边框设计后改进应用的用户体验,您可以延伸应用栏的背景,使其在透明的状态栏下绘制,然后将文字和按钮内嵌以避免发生系统栏遮挡。Material 3 组件 TopAppBarsBottomAppBars 会自动完成此操作。对于 Material 2 组件,您可以使用 windowInsets 参数。对于 View,一些 Material 组件会自动处理此问题。若未能自动处理,您也可以使用 fitsSystemWindowsViewCompat.setOnApplyWindowInsetsListener
图 6. 默认采用无边框设计。左图: 边衬区未处理。右图: 边衬区已处理。应用栏在透明的系统栏下方绘制,文本和图标避开系统栏。


3. 处理刘海屏和标题栏的边衬区


在处理边衬区时,除了状态栏和导航栏外,还要考虑刘海屏和标题栏。

刘海屏

刘海屏通常包含摄像头。若以 SDK 35 为目标,您的应用可能会在刘海屏中放置重要界面元素,尤其是在刘海屏位于设备的左或右边缘时。

例如,当目标 SDK 版本低于 35 时,您的应用可能会在横向模式下用一个大白方块来预留摄像头缺口的位置。
图 7. 以 SDK 34 为目标的应用在 Android 15 设备上运行。
若以 SDK 35 为目标,白色方块消失,所有内容向左移动。部分内容会绘制在摄像头缺口下方。
图 8. 以 SDK 35 为目标的应用在 Android 15 设备上运行。摄像头遮挡了羊的照片。

在 Compose 中处理刘海屏

在 Compose 中,您可以使用 WindowInsets.safeContentWindowInsets.safeDrawingWindowInsets.safeGestures 轻松处理刘海屏。或者,您可以使用 WindowInsets.displayCutout 来进行更精细的控制。
// Shows using WindowInsets.displayCutout for fine-grained control.// But, it's often sufficient and easier to instead use// WindowInsets.safeContent, WindowInsets.safeDrawing, or WindowInsets.safeGestures@Composablefun ChatRow(...) {   val layoutDirection = LocalLayoutDirection.current   val displayCutout = WindowInsets.displayCutout.asPaddingValues()   val startPadding = displayCutout.calculateStartPadding(layoutDirection)   val endPadding = displayCutout.calculateEndPadding(layoutDirection)   Row(       modifier = modifier.padding(               PaddingValues(                   top = 16.dp,                   bottom = 16.dp,                   // Ensure content is not hidden by display cutouts                   // when rotating the device.                   start = startPadding.coerceAtLeast(16.dp),                   end = endPadding.coerceAtLeast(16.dp)               )           ),       ...   ) { ... }


处理刘海屏问题后,应用如下所示:

图 9. 以 SDK 35 为目标的应用在 Android 15 设备上运行。刘海屏边衬区已处理。注意: 理想情况下,"Chats" 也应当向右添加内边距。

您可以在 "开发者选项" 屏幕下的 "刘海屏" 中测试各种刘海屏配置。

  • 开发者选项
    https://developer.android.google.cn/studio/debug/dev-options

要了解更多关于此示例的信息,请参阅 Codelab: 应对 Android 15 默认采用无边框设计

  • 应对 Android 15 默认采用无边框设计
    https://developer.android.google.cn/codelabs/edge-to-edge#4

在 View 中处理刘海屏

在 View 中,您可以使用 WindowInsetsCompat.Type.displayCutout 来处理刘海屏。有关代码示例,请参阅文档

  • 文档
    https://developer.android.google.cn/develop/ui/views/layout/edge-to-edge#cutout-insets

注意: 如果您的应用有一个非悬浮窗口 (例如,一个 Activity) 并且正在使用 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULTLAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVERLAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,从 Android 15 开始,Android 将会把这些刘海屏模式解释为 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS,这会导致您的窗口绘制到刘海屏区域。请确保测试使用 DEFAULTNEVERSHORT_EDGES 的屏幕。

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
    https://developer.android.google.cn/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
    https://developer.android.google.cn/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
    https://developer.android.google.cn/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
    https://developer.android.google.cn/reference/android/view/WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS

标题栏

当应用出现在桌面模拟器或自由形式窗口中时,应用会显示标题栏而不是状态栏。

图 10. 桌面模拟器中的标题栏。

在 Compose 中处理边衬区时,您可以使用 Scaffold 的 PaddingValuesWindowInsets.safeContentWindowInsets.safeDrawingWindowInsets.systemBars,这些项都包含标题栏。请勿使用 WindowInsets.statusBars

在应用基于 View 的布局中,请使用 WindowInsetsCompat.Type.systemBars。请勿使用 WindowInsetsCompat.Type.statusBars


4. 不要遗漏最后一个列表项


若以 SDK 35 为目标,列表的最后一项可能变得不可访问,特别是当该项被 "三按钮" 导航栏或任务栏遮挡时。

在 Compose 中处理最后一个列表项

若要处理 Compose 中的最后一个列表项,请使用 LazyColumn 的 contentPadding 来为最后一项添加空间,除非您正在使用 TextField:
Scaffold { innerPadding ->    LazyColumn(        contentPadding = innerPadding    ) {        // Content that does not contain TextField    }}

  • contentPadding
    https://developer.android.google.cn/develop/ui/compose/lists#content-padding

对于 TextField,使用 SpacerLazyColumn 中绘制最后一个 TextField。如需深入了解如何在列表中处理边衬区,请参阅 "边衬区消耗"
LazyColumn(    Modifier.imePadding()) {    // Content with TextField    item {        Spacer(            Modifier.windowInsetsBottomHeight(                WindowInsets.systemBars            )        )    }}

  • 边衬区消耗
    https://developer.android.google.cn/develop/ui/compose/layouts/insets#inset-consumption

在 View 中处理最后一个列表项

使用 RecyclerViewNestedScrollView 时,添加 clipToPadding="false" 可以确保最后一个列表项显示在导航栏之上。以此应用为例。当以 SDK 35 为目标时,应用的列表界面会在导航栏下方显示最后一个列表项。

图 11. 使用 RecyclerView 的应用。左图: 以 SDK 34 为目标。最后一个列表项显示在 "三按钮" 导航栏之上。右图: 以 SDK 35 为目标。最后一个列表项绘制在 "三按钮" 导航栏之下。

我们可以使用窗口边衬区监听器,以便所有列表项 (包括最后一个列表项) 都填充在导航栏之上。
图 12. 应用处理了边衬区,但由于内容没有在系统栏后方滚动,体验显得沉浸感不足。

// Figure 12ViewCompat.setOnApplyWindowInsetsListener(    findViewById(R.id.recycler_view)) { v, insets ->    val innerPadding = insets.getInsets(        // Notice we're using systemBars, not statusBar        WindowInsetsCompat.Type.systemBars()        // Notice we're also accounting for the display cutouts        or WindowInsetsCompat.Type.displayCutout()        // If using EditText, also add         // "or WindowInsetsCompat.Type.ime()"        // to maintain focus when opening the IME    )    v.setPadding(        innerPadding.left,        innerPadding.top,        innerPadding.right,        innerPadding.bottom)    insets}

但是,应用现在看起来沉浸感不足。为了达到我们想要的效果,我们需要添加 clipToPadding=false 以确保最后一个列表项位于导航栏之上,并且列表在滚动时能够显示在导航栏 (和状态栏) 后方。
图 13. 应用显示为无边框,且最后一个列表项完全可见。这达到了预期的效果。

<!-- Figure 13 --><RecyclerView    ...    android:clipToPadding="false" />


5. 不要忽略 IME


在 Activity 的 AndroidManifest.xml 条目中设置 android:windowSoftInputMode="adjustResize",为屏幕上的 IME (软键盘) 留出空间。


  • android:windowSoftInputMode="adjustResize"
    https://developer.android.google.cn/guide/topics/manifest/activity-element#wsoft

在 Compose 中处理 IME 边衬区

使用 Modifier.imePadding() 来处理 IME。例如,这有助于在 IME 打开时保持对 LazyColumnTextField 的焦点。有关代码示例和解释,请参阅 "边衬区消耗部分。


  • Modifier.imePadding()
    https://developer.android.google.cn/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).imePadding()
  • 边衬区消耗
    https://developer.android.google.cn/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).imePadding()

在 View 中处理 IME 边衬区

若目标 SDK 版本低于 35,使用 android:windowSoftInputMode="adjustResize" 就足以在打开 IME 时保持焦点,例如对 RecyclerViewEditText 的焦点。使用 "adjustResize" 时,框架会将 IME 视为系统窗口,并且会调整窗口的根视图的内边距,以避免内容被系统窗口遮挡。

若以 SDK 35 为目标,您还必须使用 ViewCompat.setOnApplyWindowInsetsListenerWindowInsetsCompat.Type.ime() 来处理 IME,因为框架将不再调整窗口的根视图的内边距。详情请参阅图 12 中的代码示例。


  • WindowInsetsCompat.Type.ime()
    https://developer.android.google.cn/reference/androidx/core/view/WindowInsetsCompat.Type#ime()


6. 为了向后兼容,请使用 

enableEdgeToEdge 而不是 

setDecorFitsSystemWindows


在您的应用处理了边衬区之后,为了让应用在之前的 Android 版本上也能实现无边框显示,您应使用 enableEdgeToEdge 而不是 setDecorFitsSystemWindowsenableEdgeToEdge 方法封装了大约 100 行代码,以确保真正的向后兼容。


  • enableEdgeToEdge
    https://developer.android.google.cn/develop/ui/views/layout/edge-to-edge#enable-edge-to-edge-display
  • setDecorFitsSystemWindows
    https://developer.android.google.cn/develop/ui/views/layout/edge-to-edge-manually
  • 100 行代码
    https://medium.com/androiddevelopers/is-your-app-providing-a-backward-compatible-edge-to-edge-experience-2479267073a0


7. 仅在必要时使用后台保护系统栏


在大多数情况下,请保持新的 Android 15 默认设置。当以 SDK 35 为目标时,状态栏和手势导航栏应该是透明的,"三按钮" 导航栏应该是半透明的 (详见图 1)。

然而,在某些情况下,您可能希望保留系统栏的背景颜色,但设置状态栏和导航栏颜色的 API 已弃用。我们计划发布一个 AndroidX 库来支持这种用例。在此期间,如果您的应用必须为 "三按钮" 导航栏或状态栏提供自定义的背景保护,您可以使用 WindowInsets.Type#tappableElement() 来获取 "三按钮" 导航栏的高度,或者使用 WindowInsets.Type#statusBars,进而在系统栏后面放置一个可组合项或视图。

  • 已弃用
    https://developer.android.google.cn/about/versions/15/behavior-changes-15#deprecated-apis

例如,要在 Compose 中显示 "三按钮" 导航后面的元素颜色,请将 window.isNavigationBarContrastEnforced 属性设置为 false。将此属性设置为 false 会使 "三按钮" 导航完全透明 (注意: 此属性不影响手势导航)。

然后,使用 WindowInsets.tappableElement 来对齐界面元素到可点按系统界面的边衬区后面。如果检测到非零值,则表示用户正在使用可点按系统栏,如 "三按钮" 导航。在这种情况下,在可点按系统栏后面绘制一个不透明的视图或框。
class MainActivity : ComponentActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContent {            window.isNavigationBarContrastEnforced = false            MyTheme {                Surface(...) {                    MyContent(...)                    ProtectNavigationBar()                }            }        }    }}

// Use only if required.@Composablefun 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) ) } }}

  • WindowInsets.tappableElement 
    https://developer.android.google.cn/reference/kotlin/androidx/compose/foundation/layout/WindowInsets.Companion#(androidx.compose.foundation.layout.WindowInsets.Companion).tappableElement()



适用于使用 Compose 界面的

边衬区处理技巧


以下技巧仅适用于使用 Jetpack Compose 的应用。如需了解更多与 Compose 相关的技巧,请参阅视频: 无边框和边衬区 | Compose 技巧

  • Jetpack Compose
    https://developer.android.google.cn/jetpack/compose
  • 无边框和边衬区 | Compose 技巧
    https://www.youtube.com/watch?v=QRzepC9gHj4


8. 使用 Scaffold 的 PaddingValues


对于 Compose,请使用 Scaffold 而不是 Surface 来整理您的应用界面,包括 TopAppBarBottomAppBarNavigationBarNavigationRail。使用 Scaffold 的 PaddingValues 参数来内嵌关键界面。通常情况下,这样做就能够满足大部分用例的需求。


  • Scaffold
    https://developer.android.google.cn/develop/ui/compose/layouts/insets#scaffold

但是,在某些情况下,应用 Scaffold 的 PaddingValues 可能会导致意外结果。Scaffold 的 PaddingValues 包含了屏幕顶部、底部、起始边和结束边的边衬区。如果您只需要某些特定边的值,一种方法是复制这个参数,并手动调整顶部、底部、起始边和结束边的边衬区,以避免应用不必要的内边距。

图 14. 左图: 当以 SDK 35 为目标时,底部的输入字段被系统的导航栏遮挡。中间: 使用了 Scaffold 的 PaddingValues 来处理输入字段。系统根据状态栏和顶部应用栏的大小来计算顶部内边距值,这导致输入字段上方产生了多余的内边距。右图: 使用了 Scaffold 的 PaddingValues,但手动去除了顶部的内边距。

以下是导致图 14 中间图像出现多余内边距的错误代码。
// Causes excess padding, seen in the middle image of Figure 14.Scaffold { innerPadding -> // innerPadding is Scaffold's PaddingValues    InputBar(        ...        contentPadding = innerPadding     ) {...}}
以下是修正后的代码,可生成如图 14 右图中所示的正确内边距。
// Function to make a copy of PaddingValues, using existing defaults unless an// alternative value is specifiedprivate fun PaddingValues.copy(    layoutDirection: LayoutDirection,    start: Dp? = null,    top: Dp? = null,    end: Dp? = null,    bottom: Dp? = null,) = PaddingValues(    start = start ?: calculateStartPadding(layoutDirection),    top = top ?: calculateTopPadding(),    end = end ?: calculateEndPadding(layoutDirection),    bottom = bottom ?: calculateBottomPadding(),)
// Produces correct padding, seen in the right-side image of Figure 14.Scaffold { innerPadding -> // innerPadding is Scaffold's PaddingValues val layoutDirection = LocalLayoutDirection.current InputBar( ... contentPadding = innerPadding.copy(layoutDirection, top = 0.dp) ) {...}}


9. 使用高阶 WindowInsets API


类似于 Scaffold 的 PaddingValues,您也可以使用高阶 WindowInset API 来轻松且安全地绘制关键界面元素。这些 API 包括:

  • WindowInsets.safeDrawing
    https://developer.android.google.cn/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.foundation.layout.WindowInsets.Companion).safeDrawing()
  • WindowInsets.safeGestures
    https://developer.android.google.cn/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.foundation.layout.WindowInsets.Companion).safeGestures()
  • WindowInsets.safeContent
    https://developer.android.google.cn/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.foundation.layout.WindowInsets.Companion).safeContent()

详情请参阅 "边衬区基础知识"

  • 边衬区基础知识
    https://developer.android.google.cn/develop/ui/compose/layouts/insets


适用于使用 View 界面的

边衬区处理技巧


以下内容仅适用于基于 View 的应用。


10. 优先使用 ViewCompat.setOn

ApplyWindowInsetsListener

而不是 fitsSystemWindows=true


可以使用 fitsSystemWindows=true 来内嵌应用的内容。这是一个简单的单行代码更改。但是,请勿在包含整个布局 (包括背景) 的 View 上使用 fitsSystemWindows。这会使应用看起来不是无边框的,因为 fitsSystemWindows 会在所有边缘处理边衬区。

图 15. 默认采用无边框设计。左图: fitsSystemWindows=false (默认)。右图: fitsSystemWindows=true。这不是我们期望的结果,因为应用看起来不是无边框的。

图 16. 默认采用无边框设计,fitsSystemWindows=true。左侧边缘的间隙是由于刘海屏造成的,这里没有显示出来。这不是我们期望的结果,因为应用看起来不是无边框的。

当使用 CoordinatorLayoutsAppBarLayouts 时,fitsSystemWindows 可以创建无边框体验。将 fitsSystemWindows 添加到 CoordinatorLayoutAppBarLayout 上,这样一来 AppBarLayout 就能实现无边框绘制,从而达到预期的效果。
图 17. 默认采用无边框设计。左图: AppBarLayout 不会自动处理边衬区。右图: 将 fitsSystemWindows 添加到 AppBarLayout 和 CoordinatorLayout 以实现无边框绘制。
<!-- Figure 17 --><CoordinatorLayout    android:fitsSystemWindows="true"    ...>    <AppBarLayout        android:fitsSystemWindows="true"        ...>        <TextView            android:text="App Bar Layout"            .../>    </AppBarLayout></CoordinatorLayout>


在这种情况下,AppBarLayout 使用 fitsSystemWindows 将内容绘制在状态栏下方,而不是避开状态栏,这与我们的预期相反。此外,设置了 fitsSystemWindows=trueAppBarLayout 仅对屏幕顶部应用内边距,而不对底部、起始边或结束边应用内边距。

覆盖 fitsSystemWindows 时,CoordinatorLayoutAppBarLayout 对象具有以下行为:

  • CoordinatorLayout: 如果子视图也设置了 fitsSystemWindows=true,则这些视图的背景会绘制在系统栏下方。系统会自动为这些 View 的内容 (如文本、图标、图片) 应用内边距,以适应系统栏和刘海屏。
  • AppBarLayout: 如果设置了 fitsSystemWindows=true,则系统会将内容绘制在系统栏下方,并自动为内容应用顶部内边距。

在大多数情况下,请使用 ViewCompat.setOnApplyWindowInsetsListener 来处理边衬区,因为它允许您定义哪些边缘需要处理边衬区,并且具有稳定一致的行为。详情请参阅技巧 #4 和 #11 中的代码示例。

11. 在布局阶段根据

应用栏的高度应用边衬区


如果您发现应用的内容被应用栏遮挡了,则可能需要在应用栏完成布局后再应用边衬区,并且要考虑到应用栏的高度。

例如,如果您在 FrameLayout 中有一个位于 AppBarLayout 下方的滚动内容,则您可以使用类似下面的代码来确保滚动内容在 AppBarLayout 布局完成后显示。请注意,内边距是在 doOnLayout 中应用的。
val myScrollView = findViewById<NestedScrollView>(R.id.my_scroll_view)val myAppBar = findViewById<AppBarLayout>(R.id.my_app_bar_layout)

ViewCompat.setOnApplyWindowInsetsListener(myScrollView) { scrollView, windowInsets -> val insets = windowInsets.getInsets( WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() ) myAppBar.doOnLayout { appBar -> scrollView.updatePadding( left = insets.left, right = insets.right, top = appBar.height, bottom = insets.bottom ) } WindowInsetsCompat.CONSUMED}
同样地,如果您的滚动内容需要显示在 BottomNavigationView 上,您需要在 BottomNavigationView 布局完成后考虑其高度。


需要更多时间进行迁移?


适配无边框体验可能需要大量的工作。在您以 SDK 35 为目标进行开发时,请考虑一下您需要多长时间来进行应用中的必要更改。

如果您需要更多时间来处理边衬区以兼容系统的默认无边框行为,您可以使用 R.attr#windowOptOutEdgeToEdgeEnforcement 以暂时不采用无边框显示。但请不要长期依赖这个标志,因为该标志即将失效。

对于包含数十个到数百个 Activity 的应用,该标记可能特别有用。您可以先逐个对 Activity 进行非无边框设置,然后一步步地让应用中的每个 Activity 实现无边框显示。

以下是使用此标记的一种方法。假设您的 minSDK 版本小于 35,这个属性必须放在 values-v35.xml 文件中。
<!-- In values-v35.xml --><resources>    <!-- TODO: Remove once activities handle insets. -->    <style name="OptOutEdgeToEdgeEnforcement">        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item></style></resources>
values.xml 中为过去版本创建一个空样式:
<!-- In values.xml --><resources>    <!-- TODO: Remove once activities handle insets. -->    <style name="OptOutEdgeToEdgeEnforcement">        <!-- android:windowOptOutEdgeToEdgeEnforcement             isn't supported before SDK 35. This empty             style enables programmatically opting-out. --></style></resources>
在访问 setContentView 中的装饰视图之前调用样式:
class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {
// Call before the DecorView is accessed in setContentView theme.applyStyle(R.style.OptOutEdgeToEdgeEnforcement, /* force */ false)
super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ... }}


资源


随着 Android 15 AOSP 的发布,我们准备了一系列文章、视频和 Codelab,帮助您让应用适应 Android 15 的无边框设计要求。以下是可供您进一步学习的资源列表。

  • Android 15 的无边框设计要求
    https://developer.android.google.cn/about/versions/15/behavior-changes-15#edge-to-edge



文档


  • 以 Android 15 为目标平台的应用的行为变更: 默认采用无边框设计
    https://developer.android.google.cn/about/versions/15/behavior-changes-15#edge-to-edge
  • Compose 中的窗口边衬区
    https://developer.android.google.cn/develop/ui/compose/layouts/insets
  • Compose 中的刘海屏
    https://developer.android.google.cn/develop/ui/compose/system/cutouts
  • 在应用中显示无边框内容 (View)
    https://developer.android.google.cn/develop/ui/views/layout/edge-to-edge
  • 支持刘海屏 (View)
    https://developer.android.google.cn/develop/ui/views/layout/display-cutout


视频


  • 无边框和边衬区 (Compose)
    https://www.youtube.com/watch?v=QRzepC9gHj4
  • 边衬区: Compose 版本
    https://www.youtube.com/watch?v=mlL6H-s0nF0
  • 演示视频: 提升 Android 应用用户体验的 3 件事: 应对 Android 15 中的无边框设计要求 (Compose)
    https://m.youtube.com/watch?v=RimGfoOU67s



Codelab


  • 应对 Android 15 的无边框设计要求 (Compose)
    https://developer.android.google.cn/codelabs/edge-to-edge#0




谷歌开发者特别招募活动进行中

诚邀热爱技术的你加入


通过多种形式 (文章/视频/coding 等) 创作与 Google 技术相关的讲解分享、实践案例或活动感受等内容,以及分享您应用 AI 技术的故事经历与成果。我们将为您提供平台和资源,助力您在分享中提升技能。更有惊喜权益等您领取,快来报名参与吧!





 点击屏末  | 了解有关无边框设计的更多信息

谷歌开发者
Google 中国官方账号。汇集 Android, Flutter, Chrome OS, Chrome/web, AI 等开发技术,以及 Google Play 平台出海相关信息。
 最新文章