Android RecyclerView:是什么,用简单示例学习

Android 中的 RecyclerView 是什么?

RecyclerView 是 GridView 和 ListView 的更灵活、更高级的版本。它是一个用于显示大量数据集的容器,可以通过维护有限数量的视图来高效地滚动。当您拥有在运行时根据网络事件或用户操作而更改元素的数据集合时,可以使用 RecyclerView 小部件。

视图

Android 平台使用 View 和 ViewGroup 类在屏幕上绘制项目。这些类是抽象的,并通过不同的实现进行扩展以适应用例。例如,TextView 的简单目的是在屏幕上显示文本内容。EditText 扩展自相同的 View 类,并增加了更多功能,使用户能够输入数据。

我们可以创建自己的自定义视图,以便在开发用户界面时获得更大的灵活性。View 类提供了我们可以重写的用于在屏幕上绘图的方法,以及一种传递参数(如宽度、高度和我们想要添加到 View 的自定义属性)的手段,以使其按照我们希望的方式运行。

ViewGroups

ViewGroup 类是一种 View,但与仅负责显示的简单 View 类不同,ViewGroup 使我们能够将多个视图放入一个视图中,我们可以将其作为一个整体来引用。在这种情况下,顶层的 View(我们将其添加到其他简单视图(我们也可以添加 ViewGroups)中)称为“父”,而内部添加的视图称为“子”。

我们可以将 View 想象成一个数组,将 ViewGroup 想象成一个数组的数组。鉴于数组的数组本身也是一个数组,我们可以看到 ViewGroup 如何被视为一个 View。

var arr1 = [1,2,3] //imagine a simple View as an Array
//we can imagine this as a NumberTextView which doesn't really exist
//but we could imagine there's one that makes it easy to use numbers
var arr2 = ["a","b","c"] // We can imagine this as another simple view

var nestedArr = [arr1,arr2] //in our anology, we can now group views 
//together and the structure that would hold that would be what we call the ViewGroup

ViewGroup 还使我们能够定义子视图在视图内的组织方式,例如,是垂直还是水平排列。我们可以在视图内拥有不同的交互规则。例如,紧随其后的 TextViews 之间应有 12dp 的距离,而 ImageViews 后面跟着 TextViews 应有 5dp 的距离。

如果我们从头开始开发自己的 ViewGroup,情况就会如此。为了使这些配置更容易,Android 提供了一个名为 LayoutParams 的类,我们可以使用它来输入这些配置。

Android 文档提供了一些我们在配置自己的 ViewGroup 时会实现的默认参数。一些常用参数是与宽度、高度和边距相关的参数。默认情况下,这些配置的结构为 android:layout_height(高度),例如 android:layout_width。在此方面,当您创建自己的 ViewGroup 时,您可以进一步创建特定于您的 ViewGroup 的行为方式的 LayoutParams。

Android 提供了默认的 Views 和 ViewGroups,我们可以使用它们来完成许多常见的任务。我们提到的一个例子是 TextView。这是一个简单的视图,具有可配置的方面,如高度、宽度、文本大小等。我们有一个 ImageView 用于显示图像,如我们之前提到的 EditText,以及许多其他。Android 还提供了自定义 ViewGroups,我们可以将我们的 Views 添加到其中并获得预期的行为。

LinearLayout

LinearLayout 允许我们将 View 项目添加到其中。LinearLayout 是一个方向属性,它决定了它如何在屏幕上布局。它还具有 LinearLayout.LayoutParams,它决定了内部视图的规则,例如,android:center_horizontal 属性会将视图沿水平轴居中,而 `android:center_vertical` 会将视图内容沿垂直轴居中。

这里有一些图片可以帮助您理解居中。我们将此视为 200px x 200px 空间内的简单 TextView,居中属性将使其行为如下。

android:center_horizontal

Horizontally Centered Content
水平居中内容

android:center_vertical

vertically Centered Content
垂直居中内容

android:center

Centered Content
居中内容

RecyclerView 的核心组件

Core Components of The RecyclerView
RecyclerView 的核心组件

以下是 RecyclerView 的重要组成部分

RecyclerView.Adapter

基础知识 #1 – Adapter 模式

