自适应布局

1. 准备工作

Android 设备的形状、尺寸和外形规格多种多样。您应设计能够在所有这些不同类型、屏幕尺寸各异的设备上运行的应用。开发者在编写可直接投入使用的应用时,可以支持 Android WearAndroid AutoAndroid TV,但这些主题不在本课程的讨论范围之内。您的应用支持的屏幕种类越多,便可供越多具有不同设备的用户使用。

您的应用必须具有灵活的布局。您定义的布局不应采用硬性尺寸(也就是不应假定采用特定的宽高比和屏幕尺寸),而是应能够很好地适应各种屏幕尺寸和方向。当您的应用在可折叠设备上运行时,此原则同样适用,因为屏幕尺寸和宽高比可能会在应用运行时发生变化。在此 Codelab 的最后,我们将对可折叠设备进行简要介绍。

aecb59fc49fb4abf.png

前提条件

  • 了解如何将代码下载到 Android Studio 并运行代码。
  • 熟悉 Android 架构组件 ViewModelLiveData
  • 具备 Navigation 组件方面的基础知识。

学习内容

  • 如何为应用添加 SlidingPaneLayout

构建内容

  • 更新 Sports 应用,使其适应大屏幕。

所需条件

  • 一台安装了 Android Studio 的计算机。
  • Sports 应用的起始代码。

下载此 Codelab 的起始代码

此 Codelab 提供了起始代码,供您使用此 Codelab 中所教的功能对其进行扩展。起始代码可能既包含您在之前的 Codelab 中已熟悉的代码,也包含您不熟悉并将在后续 Codelab 中了解的代码。

如需从 GitHub 获取此 Codelab 的代码并在 Android Studio 中打开它,请按以下步骤操作。

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Get from VCS

61c42d01719e5b6d.png

  1. Get from Version Control 对话框中,确保为 Version Control 选择 Git

9284cfbe17219bbb.png

  1. 将提供的代码网址粘贴到 URL 框中。
  2. (可选)将 Directory 从建议的默认值更改为其他目录。

5ddca7dd0d914255.png

  1. 点击 Clone。Android Studio 开始提取代码。
  2. 等待 Android Studio 打开项目。
  3. 针对 Codelab 起始代码、应用或解决方案代码选择正确的模块。

2919fe3e0c79d762.png

  1. 点击 Run 按钮 8de56cba7583251f.png 以构建并运行代码。

2. 观看配套代码演示视频(可选)

如果您想要观看某位课程讲师完成此 Codelab 的过程,请播放以下视频。

建议将视频全屏展开(使用视频右下角的 此符号突出显示了方形的 4 个角,表示全屏模式。 图标),以便更清楚地查看 Android Studio 和相关代码。

这是可选步骤。您也可以跳过视频,立即开始按照此 Codelab 中的说明操作。

3. 起始应用概览

Sports 应用由两个画面组成。第一个画面显示体育项目列表。用户可以选择特定体育项目,然后系统会显示第二个画面。第二个画面是一个详情画面,其中会显示所选体育项目的新闻。为了简化实现,详情画面将显示占位文本。

起始代码演示

您下载的起始代码已为您预先设计了列表画面布局和详情画面布局。在本衔接课程中,您只需专注于使应用适应大屏幕即可。您将使用 SlidingPaneLayout 来充分利用大屏幕。下面简要介绍了一些文件,以帮助您上手。

fragment_sports_list.xml

  • Design 视图中,打开 res/layout/fragment_sports_list.xml
  • 此文件包含应用中第一个画面(即体育项目列表)的布局。
  • 该布局由一个显示体育新闻列表的 Recyclerview 组成。

f50d3e7b41fcb338.png

d9af155f87ddbcdf.png

sports_list_item.xml

  • Design 视图中,打开 res/layout/sports_list_item.xml
  • 此文件包含 Recyclerview 中每一项的布局。
  • 该布局由以下内容组成:体育项目的缩略图、新闻标题以及简要体育新闻的占位文本。

