文章目录
一、前言
在以前的Android开发过程中,列表使用 ListView, 网格使用 GridView。随着Android不断的发展,官方推出了许多性能更优的控件, RecyclerView 就是其中之一。RecyclerView 是 ListView 和 GridView 的更高级版本,不仅仅性能更优越,也更加的灵活。
二、RecyclerView 使用入门
2.1 添加支持库
RecyclerView 属于 v7 支持库,要使用 RecyclerView 首先要添加 v7 支持库。添加支持库的如下。
dependencies {
// 使用support支持库使用这个
implementation 'com.android.support:recyclerview-v7:28.0.0'
// 使用androidx支持库用这个
implementation 'androidx.recyclerview:recyclerview:1.1.0'
}
注意事项:从 API 28 开始Google官方推荐使用Android Jetpack,包名以androidx开头居多,RecyclerView分为 support 支持库和 androidx 支持库,这两个只能选其一,不能同时存在,请跟项目实际情况选用。
2.2 将 RecyclerView 添加到布局
引入依赖后,就可以在布局中添加 RecyclerView 了。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<!-- 注意:如果使用android support库,这里的类名请使用support库里的 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_default="spread"
app:layout_constraintHeight_default="spread"/>
</androidx.constraintlayout.widget.ConstraintLayout>
2.3 在代码中引用 RecyclerView 并配置
2.3.1 设置布局管理器
现成的布局管理器有 LinearLayoutManager(线性)、StaggeredGridLayoutManager(错位网格)、GridLayoutManager(网格)等。
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview).apply {
// 设置布局管理器
layoutManager = LinearLayoutManager(this@MainActivity)
}
2.3.2 设置列表适配器
列表适配器会创建列表项的视图,并使用新数据替换不再可见的是图像。RecyclerView 的列表适配器必须扩展 RecyclerView.Adapter 类。
2.3.2.1 创建项目布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:textSize="18sp"
android:textColor="#FF000000"
android:maxLines="1"
android:ellipsize="end"/>
<TextView
android:id="@+id/tvDesc"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="5dp"
android:textSize="14sp"
android:textColor="#FF666666"
android:maxLines="3"
android:ellipsize="end"/>
</androidx.constraintlayout.widget.ConstraintLayout>
2.3.2.2 创建项目布局
创建列表适配器必须包含列表项,用来展示列表项的内容,列表项必须扩展 RecyclerView.ViewHolder 类。
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.findViewById(R.id.tvTitle)
val desc: TextView = itemView.findViewById(R.id.tvDesc)
}
2.3.2.3 编写适配器类
适配器类必须扩展 RecyclerView.Adapter 类,主要有三个方法需要重写,包括onCreateViewHolder(构建ViewHolder)、getItemCount(获取列表项目数量)、onBindViewHolder(显示列表项目视图),如下所示:
class MyListAdapter() : RecyclerView.Adapter<ItemViewHolder>() {
val data: ArrayList<String> = ArrayList<String>()
fun addData(d: ArrayList<String>) {
if(d.isNotEmpty()) {
data.addAll(d)
}
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
// 创建ViewHolder对象
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_list_1, parent, false)
return ItemViewHolder(itemView)
}
override fun getItemCount(): Int {
// 获取项目的数量
return data.size
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
// 绑定ViewHolder,这里设置需要展示的数据
holder.title.text = "Item $position"
holder.desc.text = data[position]
}
}
2.3.2.4 为 RecyclerView 设置列表适配器
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview).apply {
// 设置布局管理器
layoutManager = LinearLayoutManager(this@MainActivity)
// 设置适配器
adapter = MyListAdapter().apply {
// 添加数据
val list = ArrayList<String>().apply {
for (i in 0 .. 20) {
add("This is content of $i")
}
}
addData(list)
}
}
经过以上的步骤,你已成功入门了 RecyclerView,下面是效果图:

以上是 LinearLayoutManager 的效果,通过设置属性可以实现一些独特的效果,比如横向的列表。另外,只需要换一种布局管理,就可以实现不一样的布局样式,比如网格布局,这个可以自行尝试。
三、RecyclerView 进阶
经过前面内容的学习,基本的 RecyclerView 的入门就完成了。接下来可以更加深入学习。
3.1 列表分割线
上面的例子的样式中,项目之间缺少了分割线,看起来有点凌乱,我们都知道 ListView 可以直接在布局声明中添加分割线,但是 RecyclerView 没有这个功能。要添加列表项目间的分割线,该如何实现呢?
3.1.1 在列表项目布局中添加分割线
一种最简单的方式就是在列表项目的布局中添加(相信很多人在使用 ListView 的时候也这么干过)。在列表项目的布局中添加分割线,在线性列表布局中比较实用(横向或者纵向),如果在网格布局中,实现起来就非常的不协调了。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:textSize="18sp"
android:textColor="#FF000000"
android:maxLines="1"
android:ellipsize="end"/>
<TextView
android:id="@+id/tvDesc"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="5dp"
android:textSize="14sp"
android:textColor="#FF666666"
android:maxLines="3"
android:ellipsize="end"/>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#FF808080"
app:layout_constraintTop_toBottomOf="@+id/tvDesc"/>
</androidx.constraintlayout.widget.ConstraintLayout>
- 效果图

3.1.2 使用 DividerItemDecoration 添加分割线
使用 DividerItemDecoration 添加分割线,调用 RecyclerView 的 addItemDecoration() 方法进行。
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview).apply {
layoutManager = LinearLayoutManager(this@MainActivity).apply {
orientation = LinearLayoutManager.VERTICAL
}
adapter = MyListAdapter().apply {
val list = ArrayList<String>().apply {
for (i in 0 .. 20) {
add("This is content of $i")
}
}
addData(list)
}
}
recyclerView.addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.VERTICAL))
- 效果

注意事项:
DividerItemDecoration定义时的方向是指分割线进行分割的方向,而不是分割线本身的方向。比如一个垂直的RecyclerView列表,添加的分割线是垂直方向分割列表项目,即VERTICAL,但是分割线的线条是横向摆放的,这是容易搞错的,大家在使用过程中需要多注意。
上面的例子通过 DividerItemDecoration 添加的分割线是默认风格的分割线,如果需要自定义分割线的样式呢?其实这也很简单, DividerItemDecoration 包含了供开发者自定义样式的接口 setDrawable()。通过这个接口,开发者可以自定义自己的样式,比如具有渐变效果的分割线。
- 定义一个渐变的 Drawable
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="0.5dp" />
<gradient android:angle="0"
android:startColor="#FFFF0000"
android:centerColor="#FF00FF00"
android:endColor="#FF0000FF" />
</shape>
- DividerItemDecoration 设置自定义的 Drawable
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview).apply {
layoutManager = LinearLayoutManager(this@MainActivity).apply {
orientation = LinearLayoutManager.VERTICAL
}
adapter = MyListAdapter().apply {
val list = ArrayList<String>().apply {
for (i in 0 .. 20) {
add("This is content of $i")
}
}
addData(list)
}
}
recyclerView.addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.VERTICAL).apply {
setDrawable(resources.getDrawable(R.drawable.list_divider_drawable)!!)
})
- 效果

