如何将多个选择添加到Android RecyclerView

RecyclerView 小部件是当今大多数Android应用程序不可或缺的一部分。自从2014年末它被添加到Android支持库以来,它已经将ListView 小部件作为显示大型复杂列表的首选小部件而黯然失色。但是,缺少一个重要的功能:支持选择和跟踪列表项。RecyclerView Selection是谷歌今年3月发布的一个插件库,试图解决这个问题。

在本教程中,我将向您展示如何使用新库创建一个应用程序,该应用程序提供了一个直观的界面,用于选择列表中的多个项目。按照此Android RecyclerView多选示例,您将学习一些可以在自己的应用中应用的技能。

先决条件

要跟进,您需要:

  • 最新版本的Android Studio
  • 运行Android API级别23或更高级别的设备或模拟器

1.添加RecyclerView Android依赖项

要将RecyclerView Selection库添加到Android Studio项目,请implementationapp 模块的build.gradle 文件中提及以下依赖

1.implementation 'com.android.support:recyclerview-v7:28.0.0'
2.implementation 'com.android.support:recyclerview-selection:28.0.0'

2.创建一个列表

在本教程中,我们将使用一小部分项目,每个项目都包含一个人的姓名和电话号码。

要存储每个列表项的数据,请创建一个名为的Kotlin数据类,Person并为其添加两个属性:name 和phone。

1.data class Person(val name:String,
 2.                val phone: String)

您现在可以继续Person 在主活动中创建对象列表。

val myList = listOf(
    Person("Alice", "555-0111"),
    Person("Bob", "555-0119"),
    Person("Carol", "555-0141"),
    Person("Dan", "555-0155"),
    Person("Eric", "555-0180"),
    Person("Craig", "555-0145")
)

3.将Recycler视图添加到布局

当然,我们将使用RecyclerView 小部件来显示列表。因此<RecyclerView> ,在主活动的布局XML文件中添加 标记。

<android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/my_rv">
 
</android.support.v7.widget.RecyclerView>

要指定列表项的布局,请创建一个新的XML文件并将其命名为list_item.xml。在其中,添加两个TextView 小部件:一个用于显示名称,另一个用于显示电话号码。如果使用LinearLayout 元素来定位窗口小部件,则XML文件的内容应如下所示:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">
 
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/list_item_name"
        style="@style/TextAppearance.AppCompat.Large"/>
 
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/list_item_phone"
        style="@style/TextAppearance.AppCompat.Small"/>
         
</LinearLayout>

4.创建一个View Holder

您可以将视图持有者视为一个对象,其中包含对列表项布局中存在的视图的引用。没有它,RecyclerView 窗口小部件将无法有效地呈现列表项。

现在,您需要一个视图持有者,它包含TextView 您在上一步中创建的两个小部件。因此,创建一个扩展RecyclerView.ViewHolder 类的新类,并初始化对其中的小部件的引用。这是如何做:

class MyViewHolder(view: View)
    : RecyclerView.ViewHolder(view) {
 
    val name: TextView = view.list_item_name
    val phone: TextView = view.list_item_phone
 
    // More code here
     
}

此外,RecyclerView Selection插件需要一种可以调用以唯一标识所选列表项的方法。理想情况下,此方法属于视图持有者本身。此外,它必须返回ItemDetailsLookup.ItemDetails 类的实例。因此,请将以下代码添加到视图持有者:

fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
        object: ItemDetailsLookup.ItemDetails<Long>() {
 
            // More code here
 
        }

您现在必须覆盖ItemDetails 类中存在的两个抽象方法。首先重写getPosition() 方法并返回adapterPosition 视图持有者的属性。该adapterPosition 属性通常只是列表项的索引。

override fun getPosition(): Int = adapterPosition

接下来,覆盖该getSelectionKey() 方法。此方法必须返回可用于唯一标识列表项的键。为了简单起见,让我们返回itemId 视图持有者的 属性。

override fun getSelectionKey(): Long? = itemId

您可以自由地使用任何其他技术来生成选择键,只要它生成唯一值即可。

5.处理用户接触

为使RecyclerView Selection插件正常工作,每当用户触摸 RecyclerView 窗口小部件时,您必须将触摸的坐标转换为 ItemDetails 对象。

创建一个扩展ItemDetailsLookup 该类的新类,并为其添加一个构造函数,该构造函数可以接受该RecyclerView 小部件作为参数。请注意,由于该类是抽象的,因此Android Studio将自动为其抽象方法生成存根。

