RecyclerView 添加分割线,ItemDecoration 的实用技巧
官网解释: An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.
我的理解:ItemDecoration 允许我们给 recyclerview
中的 item
添加专门的绘制和布局;比如分割线、强调和装饰等等。
默认 recyclerview
的表现像下面这样
其实我想要的是这样
如果我们不使用这个的话,那么我们在编写 xml
文件的时候只能添加 layout_margin
这样的值,而且即便这样在有些场景下也是不好用的。其实也没关系我们可以使用代码控制,比如在 onBindViewHolder
中根据数据的位置写对应的逻辑,像我上面那种我需要把最后一个数据多对应的 layout_margin
给去掉,这样也是完全没问题的,只不过如果采用了这样的方式,首先如果我们把 layout_margin
设置到每一项上,那么将来要复用这个 xml
文件,由于间距不同,我们就没法复用,或者复用也需要在代码中控制。如果使用这个,就会非常的简单,并且不会在 adapter
中再使用代码控制了。
使用这个需要进行两步:
- 实现自己的
ItemDecoration
子类; - 添加到
recyclerView
中
1. 实现自己的 ItemDecoration
子类
这个类在 androidx.recyclerview.widget.RecyclerView.ItemDecoration
下:
class ItemSeparatorDecoration: RecyclerView.ItemDecoration()
这样就实现了,下面我们看看 ItemDecoration
的源代码,我把将要废弃的 API
都删掉:
abstract class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state) {}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {}
}
发现我们可以重写这三个函数,下面说一下这三个的含义:
1)void onDraw(Canvas c, RecyclerView parent, State state)
参数的含义:
- Canvas c 》 canvas 绘制对象
- RecyclerView 》 parent RecyclerView 对象本身
- State state 》 当前 RecyclerView 的状态
作用就是绘制,可以在任何位置绘制,如果只是想绘制到每一项里面,那么就需要计算出对应的位置。
2)void onDrawOver(Canvas c, RecyclerView parent, State state)
跟上面一样,不同的地方在于绘制的总是在最上面,也就是绘制出来的不会被遮挡。
3)void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
参数的含义:
- Rect outRect 》item 四周的距离对象
- View view 》 当前 view
- RecyclerView 》 parent RecyclerView 本身
- State state 》 RecyclerView 状态
这里可以设置 item
到 RecyclerView
各边的距离。这里需要说明一下,我这里说的到各边的距离指的是啥?
2. 实现上面的间隔
实现间隔是最简单的,因为我们只需要重写 getItemOffsets
函数,这个函数会在绘制每一项的时候调用,所以在这里我们只需要处理每一项的间隔,下面是重写代码,注意这里的单位并不是 dp
,而是 px
,所以如果需要使用 dp
的话,那么就需要自己转换一下,如果你不知道转换可以定义 dp
到 dimen.xml
中,然后直接在代码中获取:
context.resources.getDimensionPixelSize(R.dimen.test_16dp)
其中 R.dimen.test_16dp
就是你定义好的值。
下面看重写的 getItemOffsets
函数:
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
if (parent.getChildLayoutPosition(view) != 0) {
outRect.top = context.resources.getDimensionPixelSize(R.dimen.test_10dp)
}
}
有没有发现很简单,这样就可以实现上边的效果,只不过最常见的应该还是分割线了。
3. 实现分割线
看代码:
class MyItemDivider(val context: Context, orientation: Int) : RecyclerView.ItemDecoration() {
companion object {
// 分割线的 attr
private val ATTRS = intArrayOf(android.R.attr.listDivider)
const val HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL
const val VERTICAL_LIST = LinearLayoutManager.VERTICAL
}
// 分割线绘制所需要的 Drawable ,当然也可以直接使用 Canvas 绘制,只不过我这里使用 Drawable
private var mDivider: Drawable? = null
private var mOrientation: Int? = null
init {
val a = context.obtainStyledAttributes(ATTRS)
mDivider = a.getDrawable(0)
a.recycle()
setOrientation(orientation)
}
/**
* 设置方向,如果是 RecyclerView 是上下方向,那么这里设置 VERTICAL_LIST ,否则设置 HORIZONTAL_LIST
* @param orientation 方向
*/
private fun setOrientation(orientation: Int) {
// 传入的值必须是预先定义好的
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw IllegalArgumentException("invalid orientation")
}
mOrientation = orientation
}
/**
* 开始绘制,这个函数只会执行一次,
* 所以我们在绘制的时候需要在这里把所有项的都绘制,
* 而不是只处理某一项
*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent)
} else {
drawHorizontal(c, parent)
}
}
private fun drawHorizontal(c: Canvas, parent: RecyclerView) {
val top = parent.paddingTop
val bottom = parent.height - parent.paddingBottom
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val left = child.right + params.rightMargin
val right = left + (mDivider?.intrinsicWidth ?: 0)
mDivider?.setBounds(left, top, right, bottom)
mDivider?.draw(c)
}
}
private fun drawVertical(c: Canvas, parent: RecyclerView) {
// 左边的距离,
// 意思是左边从哪儿开始绘制,
// 对于每一项来说,
// 肯定需要将 RecyclerView 的左边的 paddingLeft 给去掉
val left = parent.paddingLeft
// 右边就是 RecyclerView 的宽度减去 RecyclerView 右边设置的 paddingRight 值
val right = parent.width - parent.paddingRight
// 获取当前 RecyclerView 下总共有多少 Item
val childCount = parent.childCount
// 循环把每一项的都绘制完成,如果最后一项不需要,那么这里的循环就少循环一次
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
// 上边的距离就是当前 Item 下边再加上本身设置的 marginBottom
val top = child.bottom + params.bottomMargin
// 下边就简单了,就是上边 + 分割线的高度
val bottom = top + (mDivider?.intrinsicHeight ?: 0)
mDivider?.setBounds(left, top, right, bottom)
mDivider?.draw(c)
}
}
// 这个函数会被反复执行,执行的次数跟 Item 的个数相同
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
// 由于在上面的距离绘制,但是实际上那里不会主动为我们绘制腾出空间,
// 需要重写这个函数来手动调整空间,给上面的绘制不会被覆盖
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider?.intrinsicHeight ?: 0)
} else {
outRect.set(0, 0, mDivider?.intrinsicWidth ?: 0, 0)
}
}
}
代码来源于刘望舒的三部曲,我对代码进行了解释和说明。大家可能在代码中的距离那一块不是很明白,直接看下面的图就很明白的。
注意 top
我只标注了距离当前 Item 的距离,其实不是,其实是距离最上面的距离,这里这样标注是跟代码保持统一;假如上面的红色方框是我们要画的分割线,那么我们要获取的值对应上面的标注。一般 onDraw
和 getItemOffsets
要配合使用,如果不的话,那么你绘制的也看不见,即便看见了也是不正常的。原因我在上面讲到了, onDraw
绘制会绘制到 Item 的下面,所以如果没有留足空间的话,那么结果就是看不见绘制的内容。
内容还会补充,同时关于 RecyclerView
的将来陆续推出,真正做到完全攻略,从使用到问题解决再到源码分析。