b19fd0e779c1d7c3.png

fragment_sports_news.xml

  • Design 视图中,打开 res/layout/fragment_sports_news.xml
  • 此文件包含应用中第二个画面的布局。当用户从 Recyclerview 中选择了某个体育项目时,系统就会显示这个画面。
  • 该布局由以下内容组成:体育项目图片横幅以及体育新闻的占位文本。

c2073b1752342d97.png

main_activity.xml 和 content_main.xml

这两个文件定义了只有一个 fragment 的主 activity 布局。

导航图包含两个目的地,一个对应于体育项目列表,另一个对应于体育新闻。

res/values 文件夹

此文件夹中是一些您非常熟悉的资源文件。

  • colors.xml 包含应用中使用的主题颜色。
  • strings.xml 包含应用所需的全部字符串。
  • themes.xml 包含为应用进行的界面自定义。

MainActivity.kt

此文件包含默认模板生成的代码,用于将 activity 的内容视图设为 main_activity.xml。我们替换了 onSupportNavigateUp() 方法,以便处理应用栏中的默认向上导航。

model/Sport.kt

这是一个数据类,其中包含要在体育项目列表 Recyclerview 的每一行中显示的数据。

data/SportsData.kt

此文件包含一个称为 getSportsData() 的函数,该函数可返回一个预先填充了硬编码体育项目数据的 ArrayList

SportsViewModel.kt

这是应用的共享 ViewModelViewModelSportsListFragment(显示体育项目列表的第一个画面)和 NewsDetailsFragment(显示详细体育新闻的第二个画面)共享。

  • _currentSport 属性的类型为 MutableLiveData, 用于存储用户当前选择的体育项目。currentSport 属性是 _currentSport 的后备属性,并且已作为公共只读版本公开,以供其他类使用。
  • _sportsData 属性包含体育项目数据列表。与上一个属性类似,sportsData 是此属性的公共只读版本。
  • 初始化程序 init{} 块用于初始化 _currentSport_sportsData。它会使用 data/SportsData.kt 中的整个体育项目列表初始化 _sportsData,并使用列表中的第一项初始化 _currentSport
  • updateCurrentSport() 函数可接收 Sports 实例,并使用传入的值更新 _currentSport

SportsAdapter.kt

这是 RecyclerView 的适配器。在构造函数中,会传入点击监听器。此文件中的大部分代码都是您在之前的 Codelab 中已经熟悉的样板代码。

SportsListFragment.kt

这是第一个画面 fragment,其中会显示体育项目列表。

  • onCreateView() 函数使用绑定对象膨胀 fragment_sports_list 布局 XML 文件。
  • onViewCreated() 函数可设置 RecyclerView 适配器。它会将用户选择的体育项目更新为共享 ViewModel(即 SportsViewModel)中的当前体育项目。此外,它会导航到包含体育新闻的详情画面,并使用 submitList(List) 将体育项目列表提交给适配器以供显示。

NewsDetailsFragment.kt

这是应用中的第二个画面,其中会显示体育新闻的占位文本。

  • onCreateView() 函数使用绑定对象膨胀 fragment_sports_news 布局 XML 文件。
  • onViewCreated() 函数会在 SportsViewModelcurrentSport 属性上附加一个观察器,以便在数据发生变化时自动更新界面。在观察器内部,体育项目标题、图片和新闻会保持最新状态。

构建并运行应用

  1. 构建应用并在模拟器或设备上运行应用。从体育项目列表中选择任何一项,应用应该会转到第二个画面,其中包含新闻的占位文本。

4. “列表-详情”模式

当前的起始应用无法在平板电脑等大屏幕设备上充分利用屏幕空间。为了解决这个问题,您将使用“列表-详情”模式显示应用界面,我们将在此 Codelab 中对这种模式进行介绍。