注意事项:
DividerItemDecoration并没有设定分割线高度的方法,所以给定的 Drawable 需要符合预期的 UI 设计,防止出现分割线大小失衡、被拉伸或者挤压的情形。
3.1.3 自定义列表分割线
DividerItemDecoration 是 RecyclerView 预定义的分割线类,如果无法满足需求,开发者还可以自己自定义分割线,自定义分割线需要继承自 RecyclerView.ItemDecoration 类(DividerItemDecoration 就是继承自该类)。自定义分割线需要覆盖实现以下借个方法:
getItemOffsets():获取给定项目的偏移量( 这是列表项目绘制的偏移量,按分割线的绘制要求设定项目绘制的偏移,实际上就是扩展列表项,这样就可以预留位置绘制分割线,防止分割线与列表项目重叠覆盖)。onDraw():将所有有效的装饰物(Decoration)绘制到RecyclerView提供的画布中。onDrawOver():将所有有效的装饰物(Decoration)绘制到RecyclerView提供的画布中。(PS:笔者看到官方文档描述跟onDraw()一致,实现起来的效果也一样,不知道有啥区别)
class MyDividerItemDecoration(context: Context, orientation: Int): RecyclerView.ItemDecoration() {
companion object {
const val HORIZONTAL = RecyclerView.HORIZONTAL
const val VERTICAL = RecyclerView.VERTICAL
}
private val TAG = "DividerItem"
private val ATTRS = intArrayOf(android.R.attr.listDivider)
private var mDivider: Drawable? = null
/**
* Current orientation. Either [.HORIZONTAL] or [.VERTICAL].
*/
private var mOrientation = 0
private var mBounds = Rect()
init {
val a = context.obtainStyledAttributes(ATTRS)
mDivider = a.getDrawable(0)
if (mDivider == null) {
Log.w(TAG,
"@android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()"
)
}
a.recycle()
setOrientation(orientation)
}
fun setOrientation(orientation: Int) {
if(orientation != HORIZONTAL && orientation != VERTICAL) {
throw IllegalArgumentException("Orientation value is invalid")
}
this.mOrientation = orientation
}
fun setDrawable(drawable: Drawable) {
mDivider = drawable
}
/**
* 这是列表项目绘制的偏移量,按分割线的绘制要求设定项目绘制的偏移量,这样就可以预留位置绘制分割线,防止分割线与列表项目重叠覆盖
* @param outRect
* @param view
* @param state
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0)
return
}
if (mOrientation == DividerItemDecoration.VERTICAL) {
// 绘制项目时,底部向下偏移绘制分割线高度的内容(也就是绘制项目的时候,底部多绘制分割线高度的空白部分,用来绘制分割线)
outRect.set(0, 0, 0, mDivider!!.intrinsicHeight)
} else {
outRect.set(0, 0, mDivider!!.intrinsicWidth, 0)
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (parent.layoutManager == null || mDivider == null) {
return
}
if (mOrientation == DividerItemDecoration.VERTICAL) {
drawVertical(c, parent)
} else {
drawHorizontal(c, parent)
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
}
/**
* 绘制垂直方向的分割线(分割线是横向的)
* @param canvas 画布
* @param parent RecyclerView
*/
private fun drawVertical(canvas: Canvas, parent: RecyclerView) {
canvas.save()
// 计算分割线绘制区域
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(left, parent.paddingTop, right,
parent.height - parent.paddingBottom)
} else {
left = 0
right = parent.width
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
parent.getDecoratedBoundsWithMargins(child, mBounds)
val bottom = mBounds.bottom + child.translationY.roundToInt()
val top = bottom - mDivider!!.intrinsicHeight
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
}
canvas.restore()
}
/**
* 绘制水平方向的分割线(分割线是垂直的)
* @param canvas 画布
* @param parent RecyclerView
*/
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView ) {
canvas.save()
// 计算分割线绘制区域
val top: Int
val bottom: Int
if (parent.clipToPadding) {
top = parent.paddingTop
bottom = parent.height - parent.paddingBottom
canvas.clipRect(parent.paddingLeft, top,
parent.width - parent.paddingRight, bottom)
} else {
top = 0
bottom = parent.height
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, mBounds)
val right = mBounds.right + child.translationX.roundToInt()
val left = right - mDivider!!.intrinsicWidth
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
}
canvas.restore()
}
}
- 效果

说明:
- 以上的例子中,分割线的高度(横向分割线的宽度),都是根据分割线 Drawable 来自行调整的,如果想要固定,可以在绘制分割线时指定分割线大小,但是要记住同时在计算偏移量的时候也是用固定值。
getItemOffsets()是列表项的偏移量,就是对列表项进行扩展,换句话说就是列表项比原来更高或者更宽了,扩展出来的位置用来绘制分割线。但是如果扩展受到限制(例如:屏幕限制),则会通过压缩内容来达到效果(如下图:左右扩展,但是因屏幕限制无法扩展,只能通过缩小内容区域,在两边预留需要的位置)。
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0)
return
}
if (mOrientation == DividerItemDecoration.VERTICAL) {
// 绘制项目时,底部向下偏移绘制分割线高度的内容(也就是绘制项目的时候,底部多绘制分割线高度的空白部分,用来绘制分割线)
outRect.set(100, 0, 100, mDivider!!.intrinsicHeight)
} else {
outRect.set(0, 0, mDivider!!.intrinsicWidth, 0)
}
}