适配器是一种设备,它将一个系统或设备的一个属性转换为另一个不兼容的设备或系统的属性。其中一些修改信号或电源属性,而另一些则简单地将一个连接器的物理形式适配到另一个连接器。

我们在现实生活中发现的一个简单的适配器例子是,当我们想连接设备时,但它们的连接端口不匹配。当您去一个使用不同类型插座的国家时,可能会出现这种情况。如果您携带手机或笔记本电脑的充电器,则无法将其连接到电源插座。但是,您不会放弃,而是简单地获取一个适配器,它将位于电源插座和充电器之间,并使充电能够进行。

在编程中,当我们要将两个数据结构连接在一起以完成任务,但它们的默认端口无法相互通信时,情况就是如此。

我们将使用设备和充电器的简单例子。我们将有两个充电器实例。一个美式充电器和一个英式充电器

class AmericanCharger() {
    var chargingPower = 10
}
class BritishCharger(){
    var charginPower = 5
}

然后我们将创建两个设备

class AmericanDevice() 
class BritishDevice()

例如,我们可以创建一些设备实例来进行演示。

var myAmericanPhone = new AmericanDevice()
var myBritishPhone = new BritishDevice()

然后,我们将通过在设备中添加一个名为 charge() 的方法来引入充电概念。

该方法接收其相应的充电器作为输入,并基于此进行充电。

sealed trait Device
class AmericanDevice : Device{
    fun charge(charger:AmericanCharger){
        //Do some American charging
    }
}
class BritishDevice: Device{
    fun charge(charger:BritishCharger){
        //Do some British charging
    }
}

在这种情况下,基于我们的类比,由于各种原因,我们在使用 AmericanDevice 时需要使用 BritishCharger,反之亦然。

在编程世界中,这通常发生在混合提供相同功能的库时(在我们的上下文中,我们共享的功能是充电)。我们需要找到一种方法来实现这一点。

如果我们遵循类比,我们将需要去一家电子商店购买一个适配器,该适配器将使我们能够为 BritishChargers 提供 AmericanDevices 充电。从编程角度来看,我们将是适配器的制造者。

我们将创建一个适配器,使其与我们创建另一个所需的确切模式匹配。我们将将其实现为一个类,如下所示。它不一定是一个类,而可以是一个函数,突出适配器模式通常的作用。我们将使用一个类,因为它与 Android 中的大多数用法匹配。

class AmericanToBritishChargerAdapter(theAmericanCharger:AmericanCharger){
    fun returnNewCharger(): BritishCharger{
            //convert the American charger to a BritishCharger
            //we would change the American charging functionality
            //to British charging functionality to make sure the
            //adapter doesn't destroy the device. The adapter could 
            //, for example, control the power output by dividing by 2
            //our adapter could encompass this functionality in here
           
           var charingPower:Int = charger.chargingPower / 2
           var newBritishCharger = new BritishCharger()
           newBritishCharger.chargingPower = theAmericanCharger.chargingPower/2
        
        return newBritishCharger
    }
}

在编程世界中,插座的差异类似于充电方法中差异的类比。充电器具有不同的方法将使充电器无法使用。

 var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()

尝试使用 americanChargerIFound 调用 myBritishDevice 的 charge() 方法将不起作用,因为 AmericanDevice 只接受 AmericanCharger。

所以这是不可能的

 var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()
myBritishDevice.charge(americanChargerIFound)

在这种情况下,我们创建的适配器

AmericanToBritishChargerAdapter 现在可以派上用场了。我们可以使用 returnNewCharger() 方法创建一个新的 BritishCharger,然后对其进行充电。我们所要做的就是创建一个我们的适配器实例,然后将我们拥有的 AmericanCharger 馈送给它,它将创建一个我们可以使用的 BritishCharger。

var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()
//We create the adapter and feed it the americanCharger
var myAdapter =  AmericanToBritishChargerAdapter(theAmericanCharger)
//calling returnNewCharger from myAdapter would return a BritishCharger
var britishChargerFromAdapter = myAdapter.returnNewCharger()
//and once we have the britishCharger we can now use it
myBritishDevice.charge(britishChargerFromAdapter)

RecyclerView.LayoutManager