在平板电脑上运行应用

在此任务中,您将创建一个具有平板电脑配置文件的模拟器。创建模拟器后,您将运行 Sports 应用起始代码并观察界面。

  1. 在 Android Studio 中,转到 Tools > AVD Manager
  2. 系统随即会显示 Android Virtual Device Manager 窗口。点击底部显示的 + Create New Virtual Device...
  3. 系统随即会显示 Virtual Device Configuration 窗口。您将在此窗口中配置模拟器硬件和操作系统。在左侧窗格中点击 Tablet。在中间窗格中选择 Pixel C 或任何其他类似硬件配置文件。

8303f9b3e70321eb.png

  1. 点击 Next
  2. 选择最新的系统映像,在编写此 Codelab 时,最新版本为 R(API 级别为 30)。
  3. 点击 Next
  4. 现在,您可以重命名虚拟设备(可选)。
  5. 点击 Finish
  6. 您将返回到 Android Virtual Device Manager 窗口。点击新创建的虚拟设备旁边的启动图标 38752506de85d293.png
  7. 具有平板电脑配置文件的模拟器应该会启动。不过这可能需要一点时间,请耐心等待。
  8. 关闭 Android Virtual Device Manager 窗口。
  9. 在新创建的模拟器上运行 Sports 应用。

200e209de7a2f0ad.png

请注意,在大屏幕设备上,该应用不会占用整个屏幕。在大屏幕设备上,使用“列表-详情”模式比使用列表更高效。“项目-详情”模式(也称为“主要-详情”模式)会在布局的一侧显示一个项目列表,当您点按其中的任一项时,相应项的详情便会显示在其旁边。通常情况下,只有在平板电脑等大屏幕设备上,这些视图才会显示,因为这些设备具有更多空间来显示更多内容。

以下图片是“列表-详情”模式的示例:

71698910dd129a91.png

在上面的“列表-详情”模式中,左侧显示的是项目列表,右侧显示的是所选项目的详情。

同样,如果您在 Sports 应用中使用上面的模式,新闻 fragment 将是详情画面。

51c9542717d2f875.png

在此 Codelab 中,您将学习如何使用 SlidingPaneLayout 实现“列表-详情”界面。

5. SlidingPaneLayout 模式

“列表-详情”界面可能需要根据屏幕尺寸采用不同的行为方式。在大屏幕设备上,有足够的空间并排显示列表窗格和详情窗格。点击列表中的项目后,详情窗格中便会显示其详细信息。不过,在小屏幕设备上,显示两个窗格看起来会比较拥挤。因此,最好一次显示一个窗格,而不是同时显示两个窗格。在一开始,列表窗格会填满整个屏幕。点按其中的项目会使相应项目的详情窗格取代列表窗格,该窗格也会填满整个屏幕。

您将了解如何使用 SlidingPaneLayout 来管理关于根据当前屏幕尺寸选择合适用户体验的逻辑。

b0a205de3494e95d.gif

请注意在小屏幕设备上,详情窗格是如何滑动覆盖列表窗格的。

以下图片展示了 SlidingPaneLayout 在小屏幕设备上的显示方式。请注意观察,当列表中的项目被选中时,详情窗格如何与列表窗格重叠。因此,这两个窗格始终存在!

e26f94d9579b6121.png

471b0b38d4dfa95a.png

因此,SlidingPaneLayout 支持在大屏幕设备上并排显示两个窗格,同时还会自动进行调整,以便在手机等小屏幕设备上一次只显示一个窗格。

6. 添加库依赖项

  1. 打开 build.gradle (Module: Sports.app)
  2. dependencies 部分添加以下依赖项,以便在应用中使用 SlidingPaneLayout
dependencies {
...
    implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01"
}

7. 配置体育项目列表 fragment xml