class MyLookup(private val rv: RecyclerView)
    : ItemDetailsLookup<String>() {
    override fun getItemDetails(event: MotionEvent)
                                : ItemDetails<String>? {
 
        // More code here
         
    }
}

正如您在上面的代码中看到的,该getItemDetails() 方法接收一个 MotionEvent 对象。通过将事件的X和Y坐标传递给 findChildViewUnder() 方法,您可以确定与用户触摸的列表项关联的视图。要将View对象转换为ItemDetails 对象,您只需调用该getItemDetails() 方法即可。这是如何做:

val view = rv.findChildViewUnder(event.x, event.y)
if(view != null) {
    return (rv.getChildViewHolder(view) as MyViewHolder)
            .getItemDetails()
}
return null

6.创建适配器

您现在需要一个可以将列表绑定到RecyclerView 窗口小部件的适配器。要创建一个,请创建一个扩展RecyclerView.Adapter 该类的新类。由于适配器需要访问列表和活动的上下文,因此新类必须具有可以同时接受两者作为参数的构造函数。

class MyAdapter(private val listItems:List<Person>,
                private val context: Context)
    : RecyclerView.Adapter<MyViewHolder>() {
 
}

明确指出此适配器的每个项目将具有类型的唯一稳定标识符非常重要Long。最好的地方是在一个init 街区内。

init {
    setHasStableIds(true)
}

此外,为了能够使用项目的位置作为其唯一标识符,您必须覆盖该 getItemId() 方法。

override fun getItemId(position: Int): Long {
    return position.toLong()
}

因为RecyclerView.Adapter 该类是抽象的,所以您现在必须重写另外三种方法才能使您的适配器可用。

首先,重写getItemCount() 方法以返回列表的大小。

override fun getItemCount(): Int = listItems.size

接下来,覆盖该onCreateViewHolder() 方法。此方法必须返回您在本教程前面创建的视图持有者类的实例。要创建这样的实例,必须调用类的构造函数并将列表项的膨胀布局传递给它。要扩展布局,请使用 类的inflate() 方法LayoutInflater。这是如何做:

override fun onCreateViewHolder(parent: ViewGroup,
                                viewType: Int): MyViewHolder = 
        MyViewHolder(
            LayoutInflater.from(context)
                .inflate(R.layout.list_item, parent, false)
        )

最后,重写该onBindViewHolder() 方法并适当地初始化 视图持有者中存在text 的两个TextView小部件的 属性。

override fun onBindViewHolder(vh: MyViewHolder, position: Int) {
    vh.name.text = listItems[position].name
    vh.phone.text = listItems[position].phone
}

7.显示列表
此时,您几乎拥有了渲染列表所需的一切。但是,您仍必须指定列表项的定位方式。现在,让我们使用LinearLayoutManager 实例将它们置于另一个之下。

为了获得最佳性能,我建议您还指出RecyclerView 窗口小部件的大小 在运行时不会更改。

将以下代码添加到主要活动:

my_rv.layoutManager = LinearLayoutManager(this)
my_rv.setHasFixedSize(true)

最后,将适配器的新实例分配给 窗口小部件的adapter 属性 RecyclerView。

val adapter = MyAdapter(myList, this)
my_rv.adapter = adapter

如果您现在运行您的应用程序,您将能够看到该列表。

应用程序显示列表

8.创建选择跟踪器

RecyclerView 插件还不允许你选择的任何项目。要启用多项目选择,您需要SelectionTracker 在活动中使用一个对象。

private var tracker: SelectionTracker<Long>? = null

您可以使用SelectionTracker.Builder 该类初始化跟踪器。对于其构造函数,您必须传递选择ID,RecyclerView 窗口小部件,密钥提供程序,项目详细信息查找类和存储策略。

您可以自由使用任何字符串作为选择ID。作为密钥提供者,您可以使用StableIdKeyProvider 该类的实例。

RecyclerView选择库提供了各种存储策略,所有这些策略都可确保在用户设备旋转时或Android系统在资源紧张期间关闭您的应用时不会取消选择所选项目。目前,由于选择键的类型是Long,您必须使用StorageStrategy 类型的对象Long。

一旦Builder 准备就绪,你可以调用它的withSelectionPredicate() 方法来指定你要多少项目以允许用户选择。为了支持多项选择,作为方法的参数,必须传递 方法SelectionPredicate 返回的 对象createSelectAnything()。

