使用 RecyclerView 显示可滚动列表

想一想手机上常用的应用,您会发现几乎每一个应用都至少有一个列表。通话记录屏幕、“通讯录”应用和您爱用的社交媒体应用都会显示一个数据列表。如下面的屏幕截图所示,这些应用有的只显示了一个简单的单词或词组列表,而有的则显示了更复杂的列表项,例如包含文字和图片的卡片。无论列表的内容是什么,显示数据列表都是 Android 中最常见的界面任务之一。

cf10a913f9db0ee4.png

为了帮助您构建包含列表的应用,Android 提供了 RecyclerViewRecyclerView 旨在通过重复使用(即循环使用)滚动出屏幕的视图来非常高效地显示列表项,即使是大型列表也可以高效显示。当列表项滚动出屏幕之后,RecyclerView 会重复使用相应视图来显示即将显示的下一个列表项。也就是说,用新内容填充该列表项,而该列表项会再滚动到屏幕上。RecyclerView 的这种行为可以节省大量处理时间,并能让列表滚动更顺畅。

在下图显示的序列中,可以看到一个填充了数据 ABC 的视图。当该视图滚动出屏幕之后,RecyclerView 会重复使用该视图来显示新数据 XYZ

dcf4599789b9c2a1.png

在此 Codelab 中,您将构建一个 Affirmations 应用。Affirmations 是一个简单的应用,在滚动列表中以文字形式显示十句积极的自我肯定话语。然后,在后续的 Codelab 中,您将进一步为每句自我肯定话语添加一张励志图片,并完善应用界面。

前提条件

  • 在 Android Studio 中,基于模板创建一个项目。
  • 将字符串资源添加到应用。
  • 在 XML 文件中定义一个布局。
  • 了解 Kotlin 中的类和继承(包括抽象类)。
  • 继承一个现有类并替换其方法。
  • 参阅 developer.android.com 上的文档,了解 Android 框架提供的类。

学习内容

  • 如何使用 RecyclerView 显示数据列表?
  • 如何将代码构成软件包?
  • 如何将 Adapter 与 RecyclerView 配合使用以自定义各列表项的外观?

构建内容

  • 一个使用 RecyclerView 显示自我肯定话语字符串列表的应用。

所需条件

  • 一台安装了 Android Studio 4.1 或更高版本的计算机。

创建空 Activity 项目

创建新项目之前,请确保您使用的是 Android Studio 4.1 或更高版本。

  1. 在 Android Studio 中,使用 Empty Activity 模板启动一个新的 Kotlin 项目。
  2. 输入 Affirmations 作为应用的 Name,输入 com.example.affirmations 作为 Package name,然后选择 API Level 19 作为 Minimum SDK
  3. 点击 Finish 创建项目。

创建 Affirmations 应用的下一步是添加资源。您需要将以下内容添加到项目中。

  • 字符串资源:它们将在应用中显示为自我肯定话语。
  • 数据来源:它将向应用提供自我肯定话语的列表。

添加自我肯定话语字符串

  1. Project 窗口中,依次打开 app > res > values > strings.xml。此文件目前只有一个资源,即应用的名称。
  2. strings.xml 中,将以下自我肯定话语添加为单独的字符串资源。将其分别命名为 affirmation1affirmation2,依此类推。

自我肯定话语的文字稿

I am strong.
I believe in myself.
Each day is a new opportunity to grow and be a better version of myself.
Every challenge in my life is an opportunity to learn from.
I have so much to be grateful for.
Good things are always coming into my life.
New opportunities await me at every turn.
I have the courage to follow my heart.
Things will unfold at precisely the right time.
I will be present in all the moments that this day brings.

完成后,strings.xml 文件应如下所示。

<resources>
    <string name="app_name">Affirmations</string>
    <string name="affirmation1">I am strong.</string>
    <string name="affirmation2">I believe in myself.</string>
    <string name="affirmation3">Each day is a new opportunity to grow and be a better version of myself.</string>
    <string name="affirmation4">Every challenge in my life is an opportunity to learn from.</string>
    <string name="affirmation5">I have so much to be grateful for.</string>
    <string name="affirmation6">Good things are always coming into my life.</string>
    <string name="affirmation7">New opportunities await me at every turn.</string>
    <string name="affirmation8">I have the courage to follow my heart.</string>
    <string name="affirmation9">Things will unfold at precisely the right time.</string>
    <string name="affirmation10">I will be present in all the moments that this day brings.</string>