在此任务中,您要将 fragment_sports_list 的根布局转换为 SlidingPaneLayout。正如您已经了解到的,SlidingPaneLayout 提供了一个水平的双窗格布局,以便在界面的顶层使用。在这种布局中,第一个窗格用作内容列表或浏览器,从属于另一个窗格中用于显示内容的主要详情视图。

在 Sports 应用中,第一个窗格是显示体育项目列表的 RecyclerView,第二个窗格则显示体育新闻。

添加 SlidingPaneLayout

  1. 打开 fragment_sports_list.xml。请注意,根布局是 FrameLayout
  2. FrameLayout 更改为 androidx.slidingpanelayout.widget.SlidingPaneLayout.
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".SportsListFragment">

   <androidx.recyclerview.widget.RecyclerView...>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
  1. SlidingPaneLayout 添加 android:id 属性,并将其值设为 @+id/sliding_pane_layout
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   ...
   android:id="@+id/sliding_pane_layout"
   ...>

为 SlidingPaneLayout 添加第二个窗格

在此任务中,您将为 SlidingPaneLayout 添加第二个子级。该子级将显示为右侧内容窗格。

  1. fragment_sports_list.xml 中的 RecyclerView 下方,添加第二个子级,即 androidx.fragment.app.FragmentContainerView
  2. FragmentContainerView 添加必需的属性 layout_heightlayout_width。将它们的值设为 match_parent。请注意,稍后您将更新这些值。
<androidx.fragment.app.FragmentContainerView
   android:layout_height="match_parent"
   android:layout_width="match_parent"/>
  1. FragmentContainerView 添加 android:id 属性,并将其值设为 @+id/detail_container
android:id="@+id/detail_container"
  1. 使用 android:name 属性为 FragmentContainerView 添加 NewsDetailsFragment
android:name="com.example.android.sports.NewsDetailsFragment"

更新 layout_width 属性

SlidingPaneLayout 会根据两个窗格的宽度来确定是否并排显示这些窗格。例如,如果测量后发现列表窗格的最小尺寸为 300dp,而详情窗格需要 400dp,那么只要可用宽度不小于 700dpSlidingPaneLayout 就会自动并排显示两个窗格。

如果子视图的总宽度超过了 SlidingPaneLayout 中的可用宽度,这些视图就会重叠。在这种情况下,子视图会展开,填满 SlidingPaneLayout 中的可用宽度。

为了确定子视图的宽度,您应了解一些关于设备屏幕宽度的基本信息。下表列出了一些主观的断点,以便您针对可调整大小的应用布局进行设计、开发和测试。这些断点是我们专门选择的,目的是平衡布局简单性与灵活性,以便针对独特情形优化您的应用。

宽度

断点

设备占比

较小宽度

小于 600dp

99.96% 的手机处于竖屏模式

中等宽度

600dp+

93.73% 的平板电脑在采用 portraitLarge 模式时,其展开的内屏处于竖屏模式

较大宽度

840dp+

97.22% 的平板电脑在采用 landscapeLarge 模式时,其展开的内屏处于横屏模式

a247a843310d061a.png

Sports 应用中,您希望在手机上显示单个窗格(体育项目列表),这适用于宽度小于 600dp 的设备。如需在平板电脑上显示两个窗格,总宽度应大于 840dp。您可以为第一个子视图(recycler 视图)使用宽度 550dp,为第二个子视图 (FragmentContainerView) 使用 300dp

  1. fragment_sports_list.xml 中,将 RecyclerView 的布局宽度更改为 550dp,并将 FragmentContainerView 的布局宽度更改为 300dp
<androidx.recyclerview.widget.RecyclerView
   ...
   android:layout_width="550dp"
   .../>

<androidx.fragment.app.FragmentContainerView
   ...
   android:layout_width="300dp"
   .../>
  1. 在具有平板电脑配置文件的模拟器以及具有手机配置文件的模拟器上运行应用。

ad148a96d7487e66.png