3.1.4 自定义任何的装饰物
以上介绍了通过继承 RecyclerView.ItemDecoration 类实现分割线,Decoration 的含义就是“装饰物”,通过实现这个类,可以往列表项目中添加任何的装饰物。上面的例子稍微调整一下
class MyDividerItemDecoration(context: Context, orientation: Int): RecyclerView.ItemDecoration() {
companion object {
const val HORIZONTAL = RecyclerView.HORIZONTAL
const val VERTICAL = RecyclerView.VERTICAL
}
private val TAG = "DividerItem"
private val ATTRS = intArrayOf(android.R.attr.listDivider)
private var mDivider: Drawable? = null
/**
* Current orientation. Either [.HORIZONTAL] or [.VERTICAL].
*/
private var mOrientation = 0
private var mBounds = Rect()
init {
val a = context.obtainStyledAttributes(ATTRS)
mDivider = a.getDrawable(0)
if (mDivider == null) {
Log.w(TAG,
"@android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()"
)
}
a.recycle()
setOrientation(orientation)
}
fun setOrientation(orientation: Int) {
if(orientation != HORIZONTAL && orientation != VERTICAL) {
throw IllegalArgumentException("Orientation value is invalid")
}
this.mOrientation = orientation
}
fun setDrawable(drawable: Drawable) {
mDivider = drawable
}
/**
* 这是列表项目绘制的偏移量,按分割线的绘制要求设定项目绘制的偏移量,这样就可以预留位置绘制分割线,防止分割线与列表项目重叠覆盖
* @param outRect
* @param view
* @param state
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0)
return
}
if (mOrientation == DividerItemDecoration.VERTICAL) {
// 绘制项目时,底部向下偏移绘制分割线高度的内容(也就是绘制项目的时候,底部多绘制分割线高度的空白部分,用来绘制分割线)
outRect.set(20, 0, 0, mDivider!!.intrinsicHeight)
} else {
outRect.set(0, 20, mDivider!!.intrinsicWidth, 0)
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (parent.layoutManager == null || mDivider == null) {
return
}
if (mOrientation == DividerItemDecoration.VERTICAL) {
drawVertical(c, parent)
} else {
drawHorizontal(c, parent)
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
}
/**
* 绘制垂直方向的分割线(分割线是横向的)
* @param canvas 画布
* @param parent RecyclerView
*/
private fun drawVertical(canvas: Canvas, parent: RecyclerView) {
canvas.save()
// 计算分割线绘制区域
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(left, parent.paddingTop, right,
parent.height - parent.paddingBottom)
} else {
left = 0
right = parent.width
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
parent.getDecoratedBoundsWithMargins(child, mBounds)
val bottom = mBounds.bottom + child.translationY.roundToInt()
val top = bottom - mDivider!!.intrinsicHeight
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
// 绘制左边标志装饰物
ColorDrawable().apply {
setBounds(left, child.top, 20, child.bottom)
color = if(i % 2 == 0) {
Color.RED
} else {
Color.CYAN
}
draw(canvas)
}
}
canvas.restore()
}
/**
* 绘制水平方向的分割线(分割线是垂直的)
* @param canvas 画布
* @param parent RecyclerView
*/
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView ) {
canvas.save()
val top: Int
val bottom: Int
if (parent.clipToPadding) {
top = parent.paddingTop
bottom = parent.height - parent.paddingBottom
canvas.clipRect(parent.paddingLeft, top,
parent.width - parent.paddingRight, bottom)
} else {
top = 0
bottom = parent.height
}
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, mBounds)
val right = mBounds.right + child.translationX.roundToInt()
val left = right - mDivider!!.intrinsicWidth
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(canvas)
// 绘制上边标志装饰物
ColorDrawable().apply {
setBounds(left, child.top, child.right, 20)
color = if(i % 2 == 0) {
Color.RED
} else {
Color.CYAN
}
draw(canvas)
}
}
canvas.restore()
}
}
- 效果

3.2 点击效果
对于列表,如果有点击效果(Selector),用户体验上视觉效果更好,在 ListView 上面,可以直接在 XML 定义中添加点击效果,在 RecylerView 上面没有这个设置。
3.2.1 在列表项目布局中添加点击效果
这种方式实现起来也比较简单,定义一个 Selector 资源,然后在列表项布局中设置 background 就可以了。
- 定义
Selector
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@android:color/darker_gray" />
<item android:state_selected="true"
android:drawable="@android:color/holo_blue_bright" />
<item android:drawable="@android:color/transparent" />
</selector>
- 设置列表项背景
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:clickable="true"
android:background="@drawable/item_selector"
android:focusable="true">
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:textSize="18sp"
android:textColor="#FF000000"
android:maxLines="1"
android:ellipsize="end"/>
<TextView
android:id="@+id/tvDesc"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="5dp"
android:textSize="14sp"
android:textColor="#FF666666"
android:maxLines="3"
android:ellipsize="end"/>
</androidx.constraintlayout.widget.ConstraintLayout>
- 效果