当处理 ViewGroup 时,我们将在其中放置 Views。LayoutManager 的任务是描述 Views 在其中如何布局。

为了比较,当使用 Linearlayout ViewGroup 时,我们的用例是能够将项目垂直或水平放置。这可以通过添加一个方向属性轻松实现,该属性告诉我们 linearlayout 如何放置在屏幕上。我们可以通过使用 `android:orientation=VERTICAL|HORIZONTAL` 属性来做到这一点。

我们还有一个名为 GridLayout 的 ViewGroup,当我们需要将 Views 放置在矩形网格结构中时,它的用例就体现出来了。这可能是为了使我们呈现给应用用户的数据易于理解。按设计,GridLayout 提供了配置来帮助您实现此目标,它具有可以定义网格维度的配置,例如,我们可以有一个 4x4 网格,3x2 网格。

RecyclerView.ViewHolder

ViewHolder 是我们还需要从 RecyclerView 扩展的抽象类。ViewHolder 为我们提供了常用方法,以帮助我们引用我们在 RecyclerView 上放置的 View,即使 RecyclerView 的回收机制已经更改了我们不知道的各种引用。

大列表

当我们想要向用户呈现大量的 Views 时,就会使用 RecyclerView,同时又不会耗尽我们设备上 RAM 中的每个 View 实例。

如果我们以联系人列表为例,我们会对联系人在列表中的样子有一个大致的了解。然后,我们将创建一个模板布局——实际上是一个 View——其中包含各种联系人列表数据的占位符。以下是解释整个目的的伪代码

//OneContactView
<OneContact>
<TextView>{{PlaceHolderForName}}</TextView>
<TextView>{{PlaceHolderForAddress}}</TextView>
<ImageView>{{PlaceHolderForProfilePicture}}</ImageView>
<TextView>{{PlaceHolderForPhoneNumber}}</TextView>
</OneContact>

然后,我们将拥有一个这样的联系人列表

 <ContactList>
</ContactList>

如果我们将内容硬编码,我们将无法以编程方式添加新内容到列表中,而无需重写应用程序。幸运的是,将 View 添加到 ViewGroup 支持 `addView(view:View)` 方法。

即使是那样,RecyclerView 添加子视图的方式也不是如此。

在我们的用例中,我们将有一个很长的联系人列表。对于列表中的每个联系人,我们需要创建一个 OneContactView 并填充 View 中的数据以匹配我们 Contact 类中的字段。然后,一旦我们有了 View,我们就需要将其添加到 RecyclerView 以显示列表。

data  class Contact(var name:String, var address:String, var pic:String, var phoneNumber:Int)

var contact1 = Contact("Guru","Guru97", "SomePic1.jpg", 991)
var contact2 = Contact("Guru","Guru98", "SomePic2.jpg", 992)
var contact3 = Contact("Guru","Guru99", "SomePic3.jpg", 993)

var myContacts:ArrayList<Contact> = arrayListOf<Contact>(contact1,contact2,contact3)

我们有一个名为 OneContactView 的联系人数组。它包含用于填充联系人类的内容的占位符。在 RecyclerView 中,我们必须将 Views 添加到其中,以便它能够帮助我们进行回收。

RecyclerView 实际上不允许我们添加视图,而是允许我们添加 ViewHolder。所以,在这种情况下,我们有两种要连接但又不匹配的东西。这时我们的适配器就派上用场了。RecyclerView 为我们提供了一个适配器,就像我们之前的 `AmericanToBritishChargerAdapter()` 一样,它使我们能够将无法与 BritishDevice 一起使用的 AmericanCharger 转换为可用的适配器,类似于现实生活中的电源适配器。

在这种情况下,适配器将接收我们的联系人数组和 View,然后从中生成 RecyclerView 愿意接受的 ViewHolder。

RecyclerView 提供了一个我们可以扩展的接口,通过 RecyclerView.Adapter 类来创建我们的适配器。在这个适配器内部,有一种方法可以创建 RecyclerView 希望使用的 ViewHolder 类。所以,我们所拥有的情况与之前相同,但多了一个东西,那就是适配器。

我们有一个联系人数组,一个用于显示一个联系人的视图 OneContactView。RecyclerView 是一个提供回收服务的视图列表,但它只愿意接受 ViewHolder。