请注意,在平板电脑上会显示两个窗格。您将在后续步骤中修改第二个窗格在平板电脑上的宽度。

  1. 在具有电话配置文件的模拟器上运行应用。

a6be6d199d2975ac.png

添加 layout_weight

在此任务中,您将针对平板电脑修改界面尺寸,使第二个窗格占据整个剩余空间。

SlidingPaneLayout 支持在子视图上使用布局参数 layout_weight 进行测量后,就如何划分剩余空间(如果视图不重叠)进行定义。此参数仅适用于宽度。

  1. fragment_sports_list.xml 中,为 FragmentContainerView 添加 layout_weight,并将其值设为 1。现在,在测量列表窗格后,第二个窗格会展开,以填满剩余空间。
android:layout_weight="1"
  1. 运行应用。

ce3a93fe501ee5dc.png

恭喜!您已成功添加 SlidingPaneLayout。不过,还没有大功告成呢。您必须实现返回导航,并在用户从列表中选择项目后更新第二个窗格。您将在后面的任务中实现这些操作。

8. 更换详情窗格

在具有平板电脑配置文件的模拟器上运行应用。从体育项目列表中选择一个列表项。请注意,应用会进入详情窗格。

8fedee8d4837909.png

在此任务中,您将解决这个问题。目前,应用会使用所选体育项目来更新双窗格内容,然后应用会进入 NewsDetailsFragment

  1. SportsListFragment 文件内的 onViewCreated() 函数中找到以下几行代码,这些代码用于进入详情画面。
// Navigate to the details screen
val action = SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
  1. 将上面的代码替换为以下代码:
binding.slidingPaneLayout.openPane()

SlidingPaneLayout 调用 openPane(),使第二个窗格显示在第一个窗格前方。如果两个窗格都可见(例如在平板电脑上),该操作将不会产生任何可见的影响。

  1. 在平板电脑和手机模拟器上运行应用。请注意,双窗格内容会正确更新。

b0d3c8c263be15f8.png

在下一个任务中,您将为应用添加自定义返回导航功能。

9. 添加自定义返回导航

对于列表窗格与详情窗格重叠的小屏幕设备,您应确保用户可通过系统返回按钮从详情窗格返回到列表窗格。为此,您可以提供自定义返回导航,并将 OnBackPressedCallbackSlidingPaneLayout 的当前状态相关联。

返回导航

返回导航是指用户通过先前访问过的画面历史记录往回导航。所有 Android 设备都为此类导航提供了返回按钮。此按钮可能是实体按钮,也可能是软件按钮,具体取决于用户的 Android 设备。

自定义返回导航

用户在您的应用中导航时,Android 会保留一个由各个目的地组成的返回堆栈。这样,当用户按返回按钮时,Android 通常可以正确地导航到之前的目的地。不过,在少数情况下,您的应用可能需要实现自己的“返回”行为,以便尽可能提供最佳用户体验。

例如,在使用 Chrome 浏览器等 WebView 时,您可能需要替换默认的返回按钮行为,以允许用户通过其网页浏览记录(而非在您的应用中访问过的画面)往回导航。

同样,您需要提供前往 SlidingPaneLayout 的自定义返回导航,并使应用从详情窗格导航回列表窗格。

实现自定义返回导航

如需在 Sports 应用中实现自定义返回导航,您需要:

  • 定义一个自定义回调来处理返回键按下操作,该回调将替换 OnBackPressedCallback
  • 注册并添加回调实例。

首先,定义自定义回调。

  1. SportsListFragment 文件中的 SportsListFragment 类定义下方添加一个新类。将其命名为 SportsListOnBackPressedCallback
  2. 传入 SlidingPaneLayoutprivate 实例作为构造函数参数。
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
)
  1. OnBackPressedCallback 扩展该类。OnBackPressedCallback 类负责处理 onBackPressed 回调。您很快将解决构造函数参数错误。
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback()