</resources>

现在,您已添加了字符串资源,可以在代码中将其引用为 R.string.affirmation1R.string.affirmation2 了。

创建新软件包

按照逻辑组织代码可以帮助您和其他开发者理解、维护和扩展代码。就像您可以将文书整理成文件和文件夹一样,您同样可以将代码组织成文件和软件包。

什么是软件包?

  1. 在 Android Studio 的 Project 窗口 (Android) 中,查看 app > java 下的 Affirmations 应用的新项目文件。它们应该与下面的屏幕截图相似。图中显示了三个软件包:一个用于您的代码 (com.example.affirmations),另外两个用作测试文件(com.example.affirmations (androidTest)com.example.affirmations (test))。

809a0d77a0759dc5.png

  1. 请注意,软件包的名称由几个单词组成,单词间以句点进行分隔。

软件包的使用方式有两种。

  • 为代码的不同部分创建不同的软件包。例如,开发者通常会将处理数据的类和构建界面的类分成不同的软件包。
  • 在代码中使用来自其他软件包的代码。为了使用来自其他软件包的类,您需要在构建系统的依赖项中定义这些类。此外,使用 import 将其导入代码中也是一种标准做法。如此一来,您就可以使用这些类的简短名称(例如 TextView)而无需使用其完全限定名称(例如 android.widget.TextView)。举例来说,您已对 sqrt (import kotlin.math.sqrt) 和 View (import android.view.View) 等类使用过 import 语句。

在 Affirmations 应用中,除了导入 Android 类和 Kotlin 类之外,您还需要将应用分为多个软件包。即使应用所用的类并不太多,最好也使用软件包将类按功能分组。

为软件包命名

您可以对软件包随意命名,只要是全局唯一名称即可;任何其他已发布的软件包都不能同名。由于已有软件包的数量非常庞大,要随机想出唯一的名称也非常困难;因此,为了更易于创建和理解软件包名称,编程人员遵循相应的命名惯例。

  • 软件包名称通常采用从一般到具体的结构,名称的每个部分均采用小写字母,各部分之间以句点分隔。重要提示:句点只是名称的组成部分。它既不表示代码中的层次结构,也并非一定要遵循某种文件夹结构!
  • 由于互联网域名具有全局唯一性,因此使用域名(通常是您的域名或您企业的域名)作为名称的第一个部分是一种惯例。
  • 您可以选择软件包的名称,用它来表示软件包的内容以及软件包彼此间的关系。
  • 对于我们此处所用的这种代码示例,com.example 后跟应用名称是常用的命名方式。

以下是一些预定义软件包名称及其内容的示例:

  • kotlin.math - 数学函数和常量。
  • android.widget - 视图,例如 TextView

创建软件包

  1. 在 Android Studio 的 Project 窗格中,右键点击 app > java > com.example.affirmations,然后依次选择 New > Package

39f35a81a574b7ee.png

  1. New Package 弹出式窗口中,注意建议的软件包名称前缀。建议的软件包名称的第一个部分是右键点击的软件包的名称。虽然使用软件包名称不会创建一个软件包层次结构,但我们可以通过重复使用一部分名称来说明内容之间的关系及其组织方式!
  2. 在弹出式窗口中,将 model 附加到建议的软件包名称末尾。开发者通常使用 model 作为对数据建模的类或表示数据的类的软件包名称。

3d392f8da53adc6f.png

  1. Enter 键。系统将在 com.example.affirmations(根)软件包下创建一个新软件包。这个新软件包将包含应用中定义的任何与数据相关的类。

创建 Affirmation 数据类

在此任务中,您将创建一个名为 Affirmation. 的类。一个 Affirmation 对象实例代表一句自我肯定话语,并包含这句自我肯定话语字符串的资源 ID。

  1. 右键点击 com.example.affirmations.model 软件包,然后依次选择 New > Kotlin File/Class