注意事项:列表项布局必须添加
android:clickable="true",否则点击效果无法体现。
3.2.2 在 Android 5.0 以上实现水波纹点击效果
在res 目录下新增一个 drawable-v21 目录,在里面新建 XML 资源文件,资源文件根节点为 ripple
- 示例
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/darker_gray">
<item android:drawable="@android:color/white" />
</ripple>
说明:
<ripple>节点的android:color为波纹效果的颜色;<item>节点的android:drawable为背景。- 如果您的项目
miniSdkVersion不低于 21,可以直接将资源文件放在res/drawable目录下,而不需要新建res/drawble-v21目录。
3.3 RecyclerView 添加列表点击事件
列表展示内容,点击事件是少不了的,对于 RecyclerView 而言,没有像 ListView 那样的 setOnItemClickListener,但是可以使用自己的方式实现点击事件。
3.3.1 在适配器中 onBindViewHolder 添加点击事件
最简单的方式就是在适配器的 onBindViewHolder 方法中,给绑定的 ItemView 添加点击事件。
- 定义一个接口
interface OnItemClickListener {
abstract fun onItemClick(holder: ItemViewHolder, position: Int)
}
- 在适配器类的
onBindViewHolder方法中添加点击事件处理
class MyListAdapter() : RecyclerView.Adapter<ItemViewHolder>() {
val data: ArrayList<String> = ArrayList<String>()
var onItemClickListener: OnItemClickListener? = null
get() = field
set(value) {
field = value
}
fun addData(d: ArrayList<String>) {
if(d.isNotEmpty()) {
data.addAll(d)
}
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
// 创建ViewHolder对象
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_list_1, parent, false)
return ItemViewHolder(itemView)
}
override fun getItemCount(): Int {
// 获取项目的数量
return data.size
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
// 绑定ViewHolder,这里设置需要展示的数据
holder.title.text = "Item $position"
holder.desc.text = data[position]
holder.itemView.setOnClickListener {
onItemClickListener?.onItemClick(holder, position)
}
}
}
- 添加点击响应处理
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview).apply {
layoutManager = LinearLayoutManager(this@MainActivity).apply {
orientation = LinearLayoutManager.VERTICAL
}
adapter = MyListAdapter().apply {
val list = ArrayList<String>().apply {
for (i in 0 .. 20) {
add("This is content of $i")
}
}
addData(list)
onItemClickListener = object: OnItemClickListener {
override fun onItemClick(holder: ItemViewHolder, position: Int) {
Toast.makeText(this@MainActivity, "Item $position clicked", Toast.LENGTH_SHORT).show()
}
}
}
}
- 效果