OnBackPressedCallback 的构造函数利用布尔值指示初始启用状态。仅当回调处于启用状态时,即 isEnabled() 返回 true 时,调度程序才会调用回调的 handleOnBackPressed() 来处理返回按钮事件。

  1. slidingPaneLayout.isSlideable* && slidingPaneLayout.isOpen* 作为构造函数参数传入到 OnBackPressedCallback仅当第二个窗格可滑动时,布尔值 isSlideable 才会为 true,这种情况会出现在较小的屏幕上,且只会显示一个窗格。如果第二个窗格(内容窗格)完全打开,isOpen 的值将为 true
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen)

此代码可确保,仅在小屏幕设备上以及内容窗格处于打开状态时,才会启用回调。

  1. 如需更正未实现的方法带来的错误,请点击红色灯泡 5fdf362480bfe665.png,然后选择 Implement members
  2. Implement members 弹出式窗口中点击“ok”,以替换 handleOnBackPressed 方法。

您的类应如下所示:

class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen) {
   /**
    * Callback for handling the [OnBackPressedDispatcher.onBackPressed] event.
    */
   override fun handleOnBackPressed() {
       TODO("Not yet implemented")
   }
}
  1. handleOnBackPressed() 函数内,删除 TODO 语句,并添加以下代码,以关闭内容窗格并返回到列表窗格。
slidingPaneLayout.closePane()

监控 SlidingPaneLayout 的事件

除了处理返回按钮按下事件外,您还必须监听和监控与滑动窗格相关的事件。在内容窗格滑动时,应相应地启用或停用回调。您将使用 PanelSlideListener 来实现这一点。

接口 SlidingPaneLayout.PanelSlideListener 包含三个抽象方法,即 onPanelSlide()onPanelOpened()onPanelClosed()。在详情窗格滑动、打开和关闭时,系统会调用这些方法。

  1. SlidingPaneLayout.PanelSlideListener 扩展 SportsListOnBackPressedCallback 类。
  2. 为了解决该错误,请实现上面的三个方法。点击红色灯泡,然后在 Android Studio 中选择 Apply members

ad52135eecbee09f.png

  1. 您的 SportsListOnBackPressedCallback 类应与以下代码类似:
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
  SlidingPaneLayout.PanelSlideListener{

   override fun handleOnBackPressed() {
       slidingPaneLayout.closePane()
   }

   override fun onPanelSlide(panel: View, slideOffset: Float) {
       TODO("Not yet implemented")
   }

   override fun onPanelOpened(panel: View) {
       TODO("Not yet implemented")
   }

   override fun onPanelClosed(panel: View) {
       TODO("Not yet implemented")
   }
}
  1. 移除 TODO 语句。
  2. 在详情窗格打开(可见)时,启用 OnBackPressedCallback 回调。这可以通过调用 setEnabled() 函数并传入 true 来实现。在 onPanelOpened() 中写入以下代码:
setEnabled(true)
  1. 可以使用属性访问语法简化以上代码。
override fun onPanelOpened(panel: View) {
   isEnabled = true
}
  1. 同样,当详情窗格关闭时,系统会将 isEnabled 设为 false
override fun onPanelClosed(panel: View) {
   isEnabled = false
}
  1. 完成回调的最后一步是,将 SportsListOnBackPressedCallback 监听器类添加到监听器列表中,该列表中的监听器将收到详情窗格滑动事件通知。为 SportsListOnBackPressedCallback 类添加 init 块。在 init 块内,调用 slidingPaneLayout.addPanelSlideListener() 并传入 this
init {
   slidingPaneLayout.addPanelSlideListener(this)
}

完成后的 SportsListOnBackPressedCallback 类应与以下代码类似:

class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
  SlidingPaneLayout.PanelSlideListener{

   init {
       slidingPaneLayout.addPanelSlideListener(this)
   }

   override fun handleOnBackPressed() {
       slidingPaneLayout.closePane()
   }

   override fun onPanelSlide(panel: View, slideOffset: Float) {
   }

   override fun onPanelOpened(panel: View) {
       isEnabled = true
   }

   override fun onPanelClosed(panel: View) {
       isEnabled = false
   }
}

注册回调

如需查看回调的实际效果,请使用调度程序 OnBackPressedDispatcher 注册回调。

通过 FragmentActivity 的基类,您可以使用其 OnBackPressedDispatcher 来控制返回按钮的行为。OnBackPressedDispatcher 控制将返回按钮事件分派给一个或多个 OnBackPressedCallback 对象的方式。

使用 addCallback() 方法添加回调。此方法可接收 LifecycleOwner。这可以确保仅在 LifecycleOwnerLifecycle.State.STARTED 时添加 OnBackPressedCallback。此外,与注册的回调相关联的 LifecycleOwner 被销毁时,相应 activity 或 fragment 还会移除注册的回调。这不仅可以防止内存泄漏,还可以使其适用于生命周期较短的 fragment 或其他生命周期所有者。

addCallback() 方法还会在实例中接收回调类作为第二个参数。您需要按照以下步骤注册回调:

  1. SportsListFragment 文件中的 onViewCreated() 函数内,在绑定变量的声明下方,为 SlidingPaneLayout 创建一个实例,并为其分配 binding.slidingPaneLayout 的值。
val slidingPaneLayout = binding.slidingPaneLayout
  1. SportsListFragment 文件中的 onViewCreated() 函数内,在 slidingPaneLayout 的声明下方,添加以下代码:
// Connect the SlidingPaneLayout to the system back button.
requireActivity().onBackPressedDispatcher.addCallback(
   viewLifecycleOwner,
   SportsListOnBackPressedCallback(slidingPaneLayout)
)

以上代码使用了 addCallback(),以便传入 viewLifecycleOwner 以及 SportsListOnBackPressedCallback 的实例。此回调仅在 fragment 的生命周期内有效。

  1. 接下来,在具有手机配置文件的模拟器上运行应用,并查看自定义返回按钮功能的实际效果。

33967fa8fde5b902.gif

10. 锁定模式

在手机等小屏幕设备上,如果列表窗格和详情窗格重叠,那么在默认情况下,用户可以向两个方向滑动,这样一来,即使在没有使用手势导航的情况下,也可以随意在两个窗格之间切换。您可以通过设置 SlidingPaneLayout 的锁定模式,锁定或解锁详情窗格。

  1. 在具有手机配置文件的模拟器中,尝试将详情窗格滑出屏幕。
  2. 您还可以在详情窗格中滑动,不妨自己尝试一下。
  3. Sports 应用中,建议不要使用该功能。您最好锁定 SlidingPaneLayout,以防止用户使用手势滑入和滑出。若要实现这一点,请在 onViewCreated() 方法中的 slidingPaneLayout 定义下方,将 lockMode 设为 LOCK_MODE_LOCKED
slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

如需详细了解其他锁定模式,请参阅文档

  1. 再次运行应用,您会注意到详情窗格现在已锁定。

恭喜,您已为应用添加 SlidingPaneLayout

11. 解决方案代码

此 Codelab 的解决方案代码位于下方所示的项目和模块中。

  1. 进入为此项目提供的 GitHub 代码库页面。
  2. 验证分支名称是否与此 Codelab 中指定的分支名称一致。例如,在以下屏幕截图中,分支名称为 main

1e4c0d2c081a8fd2.png

  1. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个弹出式窗口。

1debcf330fd04c7b.png

  1. 在弹出式窗口中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已经打开,则改为依次选择 File > Open 菜单选项。

8d1fda7396afe8e5.png

  1. 在文件浏览器中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 8de56cba7583251f.png 以构建并运行应用。请确保该应用按预期构建。

12. 了解更多内容