d9b07905f456ab1.png

  1. 在弹出式窗口中,选择 Class,然后输入 Affirmation 作为类的名称。系统将在 model 软件包中创建一个名为 Affirmation.kt 的新文件。
  2. 在类定义前添加 data 关键字,使 Affirmation 成为数据类。这样做会出错,因为数据类必须至少定义一个属性。

Affirmation.kt

package com.example.affirmations.model

data class Affirmation {
}

在创建 Affirmation 的实例时,您需要传入自我肯定话语字符串的资源 ID。资源 ID 是一个整数。

  1. val 整数参数 stringResourceId 添加到 Affirmation 类的构造函数中。这样就可以消除错误了。
package com.example.affirmations.model

data class Affirmation(val stringResourceId: Int)

创建要作为数据源的类

应用中显示的数据可能来自不同的来源(例如,来自应用项目内,或者来自需要连接到互联网才能下载数据的外部来源)。因此,数据格式未必正好是您需要的格式。应用的其余部分本身不应该关心数据的来源或数据的原始格式。您可以也应该在单独的 Datasource 类中隐藏此数据准备工作,由该类来为应用准备数据。

准备数据是一项需要单独处理的工作,因此请将 Datasource 类放在单独的 data 软件包中。

  1. 在 Android Studio 的 Project 窗口中,右键点击 app > java > com.example.affirmations,然后依次选择 New > Package
  2. 输入 data 作为软件包名称的最后一部分。
  3. 右键点击 data 软件包,然后选择 new Kotlin File/Class
  4. 输入 Datasource 作为类名称。
  5. Datasource 类中,创建一个名为 loadAffirmations() 的函数。

loadAffirmations() 函数需要返回 Affirmations 的列表。为此,您可以创建一个列表,并使用每个资源字符串的 Affirmation 实例填充该列表。

  1. List<Affirmation> 声明为 loadAffirmations() 方法的返回值类型。
  2. loadAffirmations() 的主体中,添加 return 语句。
  3. return 关键字之后,调用 listOf<>() 创建一个 List
  4. 在尖括号 <> 内,将列表项的类型指定为 Affirmation。根据需要导入 com.example.affirmations.model.Affirmation
  5. 在圆括号内创建 Affirmation,传入 R.string.affirmation1 作为资源 ID,如下所示。
Affirmation(R.string.affirmation1)
  1. 将其余 Affirmation 对象添加到所有自我肯定话语的列表,以英文逗号分隔。完成后的代码应如下所示。

Datasource.kt

package com.example.affirmations.data

import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

class Datasource {

    fun loadAffirmations(): List<Affirmation> {
        return listOf<Affirmation>(
            Affirmation(R.string.affirmation1),
            Affirmation(R.string.affirmation2),
            Affirmation(R.string.affirmation3),
            Affirmation(R.string.affirmation4),
            Affirmation(R.string.affirmation5),
            Affirmation(R.string.affirmation6),
            Affirmation(R.string.affirmation7),
            Affirmation(R.string.affirmation8),
            Affirmation(R.string.affirmation9),
            Affirmation(R.string.affirmation10)
        )
    }
}

[可选] 在 TextView 中显示自我肯定话语列表的大小

如需验证您能否创建自我肯定话语列表,您可以调用 loadAffirmations() 并在 Empty Activity 应用模板附带的 TextView 中显示返回的自我肯定话语列表的大小。

  1. layouts/activity_main.xml 中,为模板附带的 TextView 指定 textview 作为 id
  2. MainActivity 中,在 onCreate() 方法中的现有代码之后,获取对 textview 的引用。
val textView: TextView = findViewById(R.id.textview)
  1. 然后,添加代码以创建自我肯定话语列表并显示其大小。创建一个 Datasource,调用 loadAffirmations(),获取返回的列表的大小并将其转换为字符串,然后将其指定为 textViewtext
textView.text = Datasource().loadAffirmations().size.toString()
  1. 运行应用。屏幕应如下所示。

b4005973d4a0efc8.png

  1. 删除您刚在 MainActivity 中添加的代码。

在此任务中,您将设置一个 RecyclerView 来显示 Affirmations 的列表。