因此,在activity的onCreate() 方法中添加以下代码:

tracker = SelectionTracker.Builder<Long>(
                "selection-1",
                my_rv,
                StableIdKeyProvider(my_rv),
                MyLookup(my_rv),
                StorageStrategy.createLongStorage()
          ).withSelectionPredicate(
                SelectionPredicates.createSelectAnything()
          ).build()

要充分利用存储策略,必须始终尝试恢复onCreate() 方法内部跟踪器的状态。

if(savedInstanceState != null)
        tracker?.onRestoreInstanceState(savedInstanceState)

同样,您必须确保在活动的onSaveInstanceState() 方法中保存跟踪器的状态 。

override fun onSaveInstanceState(outState: Bundle?) {
    super.onSaveInstanceState(outState)
 
    if(outState != null)
        tracker?.onSaveInstanceState(outState)
}

除非与适配器关联,否则选择跟踪器不是很有用。因此,通过调用setTracker() 方法将其传递给适配器。

adapter.setTracker(tracker)

该setTracker() 方法尚不存在,因此在适配器类中添加以下代码:

private var tracker: SelectionTracker<Long>? = null
 
fun setTracker(tracker: SelectionTracker<Long>?) {
    this.tracker = tracker
}

如果您此时尝试运行应用程序,则可以在列表中选择项目。通过长按列表项进入多项选择模式,您将能够在大多数设备上感受到短暂的振动。但是,由于所选项目目前与未选择的项目无法区分,因此您将没有视觉反馈。要解决此问题,您需要onBindViewHolder() 在适配器的方法内进行一些更改。

突出显示所选项目的传统方法是更改​​其背景颜色。因此,您现在必须更改LinearLayout 项目布局XML文件中存在的窗口小部件的背景颜色。要获取对它的引用,请获取TextView 对视图持有者中可用的其中一个小部件的父级的引用。

在onBindViewHolder() 方法结束之前添加以下代码:

val parent = vh.name.parent as LinearLayout
 
// More code here

接下来,您可以调用 对象的isSelected() 方法SelectionTracker来确定是否选择了项目。

以下代码显示如何将所选项目的背景颜色更改为青色:

if(tracker!!.isSelected(position.toLong())) {
    parent.background = ColorDrawable(
            Color.parseColor("#80deea")
    )
} else {
    // Reset color to white if not selected
    parent.background = ColorDrawable(Color.WHITE)
}

如果您现在运行该应用程序,您应该能够看到您选择的项目。

应用程序显示所选项目的列表

9.创建选择观察者

通常,您希望向用户显示当前选择的项目数。使用RecyclerView Selection库,这样做非常简单。

SelectionObserver 通过调用addObserver() 方法将对象与选择跟踪器相关联 。在onSelectionChanged() 观察者的方法内,您可以检测所选项目数的变化。

tracker?.addObserver(
        object: SelectionTracker.SelectionObserver<Long>() {
            override fun onSelectionChanged() {
                val nItems:Int? = tracker?.selection?.size()
         
                // More code here
            }
})

如何显示所选项目的数量取决于您。目前,我建议您直接在活动的操作栏中显示该号码。(可选)您还可以更改操作栏的背景颜色,以便让用户知道列表中有活动选择。以下代码显示了如何:

if(nItems!=null && nItems > 0) {
 
    // Change title and color of action bar
 
    title = "$nItems items selected"
    supportActionBar?.setBackgroundDrawable(
            ColorDrawable(Color.parseColor("#ef6c00")))
} else {
 
    // Reset color and title to default values    
 
    title = "RVSelection"
    supportActionBar?.setBackgroundDrawable(
            ColorDrawable(getColor(R.color.colorPrimary)))
}

如果再次运行该应用程序,您现在应该看到标题更改以反映您选择的列表项的数量。

应用程序显示所选项目的计数

结论

在本教程中,您学习了如何使用RecyclerView Selection插件库为RecyclerView 窗口小部件添加简单的项目选择支持。您还学习了如何动态更改所选项目的外观,以便用户可以将它们与未选择的项目区分开来。

想学习更多Android知识,或者获取相关资料请加入Android技术开发交流2群:935654177。本群可免费获取Gradle,RxJava,小程序,Hybrid,移动架构,NDK,React Native,性能优化等技术教程!
FMNX`NW45P@2{~8XII3B4VF.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容