但在这种情况下,我们现在有了 RecyclerView.Adapter 类,它有一个创建 ViewHolder 的方法。

fun createViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder

RecyclerView.ViewHolder 是一个抽象类,它接收我们的 View 作为参数并将其转换为 ViewHolder。

它使用了用于扩展类能力的包装器模式。

基础知识 #2 – 包装器模式

我们将使用一个简单的例子来演示如何让动物说话。

sealed trait Animal{
    fun sound():String
}

data class Cat(name:String):Animal{
    fun sound(){
        "Meow"
        }
}
data class Dog(name:String):Animal{
    fun sound(){
        "Woof"
        }
}

var cat1 = Cat("Tubby")
var dog1 = Dog("Scooby")
cat1.sound() //meow
dog1.sound() //woof

在上面的例子中,我们有两个动物。如果碰巧我们想添加一个方法让它们说话,但库的作者不太好,我们仍然可以找到一种方法。我们需要的是 Animal 类的包装器。我们将通过将 Animal 作为类的构造函数来做到这一点

 class SpeechPoweredAnimalByWrapper(var myAnimal:Animal){

    fun sound(){
        myAnimal.sound()
    }

    speak(){
        println("Hello, my name is ${myAnimal.name}")
    }
}

现在我们可以将一个 animal 实例传递给 SpeechPoweredAnimalByWrapper。调用它的 sound() 方法将调用传入的 animal sound() 方法。我们还有一个额外的 speak() 方法,它被算作我们添加到传入动物的新功能。我们可以像这样使用它

var cat1 =  Cat("Garfield")
cat1.sound()//"meow"
cat1.speak()// doesn't work as it isn't implemented
var talkingCat = new SpeechPoweredAnimalByWrapper(cat1)
talkingCat.sound() //"meow" the sound method calls the one defined for cat1
talkingCat.speak() //"Hello, my name is Garfield"

使用这种模式,我们可以接受类并添加功能。我们只需要传递一个类实例和我们的包装类定义的新方法。

在我们上面的例子中,我们使用了一个具体的类。也可以在抽象类中实现相同的功能。我们要做的是将 SpeechPoweredAnimalByWrapper 类更改为抽象类,我们就完成了。我们将更改类名以使其更短,以便更容易阅读。

abstract class SpeechPowered(var myAnimal:Animal){

    fun sound(){
        myAnimal.sound()
    }

    speak(){
        println("Hello, my name is ${myAnimal.name}")
    }
}

它与之前相同,但它意味着不同的东西。在普通类中,我们可以有一个类的实例,就像我们创建 cat1 和 dog1 一样。然而,抽象类不应该被实例化,而应该用于扩展其他类。那么我们如何使用新的 SpeechPowered(var myAnimal:Animal) 抽象类呢?我们可以通过创建将扩展它并获得其功能的新类来使用它。

在我们的例子中,我们将创建一个 SpeechPoweredAnimal 类,它扩展了该类

class SpeechPoweredAnimal(var myAnimal:Animal):SpeechPowered(myAnimal)
var cat1 =  Cat("Tubby")
var speakingKitty = SpeechPoweredAnimal(cat1)
speakingKitty.speak() //"Hello, my name is Tubby"

这与 ViewHolder 中使用的模式相同。RecyclerView.ViewHolder 类是一个抽象类,它为 View 添加了功能,就像我们为动物添加了 speak 方法一样。添加的功能是使其在处理 RecyclerView 时起作用。

这就是我们将如何从 OneContactView 创建一个 OneContactViewHolder

//The View argument we pass is converted to a ViewHolder which uses the View to give it more abilities and in turn work with the RecyclerView
class OneContactViewHolder(ourContactView: View) : RecyclerView.ViewHolder(ourContactView)

RecyclerView 有一个适配器,它允许我们将 Contacts 数组连接到具有 RecyclerView 的 ContactsView

添加一个 View

ViewGroup 不会自动重绘 ViewGroup,而是遵循特定的计划。在您的设备上,它可能每 10 毫秒或 100 毫秒重绘一次,或者如果我们选择一个荒谬的数字,比如 1 分钟,当我们将一个 View 添加到 ViewGroup 时,您会在 1 分钟后看到更改,当 ViewGroup“刷新”时。