创建和使用 RecyclerView 需要编写许多代码段。您可以将其想象成劳动分工。下图所示为其概览,至于每段代码的详情,则有待您在实现过程中逐一了解。

  • 列表项 - 要显示的列表的一个数据项。代表应用中的一个 Affirmation 对象。
  • Adapter - 获取数据并准备数据以供 RecyclerView 显示。
  • ViewHolder - 供 RecyclerView 用于和重用于显示自我肯定话语的视图池。
  • RecyclerView - 屏幕上的视图。

4e9c18b463f00bf7.png

将 RecyclerView 添加到布局中

Affirmations 应用由一个名为 MainActivity 的 activity 及其名为 activity_main.xml 的布局文件组成。首先,您需要将 RecyclerView 添加到 MainActivity 的布局中。

  1. 打开 activity_main.xml (app > res > layout > activity_main.xml)。
  2. 切换到 Split 视图(如果您尚未使用该视图)。

7e7c3e8429267dac.png

  1. 删除 TextView

当前布局使用的是 ConstraintLayout。如果想在布局中放置多个子视图,那么 ConstraintLayout 就是理想而又灵活的选择。不过,因为您的布局只有一个子视图 RecyclerView,所以您可以切换到更简单的名为 FrameLayoutViewGroup,这才是只包含一个子视图时应该使用的选择。

9e6f235be5fa31a8.png

  1. 在 XML 文件中,将 ConstraintLayout 替换为 FrameLayout。完成后的布局应如下所示。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".MainActivity">
</FrameLayout>
  1. 切换到 Design 视图。
  2. Palette 中,选择 Containers,然后找到 RecyclerView
  3. RecyclerView 拖放到布局中。
  4. 如果系统显示 Add Project Dependency 弹出式窗口,请阅读里面的内容并点击 OK。(如果未显示此弹出式窗口,无需执行任何操作。)
  5. 等待 Android Studio 完成操作并且 RecyclerView 显示在布局中。
  6. 根据需要将 RecyclerViewlayout_widthlayout_height 属性更改为 match_parent,以便 RecyclerView 能够填满整个屏幕。
  7. RecyclerView 的资源 ID 设为 recycler_view

RecyclerView 支持以线性列表或网格等不同方式显示列表项。列表项的排列由 LayoutManager 处理。Android 框架针对基本的列表项布局提供了布局管理器。Affirmations 应用以垂直列表的形式显示列表项,因此您可以使用 LinearLayoutManager

  1. 切换回 Code 视图。在 XML 代码中的 RecyclerView 元素内,添加 LinearLayoutManager 作为 RecyclerView 的布局管理器属性,如下所示。
app:layoutManager="LinearLayoutManager"

为了能够滚动浏览超出屏幕长度的垂直项目列表,您需要添加一个垂直滚动条。

  1. RecyclerView 内,添加设为 verticalandroid:scrollbars 属性。
android:scrollbars="vertical"

最终的 XML 布局应如下所示:

activity_main.xml

<FrameLayout 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=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layoutManager="LinearLayoutManager" />

</FrameLayout>
  1. 运行应用。

项目应能不出任何问题地顺利编译和运行。不过,由于缺少一段重要代码,应用中只会显示白色背景。现在,您已将数据源和 RecyclerView 添加到布局中,但 RecyclerView 尚无有关如何显示 Affirmation 对象的信息。

为 RecyclerView 实现 Adapter

应用需要通过某种方式从 Datasource 获取数据并为其设置格式,这样才能让每个 AffirmationRecyclerView 中显示为一个列表项。

Adapter 是一种设计模式,可将数据调整为可供 RecyclerView 使用的内容。在本例中,您需要一个能完成以下任务的 Adapter:从 loadAffirmations() 返回的列表中获取 Affirmation 实例并将其转换为列表项视图,以便在 RecyclerView. 中显示它。

当您运行应用时,RecyclerView 会使用 Adapter 确定如何在屏幕上显示您的数据。RecyclerView 要求 Adapter 为列表中的第一个数据项创建新的列表项视图。有了此视图后,它会要求 Adapter 提供用于绘制该列表项的数据。此过程不断重复,直到 RecyclerView 不再需要更多视图来填满屏幕。如果屏幕上一次只能同时容纳 3 个列表项视图,那么 RecyclerView 只会要求 Adapter 准备这 3 个列表项视图(而不是全部 10 个列表项视图)。

