注册

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 的表现像下面这样


image.png


其实我想要的是这样


image.png


如果我们不使用这个的话,那么我们在编写 xml 文件的时候只能添加 layout_margin 这样的值,而且即便这样在有些场景下也是不好用的。其实也没关系我们可以使用代码控制,比如在 onBindViewHolder 中根据数据的位置写对应的逻辑,像我上面那种我需要把最后一个数据多对应的 layout_margin 给去掉,这样也是完全没问题的,只不过如果采用了这样的方式,首先如果我们把 layout_margin 设置到每一项上,那么将来要复用这个 xml 文件,由于间距不同,我们就没法复用,或者复用也需要在代码中控制。如果使用这个,就会非常的简单,并且不会在 adapter 中再使用代码控制了。


使用这个需要进行两步:



  1. 实现自己的 ItemDecoration 子类;
  2. 添加到 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 状态

这里可以设置 itemRecyclerView 各边的距离。这里需要说明一下,我这里说的到各边的距离指的是啥?


image.png


2. 实现上面的间隔


实现间隔是最简单的,因为我们只需要重写 getItemOffsets 函数,这个函数会在绘制每一项的时候调用,所以在这里我们只需要处理每一项的间隔,下面是重写代码,注意这里的单位并不是 dp ,而是 px ,所以如果需要使用 dp 的话,那么就需要自己转换一下,如果你不知道转换可以定义 dpdimen.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)
}
}
}

代码来源于刘望舒的三部曲,我对代码进行了解释和说明。大家可能在代码中的距离那一块不是很明白,直接看下面的图就很明白的。


1629513919(1).png 注意 top 我只标注了距离当前 Item 的距离,其实不是,其实是距离最上面的距离,这里这样标注是跟代码保持统一;假如上面的红色方框是我们要画的分割线,那么我们要获取的值对应上面的标注。一般 onDrawgetItemOffsets 要配合使用,如果不的话,那么你绘制的也看不见,即便看见了也是不正常的。原因我在上面讲到了, onDraw 绘制会绘制到 Item 的下面,所以如果没有留足空间的话,那么结果就是看不见绘制的内容。


内容还会补充,同时关于 RecyclerView 的将来陆续推出,真正做到完全攻略,从使用到问题解决再到源码分析。

0 个评论

要回复文章请先登录注册