RecyclerView.Recycler

基础知识 #3。缓存

我们经常进行刷新的一个最好的例子是在浏览器中。例如,让我们假设我们访问的网站是静态的,并且不动态发送内容,我们需要不断刷新才能看到更改。

在这个例子中,让我们假设相关网站是 Twitter。我们将有一系列静态推文,我们唯一能看到新推文的方式就是点击刷新按钮来重新获取内容。

重新绘制整个屏幕显然是一件耗时的事情。如果我们考虑到我们通过手机提供商的带宽有限。而我们的推文列表有很多图片和视频,每次刷新时重新下载页面的所有内容都会很昂贵。

我们需要一种方法来存储已加载的推文,并确保我们的下一个请求能够说明它已有的推文。因此,它不会重新下载所有内容,只获取它已有的新推文,并检查本地保存的某个推文是否不再存在,以便它可以本地删除。我们所描述的称为缓存。

我们发送给网站的关于我们拥有内容的信息称为元数据。所以实际上我们不仅仅是说“我们要加载你的网站”,而是说“我们要加载你的网站,这是我们上次加载时保存的一些内容,请用它来只发送不存在的内容,这样我们就不会使用大量带宽,因为我们没有很多资源。”

布局调用——Tweet 列表必须疯狂

布局调用的一个例子是 scrollToPosition

这是一个常见的例子,存在于聊天应用程序等应用中。如果聊天线程中的某个人回复了早期的聊天气泡,一些聊天应用程序会包含回复以及指向聊天气泡的链接,点击该链接后,您将被导航到原始消息所在的位置。

在这种情况下,在我们向 RecyclerView 添加 LayoutManager 之前,以及在我们拥有 RecyclerView.Adapter 之前,调用 scrollToPosition(n:Int) 方法将被简单地忽略。

RecyclerView 组件之间的通信

基础知识 #4。回调

RecyclerView 在其工作中,有很多活动部分。它必须处理 LayoutManager,它告诉我们如何组织 Views,无论是线性的还是网格的。它必须处理一个适配器,该适配器负责将我们的 items contactList 转换为 Views OneContactView,然后转换为 RecyclerView 愿意在其提供的方法中使用的 ViewHolders OneContactViewHolder。

RecyclerView 的原始材料是我们的 Views,例如 OneContactView 和数据源。

contactList:Array<Contact>

我们使用了简单的场景作为起点,以了解 RecyclerView 试图实现的目标。

当我们要显示 1000 个联系人的静态数组时,一个基本情况很容易理解。

当列表不再是静态的时,RecyclerView 机制才真正开始活跃起来。

对于动态列表,我们必须考虑当我们将一个项目添加到列表或从列表中删除一个项目时,屏幕上的 View 会发生什么。

RecyclerView.LayoutManager

除了决定我们的视图如何布局(线性或网格)之外,LayoutManager 还在幕后做了很多工作,以帮助 Recycler 知道何时进行回收。

它负责跟踪屏幕上当前可见的 Views,并将此信息传达给回收机制。当用户向下滚动时,Layout Manager 负责将那些移出顶部视野的 View 通知回收系统,以便它们可以被重新使用,而不是保留在那里占用内存,或者不是销毁它们并必须创建新的。

这意味着 LayoutManager 需要跟踪用户在滚动列表时的位置。它通过维护一个基于索引的位置列表来实现,即第一个项目从 0 开始,增加以匹配我们列表中的项目数。

如果我们可以在一个包含 100 个项目的列表中看到 10 个项目,一开始,LayoutManager 就知道它关注的视图是从 View-0 到 View-9。当我们滚动时,LayoutManager 能够计算出移出焦点的视图。

LayoutManager 能够将这些视图释放给回收机制,以便它们可以被重用(例如,可以移除 View 的联系人数据,并用下一段的联系人数据替换占位符)。

如果我们的列表是静态的,这是一种理想情况,但使用 RecyclerView 的最常见用例之一是动态列表,其中数据可以来自在线端点,甚至可能来自传感器。不仅添加了数据,而且我们列表中的数据有时也会被删除或更新。