在此步骤中,您将构建一个 Adapter,用于调整 Affirmation 对象实例,使其可在 RecyclerView 中显示。

创建 Adapter

Adapter 包含多个部分,您需要编写大量代码,而且这些代码比您在本课程中迄今为止完成的代码更加复杂。如果您刚开始无法完全理解细节也没关系。当您完成使用 RecyclerView 的整个应用后,您就能更深入地了解各部分如何组合到一起了。今后,当您使用 RecyclerView 创建应用时,您还可以重复使用此代码作为基础。

为列表项创建布局

RecyclerView 中的每个列表项都有自己的布局,您可以在单独的布局文件中定义这些布局。由于您只需要显示字符串,因此可将 TextView 用于列表项布局。

  1. res > layout 中,新建名为 list_item.xml 的空 File
  2. Code 视图中打开 list_item.xml
  3. 添加 iditem_titleTextView
  4. 针对 layout_widthlayout_height 添加 wrap_content,如下面的代码所示。

请注意,您不需要对布局使用 ViewGroup,因为稍后我们会膨胀此列表项布局并将其作为子视图添加到父 RecyclerView 中。

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

或者,您也可以使用 File > New > Layout Resource File,将 File name 设为 list_item.xml,并以 TextView 作为 Root element。然后,更新生成的代码以匹配上面的代码。

dbb34ca516c804c6.png

创建 ItemAdapter 类

  1. 在 Android Studio 的 Project 窗格中,右键点击 app > java > com.example.affirmations,然后依次选择 New > Package
  2. 输入 adapter 作为软件包名称的最后一部分。
  3. 右键点击 adapter 软件包,然后依次选择 New > Kotlin File/Class
  4. 输入 ItemAdapter 作为类名称,完成创建操作,系统将打开 ItemAdapter.kt 文件。

您需要将一个参数添加到 ItemAdapter 的构造函数中,以便将自我肯定话语列表传入 Adapter。

  1. 将一个参数添加到 ItemAdapter 的构造函数中,该参数是名为 dataset、类型为 List<Affirmation>val。根据需要导入 Affirmation
  2. 由于 dataset 仅在此类中使用,因此请将其设为 private

ItemAdapter.kt

import com.example.affirmations.model.Affirmation

class ItemAdapter(private val dataset: List<Affirmation>) {

}

ItemAdapter 需要有关如何解析字符串资源的信息。这些信息与有关应用的其他信息一起存储在 Context 对象实例中,您可将此实例传入 ItemAdapter 实例。

  1. 将一个参数添加到 ItemAdapter 的构造函数中,该参数是名为 context、类型为 Contextval。请将其作为构造函数中的第一个参数。
class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

}

创建 ViewHolder

RecyclerView 不会直接与列表项视图交互,而是处理 ViewHolders。一个 ViewHolder 代表 RecyclerView 中的一个列表项视图,可根据需要对其重复使用。ViewHolder 实例存储着对列表项布局中各个视图的引用(因此被命名为“view holder”,意思是视图存储器)。这样,用新数据更新列表项视图就会更轻松。ViewHolder 还会添加一些信息,供 RecyclerView 用于在屏幕上高效移动视图。

  1. ItemAdapter 类中,在 ItemAdapter 的右花括号之前,创建一个 ItemViewHolder 类。
class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

    class ItemViewHolder()
}
  • 在一个类中定义另一个类称为创建嵌套类
  • 由于 ItemViewHolder 仅由 ItemAdapter 使用,因此在 ItemAdapter 内创建它可以显示此关系。这并非强制性要求,但这样做有助于其他开发者了解您的程序的结构。
  1. 将类型为 Viewprivate val view 作为参数添加到 ItemViewHolder 类构造函数。
  2. ItemViewHolder 设为 RecyclerView.ViewHolder 的子类,并将 view 参数传入父类构造函数。
  3. ItemViewHolder 内,定义类型为 TextViewval 属性 textView。为其分配您在 list_item.xml 中定义的 ID 为 item_title 的视图。
class ItemAdapter(private val context: Context, private val dataset: List<Affirmation>) {

    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }
}