注意事项:在列表项视图绑定时添加点击事件的方式,可以简单实现点击事件,但是如果要同时实现长按事件,会有冲突,因为
onClick是在触摸事件ACTION_UP回调,但是onLongClick是在触摸事件ACTION_DOWN之后,长时间没有ACTION_UP的时候相应,因此在执行ACTION_UP的时候,依旧会回调onClick,其实处理起来也很简单,只需要在onLongClick回调中,返回值为 true 即可(意思是事件在此处已处理完毕,不再往下传递)。
3.3.2 使用 RecyclerView.OnItemTouchListener 实现点击事件
通过 RecyclerView 的 addOnItemTouchListener 添加 RecyclerView 的点击事件,但是点击事件相应是整个 RecyclerView,需要通过点击事件的坐标通过 findChildViewUnder() 寻找到对应的子 View,然后使用 getChildAdapterPosition() 获取点击所在的列表项目位置(position)。
val child = recyclerView.findChildViewUnder(e.x, e.y)
if(null != child) {
val position = recyclerView.getChildAdapterPosition(child)
Toast.makeText(this@MainActivity, "Item $position was long clicked", Toast.LENGTH_SHORT).show()
}
可以通过 GestureDetectorCompat 来解析 RecyclerView.OnItemTouchListener 监听到的事件,直接获取是点击事件还是长按事件,减少工作量。
gestureDetector = GestureDetectorCompat(this@MainActivity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean {
e?.also {
val child = recyclerView.findChildViewUnder(e.x, e.y)
if(null != child) {
val position = recyclerView.getChildAdapterPosition(child)
Toast.makeText(this@MainActivity, "Item $position was clicked", Toast.LENGTH_SHORT).show()
}
}
return super.onSingleTapUp(e)
}
override fun onLongPress(e: MotionEvent?) {
e?.also {
val child = recyclerView.findChildViewUnder(e.x, e.y)
if(null != child) {
val position = recyclerView.getChildAdapterPosition(child)
Toast.makeText(this@MainActivity, "Item $position was long clicked", Toast.LENGTH_SHORT).show()
}
}
super.onLongPress(e)
}
})
- 完整代码
recyclerView.addOnItemTouchListener(object: RecyclerView.OnItemTouchListener {
var gestureDetector: GestureDetectorCompat
init {
// 定义GestureDetectorCompat对象,快速解析触摸事件,分发为onClick和onLongClick
gestureDetector = GestureDetectorCompat(this@MainActivity, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean {
// 处理点击事件
e?.also {
val child = recyclerView.findChildViewUnder(e.x, e.y)
if(null != child) {
val position = recyclerView.getChildAdapterPosition(child)
Toast.makeText(this@MainActivity, "Item $position was clicked", Toast.LENGTH_SHORT).show()
}
}
return super.onSingleTapUp(e)
}
override fun onLongPress(e: MotionEvent?) {
// 处理长按事件
e?.also {
val child = recyclerView.findChildViewUnder(e.x, e.y)
if(null != child) {
val position = recyclerView.getChildAdapterPosition(child)
Toast.makeText(this@MainActivity, "Item $position was long clicked", Toast.LENGTH_SHORT).show()
}
}
super.onLongPress(e)
}
})
}
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
}
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
// 调用GestureDetectorCompat对象处理分发事件
gestureDetector.onTouchEvent(e)
// 此处不要返回true,否则点击效果将会失效
return false
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
}
})
注意事项:通过
RecyclerView.OnItemTouchListener监听处理触摸事件的方式实现点击,请勿在onInterceptTouchEvent()返回 true,否则将会拦截点击事件,在列表项布局中添加的列表的点击效果将会失效。
3.4 RecyclerView 实现列表项选择
RecyclerView 不像 ListView 可以直接实现列表项选择,需要借助 recyclerview-selection 库,大致的思路是:
- 为列表项添加支持选择状态的背景资源(或者可以显示选中状态的其他标记,例如:
RadioButton); - 构建一个
SelectionTracker对象; - 在适配器的
onBindViewHolder()回调中根据SelectionTracker对象记录的项目选择状态并更新显示;
注意事项:实现列表项的选择,如果使用背景资源标识,必须支持选中状态的效果(
recyclerview-selection需要state_activated状态),否则选中后无法识别,当然,你可以在列表项中添加RadioButton、CheckBox之类的控件,用来标识选中状态。
3.4.1 引入 recyclerview-selection 库
- 首先,需要在项目中引入
recyclerview-selection库:
implementation 'androidx.recyclerview:recyclerview-selection:1.0.0'
3.4.2 构建 SelectionTracker 对象
构建 SelectionTracker 对象,需要准备一些必要的东西:
RecyclerView实例;RecyclerView.Adapter实例,并设置为RecyclerView实例的适配器;- 用来确定选项 Key 类型的
ItemKeyProvider实例; - 用来查询项目详情的
ItemDetailsLookup实例; - 用来确定选择状态存储策略的
StorageStrategy实例。
对于前两项,前面已经介绍过,这里就不再重复。接下来主要详细讲解下另外三个类型的实例对象。
3.4.2.1 确定选项 Key 类型的 ItemKeyProvider 实例
对于列表选择,首先要确定用来标识列表项目的 Key 类型,Key 标识必须唯一,支持的类型有 Long、String、Parcelable。确定了使用哪种类型之后,构建 ItemKeyProvider 实例。
在 recyclerview-selection 库中自带 Long 类型的 Key 的 StableIdKeyProvider,但是需要注意的是,RecyclerView 在默认情况下,适配器中通过 getItemId() 返回的 ID 不是稳定的,所以需要在适配器中使用 setHasStableIds(true) 设定 ID 为稳定的,这样就会使得 ID 和列表的 position 进行绑定,变得稳定。
// 定义 Long 类型的 Key,StableIdKeyProvider
val itemLongKeyProvider = StableIdKeyProvider(recyclerView)
// 修改适配器代码,设置为稳定 ID
class MyListAdapter() : RecyclerView.Adapter<ItemViewHolder>() {
// ........ 此处省略代码
init {
// 设置为稳定 ID
setHasStableIds(true)
}
// ........ 此处省略代码
override fun getItemId(position: Int): Long {
// 稳定 ID,与 position 进行绑定。
return position.toLong()
}
}
StableIdKeyProvider可以满足大多数需求,若无法满足,也可以选择适合自己的 Key 类型。自定义 Key 类型需要扩展 ItemKeyProvider 类,并重写 getKey()和getPosition()两个方法。
getKey():根据项目位置,返回对应的 KeygetPosition(): 根据 Key,获取项目所在的位置
class ItemStringKeyProvider(var adapter: SelectionAdapter): ItemKeyProvider<String>(ItemKeyProvider.SCOPE_MAPPED) {
override fun getKey(position: Int): String? {
return adapter.data[position]
}
override fun getPosition(key: String): Int {
return adapter.data.indexOf(key)
}
}
说明:以上是 String 类型的 ItemKeyProvider 示例,必须注意的是,必须保证 Key 的唯一性。
3.4.2.2 查询项目详情的 ItemDetailsLookup 实例
ItemDetailsLookup 实例用来查询项目的详情,获得 ItemDetails 实例, ItemDetails 对象包含两个必须实现的方法,getPosition() 和 getSelectionKey(),分别是用来获取项目的位置和 Key。
val itemDetailsLookup = object : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
// 根据触摸事件获取点击的View
val view = recyclerView.findChildViewUnder(e.x, e.y)
if (view != null) {
// 根据View获取ViewHolder对象
val itemViewHolder = recyclerView.getChildViewHolder(view)
return object : ItemDetails<Long>() {
override fun getPosition(): Int = itemViewHolder.adapterPosition
override fun getSelectionKey(): Long? = itemViewHolder.itemId
}
}
return null
}
}
3.4.2.3 确定选择状态存储策略的 StorageStrategy 实例
recyclerview-selection 需要将存储状态进行存储,在UI重构时选择状态不丢失(例如:屏幕旋转),针对 Key 类型的不同,库提供了三个对应的存储策略,分别是 LongStorageStrategy、StringStorageStrategy 和 ParcelableStorageStrategy。本文 Key 为 Long 类型,所以选择 LongStorageStrategy 。
val longStorageStrategy = StorageStrategy.createLongStorage();
3.4.2.4 构建 SelectionTracker 对象
所有需要的参数都准备好了,下一步就是构建 SelectionTracker 对象。
var selectionTracker = SelectionTracker.Builder<Long>("selection_id",
recyclerView, StableIdKeyProvider(recyclerView),
itemDetailsLookup, longStorageStrategy)
.withSelectionPredicate(SelectionPredicates.createSelectAnything()) // 设置选择模式,单选/多选
.build()
说明:在构建
SelectionTracker时可设置其他额外的属性,比如选择模式SelectionPredicates,可设置单选(SelectionPredicates.createSelectSingleAnything) /多选(SelectionPredicates.createSelectAnything())模式,更多的属性设置可以参考官方Doc文档 SelectionTracker.Builder.
注意事项:在构建SelectionTracker时所传入的RecyclerView对象必须是已经设置了RecyclerView.Adapter的,否则将抛出IllegalArgumentException异常
3.4.3 根据 SelectionTracker 的选择状态信息更新UI
经过前面的步骤, SelectionTracker 构建完成并且与 RrecyclerView 进行了关联,但是选择状态并不会自动在UI呈现出来,而是需要在 RecyclerView.Adapter 中的 onBindViewHolder() 方法中,对UI进行跟新显示。
class SelectionAdapter() : RecyclerView.Adapter<ItemViewHolder>() {
val data = ArrayList<String>()
// 定义SelectionTracker参数
var tracker: SelectionTracker<Long>? = null
init {
setHasStableIds(true)
}
fun addData(d: ArrayList<String>) {
if(d.isNotEmpty()) {
data.addAll(d)
}
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
// 创建ViewHolder对象
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_list_1, parent, false)
return ItemViewHolder(itemView)
}
override fun getItemCount(): Int {
// 获取项目的数量
return data.size
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
// 绑定ViewHolder,这里设置需要展示的数据
holder.title.text = data[position]
holder.desc.apply {
visibility = View.VISIBLE
text = "This is item $position"
}
// 根据SelectionTracker记录的选择状态,更新UI显示
tracker?.let {
// 如果使用其他标记是否选中(如:RadioButton),可在这里更改控件的状态
// holder.check.isChecked = it.isSelected(position.toLong())
holder.itemView.isActivated = it.isSelected(position.toLong())
}
}
}
注意事项:由于在构建
SelectionTracker时所传入的RecyclerView对象必须是已经设置了RecyclerView.Adapter,所以RecyclerView.Adapter内部的SelectionTracker对象赋值不能在适配器类的构造函数中传入(从逻辑上已经冲突了)。
- 完整代码
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview_selection).apply {
layoutManager = LinearLayoutManager(this@SelectionActivity).apply {
orientation = LinearLayoutManager.VERTICAL
}
addItemDecoration(MyDividerItemDecoration(this@SelectionActivity, MyDividerItemDecoration.VERTICAL).apply {
setDrawable(resources.getDrawable(R.drawable.list_divider_drawable)!!)
})
}
val rcAdapter = SelectionAdapter().apply {
val data = ArrayList<String>()
for (i in 0 until 20) {
data.add("Item $i")
}
addData(data)
}
// 设置适配器
recyclerView.adapter = rcAdapter
// 定义项目详情查询器
val itemDetailsLookup = object : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
// 根据触摸事件获取点击的View
val view = recyclerView.findChildViewUnder(e.x, e.y)
if (view != null) {
// 根据View获取ViewHolder对象
val itemViewHolder = recyclerView.getChildViewHolder(view)
return object : ItemDetails<Long>() {
override fun getPosition(): Int = itemViewHolder.adapterPosition
override fun getSelectionKey(): Long? = itemViewHolder.itemId
}
}
return null
}
}
// 定义选择状态存储策略
val longStorageStrategy = StorageStrategy.createLongStorage();
// 定义SelectionTracker(关联的RecyclerView 必须先设置Adapter)
var selectionTracker = SelectionTracker.Builder<Long>("selection_id",
recyclerView, StableIdKeyProvider(recyclerView),
itemDetailsLookup, longStorageStrategy)
.withSelectionPredicate(SelectionPredicates.createSelectAnything()) // 设置选择模式,单选/多选
.build()
// 设置SelectionTracker对象
rcAdapter.tracker = selectionTracker
- 效果