我们数据的动态状态可能使得 LayoutManager 非常难以理解。出于这个原因,LayoutManager 维护着一个自己的关于项目和位置的列表,该列表与回收组件使用的列表是分开的。这确保了它能够正确地执行其布局工作。

同时,RecyclerView 的 LayoutManager 不希望错误地表示它拥有的数据。为了正确运行,LayoutManager 以给定的间隔(60 毫秒)与 RecyclerView.Adapter 同步,共享关于我们列表项目的信息(即,添加、更新、删除、从一个位置移动到另一个位置的项目)。LayoutManager 在收到此信息后,会根据需要重新组织屏幕上的内容以匹配更改。

许多处理 RecylerView 的核心操作都围绕着 RecyclerView.LayoutManager 和 RecyclerView.Adapter 之间的通信,后者存储了我们有时是静态的、有时是动态的数据列表。

更重要的是,RecyclerView 为我们提供了可以用来监听事件的方法,例如 onBindViewHolder,当我们的 RecyclerView.Adapter 将列表内容(例如,联系人)绑定到 ViewHolder 时,以便它现在可以用于在屏幕上显示信息。

另一个是 onCreateViewHolder,它告诉我们 RecyclerView.Adapter 何时将常规 View(如 OneContactView)转换为 RecyclerView 可以使用的 ViewHolder 项。从我们的 ViewHolder 到 RecyclerView 的 onViewDetached 的使用

除了启用回收的核心机制之外,RecyclerView 还提供了自定义行为而不影响回收的方法。

重用 Views 会使我们难以执行我们习惯于使用静态 Views 完成的常见操作,例如响应 onClick 事件。

正如我们所知,RecyclerView.LayoutManager 将 Views 显示给用户,它可能暂时拥有的项目列表与 RecyclerView.Adapter 拥有的我们存储在数据库或从源流式传输的列表有所不同。直接在 Views 上放置 OnClick 事件可能导致意外行为,例如删除错误的联系人或更改。

Gradle

如果我们想使用 RecyclerView,我们需要在我们的 build .gradle 文件中添加它作为依赖项。

在下面的示例中,我们使用了 implementation “androidx.recyclerview:recyclerview:1.1.0”,这是本文档中最新版本。

将依赖项添加到 Gradle 文件后,Android Studio 会提示我们同步更改。

在空项目仅包含默认设置的情况下添加 RecyclerView 后,我们的 Gradle 文件看起来如下。

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.guru99.learnrecycler"
        minSdkVersion 17
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
             'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation "androidx.recyclerview:recyclerview:1.1.0"
}

目前我们只有一个布局文件。我们将从一个简单的例子开始,使用 RecyclerView 在屏幕上显示水果名称列表。

项目列表

我们将导航到我们的 MainActivity 文件,并在 onCreate() 方法生成之前创建一个包含水果名称的数组。

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

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

我们的下一个目标是使用 RecyclerView 将此列表呈现在屏幕上。

为此,我们将导航到保存 Layouts 的 layout 目录,并创建一个将负责显示一个水果的 View。

用于列表中每个项目的布局

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

在上面的 TextView 中,我们添加了一个 id 字段,该字段将用于识别 View。

它不是默认生成的。我们将 TextView 命名为 fruitName,以匹配将绑定到它的数据。

将 RecyclerView 添加到主布局

在同一个 activity 中,还有一个 main_layout.xml 布局文件是我们默认生成的。

如果我们选择了一个空项目,它将生成一个包含 ConstraintLayout 的 XML,其中将包含一个带有“Hello”文本的 TextView。

我们将删除所有内容,只让布局包含 RecyclerView,如下所示。

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView 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:id="@+id/fruitRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" />

我们还为 RecyclerView 添加了一个 id 属性,我们将在代码中引用它。

 android:id="@+id/fruitRecyclerView"

然后我们将导航回我们的 MainActivity 文件。使用我们创建的 id,我们将能够引用我们刚刚创建的 View。

我们将从在 Android 提供的 findViewById() 方法中引用 RecyclerView 开始。我们将在 onCreate() 方法中这样做。

我们的 onCreate() 方法将如下所示。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
    }

创建 ViewHolder