替换 Adapter 方法

  1. 添加代码以从抽象类 RecyclerView.Adapter 扩展您的 ItemAdapter。在尖括号中,将 ItemAdapter.ItemViewHolder 指定为 ViewHolder 类型。
class ItemAdapter(
    private val context: Context,
    private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }
}

您会看到一条错误消息,因为您需要从 RecyclerView.Adapter 实现一些抽象方法。

  1. 将光标放在 ItemAdapter 上,然后按 Command+I(在 Windows 上,则请按 Control+I)。系统将列出您需要实现的方法:getItemCount()onCreateViewHolder()onBindViewHolder()

7a8a383a8633094b.png

  1. 使用 Shift+click 将三个函数全部选中,然后点击 OK

这样就会使用正确的参数创建这三个方法的桩,如下所示。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
    TODO("Not yet implemented")
}

override fun getItemCount(): Int {
    TODO("Not yet implemented")
}

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
    TODO("Not yet implemented")
}

您应该不会再看到错误消息。接下来,您需要实现这些方法,使其能对您的应用正确发挥作用。

实现 getItemCount()

getItemCount() 方法需要返回数据集的大小。应用的数据位于您传入 ItemAdapter 构造函数的 dataset 属性中,您可以使用 size 获取其大小。

  1. getItemCount() 替换为:
override fun getItemCount() = dataset.size

下面是更简洁的编写方式:

override fun getItemCount(): Int {
    return dataset.size
}

实现 onCreateViewHolder()

onCreateViewHolder() 方法由布局管理器调用,用于为 RecyclerView 创建新的 ViewHolder(如果不存在可重复使用的 ViewHolder)。请注意,一个 ViewHolder 代表一个列表项视图。

onCreateViewHolder() 方法接受两个参数并返回一个新的 ViewHolder

  • parent 参数,即新列表项视图将作为子视图附加到的视图组。父项是 RecyclerView
  • viewType 参数,当同一个 RecyclerView 中存在多种列表项视图类型时,此参数可发挥重要作用。如果在 RecyclerView 内显示不同的列表项布局,就会有不同的列表项视图类型。您只能循环使用列表项视图类型相同的视图。针对您的情况而言,只有一个列表项布局和一个列表项视图类型,因此您不必担心此参数。
  1. onCreateViewHolder() 方法中,从提供的上下文(parentcontext)中获取 LayoutInflater 的实例。布局膨胀器知道如何将 XML 布局膨胀为视图对象的层次结构。
val adapterLayout = LayoutInflater.from(parent.context)
  1. 获取 LayoutInflater 对象实例后,添加一个句点,后跟另一个方法调用以膨胀实际的列表项视图。传入 XML 布局资源 ID R.layout.list_itemparent 视图组。第三个布尔值参数是 attachToRoot。此参数需为 false,因为 RecyclerView 会在需要的时候替您将此列表项添加到视图层次结构中。
val adapterLayout = LayoutInflater.from(parent.context)
       .inflate(R.layout.list_item, parent, false)

现在,adapterLayout 存储着对列表项视图的引用(稍后我们可从中找到

子视图,例如 TextView)。

  1. onCreateViewHolder() 中,返回根视图为 adapterLayout 的新 ItemViewHolder 实例。
return ItemViewHolder(adapterLayout)

以下就是截至目前为止 onCreateViewHolder() 的代码。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
    // create a new view
    val adapterLayout = LayoutInflater.from(parent.context)
        .inflate(R.layout.list_item, parent, false)

    return ItemViewHolder(adapterLayout)
}

实现 onBindViewHolder()

需要替换的最后一个方法是 onBindViewHolder()。此方法由布局管理器调用,以便替换列表项视图的内容。

onBindViewHolder() 方法有两个参数:其一是先前由 onCreateViewHolder() 方法创建的 ItemViewHolder;其二是一个 int,它代表当前列表项的 position。在此方法中,您将根据位置从数据集中找到正确的 Affirmation 对象。

  1. onBindViewHolder() 内,创建一个 val item 并获取 dataset 中位于给定 position 的列表项。
val item = dataset[position]