注意事项:如果通过背景来显示选中状态,并且使用了Android 5.0 以上的系统波纹效果,那么必须让你定义的
ripple支持选中效果显示。将内部点击效果声明的<item>使用<selector>实现即可(如下所示)。<?xml version="1.0" encoding="utf-8"?> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@android:color/darker_gray"> <item> <selector> <item android:state_activated="true" android:drawable="@android:color/holo_blue_bright" /> <item android:drawable="@android:color/white" /> </selector> </item> </ripple>
3.4.4 添加选中状态变更的观察者
如果需要实时观察列表中的选择状态变更,可以为 SelectionTracker 添加一个 SelectionTracker.SelectionObserver 观察者。
- 示例代码
selectionTracker.addObserver(object: SelectionTracker.SelectionObserver<Long>() {
override fun onItemStateChanged(key: Long, selected: Boolean) {
super.onItemStateChanged(key, selected)
}
override fun onSelectionChanged() {
super.onSelectionChanged()
tvMsg.text = "Selected Count: ${selectionTracker.selection.size()}"
}
override fun onSelectionRefresh() {
super.onSelectionRefresh()
}
override fun onSelectionRestored() {
super.onSelectionRestored()
}
})
- 效果(实时显示选中项目数量)

四、参考
[1] Github示例代码:RecyclerviewDemo
[2] RecyclerView Doc
本文详细介绍RecyclerView的使用方法,包括入门教程、分割线添加、点击效果实现、点击事件处理及列表项选择技巧。从添加支持库到自定义列表分割线,再到实现列表项选择,全方位解析RecyclerView的高级应用。
1万+

被折叠的 条评论
为什么被折叠?



