文章目录
一、前言
在以前的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