最后,您需要更新 ViewHolder 引用的所有视图,以便反映此列表项的正确数据。在本例中,只有一个视图:ItemViewHolder 内的 TextView。设置 TextView 的文本以显示此列表项的 Affirmation 字符串。

  1. 有了 Affirmation 对象实例,您可以通过调用 item.stringResourceId 查找相应的字符串资源 ID。不过,这是一个整数,您需要查找它与实际字符串值的映射。

在 Android 框架中,您可以使用字符串资源 ID 调用 getString(),它会返回与其关联的字符串值。getString()Resources 类中的一个方法,您可以通过 context 获取 Resources 类的实例。

这意味着,您可以调用 context.resources.getString() 并传入字符串资源 ID。返回的字符串可在 holder ItemViewHolder 中设为 textViewtext。简言之,这一行代码会更新 ViewHolder 以显示这句自我肯定话语字符串。

holder.textView.text = context.resources.getString(item.stringResourceId)

完成后的 onBindViewHolder() 方法应如下所示。

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
    val item = dataset[position]
    holder.textView.text =  context.resources.getString(item.stringResourceId)
}

以下是完成后的 Adapter 代码。

ItemAdapter.kt

package com.example.affirmations.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

/**
 * Adapter for the [RecyclerView] in [MainActivity]. Displays [Affirmation] data object.
 */
class ItemAdapter(
    private val context: Context,
    private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder.
    // Each data item is just an Affirmation object.
    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }

    /**
     * Create new views (invoked by the layout manager)
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        // create a new view
        val adapterLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.list_item, parent, false)

        return ItemViewHolder(adapterLayout)
    }

    /**
     * Replace the contents of a view (invoked by the layout manager)
     */
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = dataset[position]
        holder.textView.text = context.resources.getString(item.stringResourceId)
    }

    /**
     * Return the size of your dataset (invoked by the layout manager)
     */
    override fun getItemCount() = dataset.size
}

现在,您已实现了 ItemAdapter,接下来您需要指示 RecyclerView 使用此 Adapter。

修改 MainActivity 以使用 RecyclerView

最后,您需要使用 DatasourceItemAdapter 类在 RecyclerView 中创建和显示列表项。您可以在 MainActivity 中执行此操作。

  1. 打开 MainActivity.kt
  2. MainActivity, 中,转到 onCreate() 方法。在调用 setContentView(R.layout.activity_main). 之后,插入以下步骤中所述的新代码。
  3. 创建 Datasource 的实例,并对其调用 loadAffirmations() 方法。将返回的自我肯定话语列表存储在名为 myDatasetval 中。
val myDataset = Datasource().loadAffirmations()
  1. 创建一个名为 recyclerView 的变量,并使用 findViewById() 查找对布局中 RecyclerView 的引用。
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
  1. 为了指示 recyclerView 使用您创建的 ItemAdapter 类,请创建一个新的 ItemAdapter 实例。ItemAdapter 应该有两个参数:此 activity 的上下文 (this) 和 myDataset 中的自我肯定话语。
  2. ItemAdapter 对象分配给 recyclerViewadapter 属性。
recyclerView.adapter = ItemAdapter(this, myDataset)
  1. 由于 activity 布局中的 RecyclerView 布局大小是固定的,因此您可以将 RecyclerViewsetHasFixedSize 参数设为 true。只有在提高性能时才需要使用此设置。如果您知道内容的更改不会更改 RecyclerView 的布局大小,请使用此设置。
recyclerView.setHasFixedSize(true)
  1. 完成后,MainActivity 的代码应与下面的代码类似。

MainActivity.kt

package com.example.affirmations

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.adapter.ItemAdapter
import com.example.affirmations.data.Datasource

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initialize data.
        val myDataset = Datasource().loadAffirmations()

        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        recyclerView.adapter = ItemAdapter(this, myDataset)

        // Use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        recyclerView.setHasFixedSize(true)
    }
}
  1. 运行应用。屏幕上应显示自我肯定话语字符串的列表。

427c10d4f29d769d.png

祝贺您!您刚刚创建了一个使用 RecyclerView 和自定义 Adapter 显示数据列表的应用。请花些时间检查您创建的代码,了解不同组件能否协同工作。