接下来,我们将创建一个 RecyclerView.ViewHolder,它负责接收我们的 View 并将其转换为 ViewHolder,RecyclerView 使用它来显示我们的项目。

我们将在 fun onCreate() 方法之后立即这样做。

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

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

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
    }
    
    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)
    
}

创建 RecyclerViewAdapter

接下来,我们将创建一个 FruitArrayAdapter 类,它扩展了 RecyclerView.Adapter 类。

我们创建的 FruitArrayAdapter 将负责执行以下操作。

它将从 fruit 数组中获取水果名称。它将使用我们的视图 one_fruit_view.xml 创建一个 ViewHolder。然后它将水果绑定到 ViewHolder,并动态地将内容绑定到我们创建的视图 one_fruit_view.xml。

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

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

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
    }
    
    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)
    
    class FruitArrayAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>()
}

Android Studio 会在我们的 FruitArrayAdapter 上显示红色波浪线,告诉我们需要实现一个方法,RecyclerView 可以使用该方法将我们的数组连接到 RecyclerView 可以使用的 ViewHolder。

   class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {
            
        }

        override fun getItemCount(): Int {
         
        }

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {
          
        }
    }

我们将从生成代码中最简单的部分开始,即 getItemCount() 方法。我们知道可以通过调用 array.length 方法来获取数组中的项目数。

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {
            
        }

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

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {
          
        }
    }

然后我们将实现方法重写 fun onCreateViewHolder。

这是 RecyclerView 要求我们帮助它构建 FruitHolder 的地方。

如果我们记得,这就是我们的 FruitViewHolder 类看起来的样子

class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)

它需要我们的 fruitView,我们将其创建为 XML 文件 one_fruit_view.xml。

我们可以像这样创建一个对该 XML 的引用并将其转换为 View。

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder

        }

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

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

        }
    }

剩下的部分是重写

fun onBindViewHolder(holder: FruitViewHolder, position: Int)

RecyclerView.Adapter 会要求一个位置整数,我们将使用它来从我们的列表中获取一个项目。它还为我们提供了一个 holder,以便我们可以将从 fruitArray 获取的项目绑定到视图持有者中包含的视图。

包含在 ViewHoder 中的视图可以通过字段 ViewHolder.itemView 访问。一旦我们获得视图,我们就可以使用我们之前创建的 id fruitName 来设置内容。

  override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

            var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

            var aFruitName = fruitArray.get(position)

            ourFruitTextView.setText(aFruitName)
        }

这样,我们的 FruitArrayAdapter 就完成了,如下所示。

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder
        }

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

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

            var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

            var aFruitName = fruitArray.get(position)

            ourFruitTextView.setText(aFruitName)
        }
    }

最后,我们准备好连接 RecyclerView 的其余部分。那就是创建 LayoutManager,它将告诉 RecyclerView 如何显示列表内容。是使用 LinearLayoutManager 以线性方式显示,还是使用 GridLayoutManager 或 StaggeredGridLayoutManager 以网格方式显示。

创建 Layout Manager

我们将回到 onCreate 函数内并添加 LayoutManager。

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

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
        
        var fruitLinearLayout = LinearLayoutManager(this)
        
        myFruitRecyclerView.layoutManager =fruitLinearLayout
        
    }

将我们的适配器挂接到项目并设置在 RecyclerView 上

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

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
        
        var fruitLinearLayout = LinearLayoutManager(this)
        
        myFruitRecyclerView.layoutManager =fruitLinearLayout
        
        var fruitListAdapter = FruitListAdapter(fruitNames)
        
        myFruitRecyclerView.adapter =fruitListAdapter
    }

我们还创建了一个 fruitListAdapter 实例并将其馈送了水果名称数组。

基本上,我们都完成了。

完整的 MainActivity.kt 文件如下所示。

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

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

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)

        var fruitLinearLayout = LinearLayoutManager(this)

        myFruitRecyclerView.layoutManager =fruitLinearLayout

        var fruitListAdapter = FruitListAdapter(fruitNames)

        myFruitRecyclerView.adapter =fruitListAdapter
    }

    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)

    class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder
        }

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

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

            var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

            var aFruitName = fruitArray.get(position)

            ourFruitTextView.setText(aFruitName)
        }
    }
}

下载项目