此应用已有显示自我肯定话语所需的全部组件,但还不能说已完全准备好投入使用。界面还可以稍加改进。在下一个 Codelab 中,您将改进代码、学习如何在应用中添加图像并完善界面。

此 Codelab 的解决方案代码位于下方所示的项目和模块中。请注意,有些 Kotlin 文件位于不同的软件包中,如文件开头的 package 语句所示。

res/values/strings.xml

<resources>
    <string name="app_name">Affirmations</string>
    <string name="affirmation1">I am strong.</string>
    <string name="affirmation2">I believe in myself.</string>
    <string name="affirmation3">Each day is a new opportunity to grow and be a better version of myself.</string>
    <string name="affirmation4">Every challenge in my life is an opportunity to learn from.</string>
    <string name="affirmation5">I have so much to be grateful for.</string>
    <string name="affirmation6">Good things are always coming into my life.</string>
    <string name="affirmation7">New opportunities await me at every turn.</string>
    <string name="affirmation8">I have the courage to follow my heart.</string>
    <string name="affirmation9">Things will unfold at precisely the right time.</string>
    <string name="affirmation10">I will be present in all the moments that this day brings.</string>
</resources>

affirmations/data/Datasource.kt

package com.example.affirmations.data

import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

class Datasource {

    fun loadAffirmations(): List<Affirmation> {
        return listOf<Affirmation>(
            Affirmation(R.string.affirmation1),
            Affirmation(R.string.affirmation2),
            Affirmation(R.string.affirmation3),
            Affirmation(R.string.affirmation4),
            Affirmation(R.string.affirmation5),
            Affirmation(R.string.affirmation6),
            Affirmation(R.string.affirmation7),
            Affirmation(R.string.affirmation8),
            Affirmation(R.string.affirmation9),
            Affirmation(R.string.affirmation10)
        )
    }
}

affirmations/model/Affirmation.kt

package com.example.affirmations.model

data class Affirmation(val stringResourceId: Int)

affirmations/MainActivty.kt

package com.example.affirmations

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.adapter.ItemAdapter
import com.example.affirmations.data.Datasource

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initialize data.
        val myDataset = Datasource().loadAffirmations()

        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        recyclerView.adapter = ItemAdapter(this, myDataset)

        // Use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        recyclerView.setHasFixedSize(true)
    }
}

affirmations/adapter/ItemAdapter.kt

package com.example.affirmations.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.affirmations.R
import com.example.affirmations.model.Affirmation

/**
 * Adapter for the [RecyclerView] in [MainActivity]. Displays [Affirmation] data object.
 */
class ItemAdapter(
    private val context: Context,
    private val dataset: List<Affirmation>
) : RecyclerView.Adapter<ItemAdapter.ItemViewHolder>() {

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder.
    // Each data item is just an Affirmation object.
    class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.item_title)
    }

    /**
     * Create new views (invoked by the layout manager)
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        // create a new view
        val adapterLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.list_item, parent, false)

        return ItemViewHolder(adapterLayout)
    }

    /**
     * Replace the contents of a view (invoked by the layout manager)
     */
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        val item = dataset[position]
        holder.textView.text = context.resources.getString(item.stringResourceId)
    }

    /**
     * Return the size of your dataset (invoked by the layout manager)
     */
    override fun getItemCount() = dataset.size
}

src/main/res/layout/activty_main.xml

<FrameLayout 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=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layoutManager="LinearLayoutManager" />

</FrameLayout>

src/main/res/layout/list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
  • RecyclerView 微件可帮助您显示数据列表。
  • RecyclerView 使用 Adapter 模式来调整和显示数据。
  • ViewHolderRecyclerView 创建并存储视图。
  • RecyclerView 附带内置 LayoutManagersRecyclerView 将列表项的布局工作委托给 LayoutManagers

为实现 Adapter,请执行以下操作:

  • 为 Adapter 创建一个新类,例如 ItemAdapter
  • 创建用于代表单个列表项视图的自定义 ViewHolder 类。从 RecyclerView.ViewHolder 类进行扩展。
  • 修改 ItemAdapter 类,以便从 RecyclerView.Adapter 类通过自定义 ViewHolder 类进行扩展。
  • 在 Adapter 内实现以下方法:getItemsCount()onCreateViewHolder()onBindViewHolder()