深入学习RecyclerView之ItemDecoration的使用

ListView默认有divider属性,但是RecyclerView没有。有人因此就说RecyclerView是low逼?显然不是这样。谷歌已经将divider的功能完全放权给我们开发者去自由定制了。那么我们就来看看正统的实现方式到底是怎样的
相关代码已经上传到github,欢迎大家star、fork

ItemDecoration

这里我就不卖关子了,直接切入正题。我们需要通过RecyclerView提供的ItemDecoration类来给每条item添加装饰,这里的装饰不仅仅是分隔线,还有其他更为复杂的东西。
复杂的东西我们不说,就讲讲如何绘制divider吧?绘制过程大体上是这样的:首先确定可绘制区域范围,然后在这个区域上进行绘制,绘制既可以在itemView的下方也可以在itemView的上方
初步了解绘制过程之后,我们就来看下ItemDecoration类具体有什么功能

ItemDecoration类中有3个方法(抛开已过时的不算):getItemOffsetsonDrawonDrawOver

深入学习RecyclerView之ItemDecoration的使用_第1张图片
ItemDecoration

getItemOffsets:

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), parent);
}

此方法作用于每个itemView,使用时直接分别用outRect.top=x、outRect.bottom=x、outRect.left=x、outRect.right=x在itemView的上下左右方向撑出x像素的空间,效果类似于给每个itemView添加了padding值,视图向中间收缩。此方法直接对每个itemView生效,参数中的view就是当前item的视图。在使用过程中我们只需处理特殊项,无需遍历(如首项尾项可能分别不需要上分割线和下分割线,此时无需为他们撑开上下空间)。

下图应该很形象的展示设置后的效果

深入学习RecyclerView之ItemDecoration的使用_第2张图片
getItemOffsets

onDraw:

public void onDraw(Canvas c, RecyclerView parent, State state) {
        onDraw(c, parent);
}

此方法使用Canvas在itemView的下层空间进行绘制,所以可能被itemView遮挡住,因此一定要在getItemOffsets方法撑开的空白区域内进行绘制。这个方法的参数中并没有提供每个item的视图,所以要通过遍历拿到要绘制的item的视图,根据视图的位置确定Decoration的绘制位置。

onDrawOver:

public void onDrawOver(Canvas c, RecyclerView parent, State state) {
        onDrawOver(c, parent);
}

此方法使用Canvas在itemView的上层空间进行绘制,使用方法和onDraw方法相同,不同的是可能会遮挡Item的视图内容。所以使用时一定要把内容绘制在正确的位置上,不要遮挡了界面上的有效内容。

需要注意的是,此处没有任何点击响应事件

实战

下面我们通过几个小例子来学习一下

  1. 绘制水平分隔线与垂直分隔线

这个应该算最寻常的需求了

我准备绘制一条蓝色的divider,这个divider是一个drawable



    
    

线有了就开始绘制吧,首先继承ItemDecoration

class VerticalDecoration : RecyclerView.ItemDecoration() {
    override fun onDrawOver(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.onDrawOver(c, parent, state)
    }

    override fun onDraw(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.onDraw(c, parent, state)
    }

    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
    }
}

当然这里我们不需要onDrawOver。我们仅需要显示底部的分隔线,所以rect只要设置底部即可,值为drawable的高度。注意考虑到itemView的margin还有其可能存在的属性动画效果

class Divider(val context: Context, val drawable: Drawable) : RecyclerView.ItemDecoration() {

    override fun onDraw(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.onDraw(c, parent, state)
        if (parent != null) {
            for (i in 0 until parent.childCount) {
                val child = parent.getChildAt(i)
                val params = child.layoutParams as RecyclerView.LayoutParams
                val left = parent.paddingLeft + params.leftMargin + child.translationX.toInt()
                val right = parent.width - params.rightMargin + child.translationX.toInt()
                val top = child.bottom + params.bottomMargin + child.translationY.toInt()
                val bottom = top + drawable.intrinsicHeight
                drawable.bounds = Rect(left, top, right, bottom)
                drawable.draw(c)
            }
        }
    }

    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect?.set(Rect(0, 0, 0 ,drawable?.intrinsicHeight))
    }
}

看看效果

深入学习RecyclerView之ItemDecoration的使用_第3张图片
横向分隔线

横向的看完了,那纵向的应该问题也不大吧。就是底部变成右侧而已了。这里也不啰嗦了,小改造一下

class Divider(val context: Context, val drawable: Drawable, val orientation: Int = LinearLayoutManager.VERTICAL) : RecyclerView.ItemDecoration() {

    override fun onDraw(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.onDraw(c, parent, state)
        if (parent != null) {
            for (i in 0 until parent.childCount) {
                val child = parent.getChildAt(i)
                val params = child.layoutParams as RecyclerView.LayoutParams
                var left = 0
                var right = 0
                var top = 0
                var bottom = 0
                if (orientation == LinearLayoutManager.VERTICAL) {
                    left = parent.paddingLeft + params.leftMargin + child.translationX.toInt()
                    right = parent.width - params.rightMargin + child.translationX.toInt()
                    top = child.bottom + params.bottomMargin + child.translationY.toInt()
                    bottom = top + drawable.intrinsicHeight
                }
                else {
                    left = child.right + params.rightMargin + child.translationX.toInt()
                    right = left + drawable.intrinsicWidth
                    top = child.top - params.topMargin + child.translationY.toInt()
                    bottom = child.bottom + params.bottomMargin + child.translationY.toInt()
                }
                drawable.bounds = Rect(left, top, right, bottom)
                drawable.draw(c)
            }
        }
    }

    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        if (orientation == LinearLayoutManager.VERTICAL) {
            outRect?.set(Rect(0, 0, 0 ,drawable?.intrinsicHeight))
        }
        else {
            outRect?.set(Rect(0, 0 ,drawable?.intrinsicWidth, 0))
        }
    }
}
深入学习RecyclerView之ItemDecoration的使用_第4张图片
纵向分隔线
  1. 绘制表格分隔线
    其实你把之前横向纵向都搞清楚了,这个也没什么难度,都是依葫芦画瓢
class GridDivider constructor(val context: Context, val dividerDrawable: Drawable) : RecyclerView.ItemDecoration() {

    override fun onDraw(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.onDraw(c, parent, state)
        drawLeft(c, parent)
        drawTop(c, parent)
        drawRight(c, parent)
        drawBottom(c, parent)
    }

    private fun drawTop(c: Canvas?, parent: RecyclerView?) {
        if (parent != null) {
            for (i in 0 until parent.childCount) {
                val child = parent.getChildAt(i)
                val params = child.layoutParams as RecyclerView.LayoutParams
                val left = child.left - params.leftMargin + child.translationX.toInt()
                val right = child.right + params.rightMargin + child.translationX.toInt()
                val bottom = child.top - params.topMargin + child.translationY.toInt()
                val top = bottom - dividerDrawable?.intrinsicHeight
                dividerDrawable?.bounds = Rect(left, top, right, bottom)
                dividerDrawable?.draw(c)
            }
        }
    }

    private fun drawLeft(c: Canvas?, parent: RecyclerView?) {
        if (parent != null) {
            for (i in 0 until parent.childCount) {
                val child = parent.getChildAt(i)
                val params = child.layoutParams as RecyclerView.LayoutParams
                val right = child.left - params.leftMargin + child.translationX.toInt()
                val left = right - dividerDrawable?.intrinsicWidth
                val top = child.top - params.topMargin + child.translationY.toInt()
                val bottom = child.bottom + params.bottomMargin + child.translationY.toInt()
                dividerDrawable?.bounds = Rect(left, top, right, bottom)
                dividerDrawable?.draw(c)
            }
        }
    }

    private fun drawBottom(c: Canvas?, parent: RecyclerView?) {
        if (parent != null) {
            for (i in 0 until parent.childCount) {
                val child = parent.getChildAt(i)
                val params = child.layoutParams as RecyclerView.LayoutParams
                val left = child.left - params.leftMargin + child.translationX.toInt()
                val right = child.right + params.rightMargin + child.translationX.toInt()
                val top = child.bottom + params.bottomMargin + child.translationY.toInt()
                val bottom = top + dividerDrawable?.intrinsicHeight
                dividerDrawable?.bounds = Rect(left, top, right, bottom)
                dividerDrawable?.draw(c)
            }
        }
    }

    private fun drawRight(c: Canvas?, parent: RecyclerView?) {
        if (parent != null) {
            for (i in 0 until parent.childCount) {
                val child = parent.getChildAt(i)
                val params = child.layoutParams as RecyclerView.LayoutParams
                val left = child.right + params.rightMargin + child.translationX.toInt()
                val right = left + (dividerDrawable?.intrinsicWidth ?: 0)
                val top = child.top - params.topMargin + child.translationY.toInt()
                val bottom = child.bottom + params.bottomMargin + child.translationY.toInt()
                dividerDrawable?.bounds = Rect(left, top, right, bottom)
                dividerDrawable?.draw(c)
            }
        }
    }

    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        if (parent != null) {
            val position = parent.getChildAdapterPosition(view)
            val manager = parent.layoutManager as GridLayoutManager
            when {
            // 左侧一排加顶上、左边、右边的线
                position % manager.spanCount == 0 -> outRect?.set(Rect(dividerDrawable?.intrinsicWidth, dividerDrawable?.intrinsicHeight, dividerDrawable?.intrinsicWidth, 0))
            // 其余加顶上的线、右边的线
                else -> outRect?.set(Rect(0, dividerDrawable?.intrinsicHeight, dividerDrawable?.intrinsicWidth, 0))
            }
        }
    }
}

其实我这边有一个困惑的地方,我没有画底部的线条,但是它却自己添加进来了,我有点晕乎乎的


深入学习RecyclerView之ItemDecoration的使用_第5张图片
GridDivider
  1. 绘制悬停Section
    这个功能一般多用在联系人列表,我在之前的文章中也实现过相应的功能,用的是控制View的移动,那个更实用一点
    我们需要在内容发生变化的那一列itemView的头部画此Section区域。怎么判断itemView内容是否与之前的那个不同呢?这个就需要交给adapter去做判断了,然后将判断的结果保存在itemView的tag上,这样这里的ItemDecoration就能通过view参数拿到这个tag了
    先来看看adapter的代码,我创建一个SectionBean对象存储新内容判断结果
class StickyHeaderAdapter() : RecyclerView.Adapter() {

    var context: Context? = null
    var beans: ArrayList? = null

    constructor(context: Context, beans: ArrayList) : this() {
        this.context = context
        this.beans = beans
    }

    override fun onBindViewHolder(holder: StickyHeaderHolder?, position: Int) {
        holder?.tv_adapter!!.text = beans!![position]

        val section = SectionBean()
        if (position == 0) {
            section.start = true
        }
        else {
            section.start = beans!![position] != beans!![position-1]
        }
        holder?.layout_adapter.tag = section
    }

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): StickyHeaderHolder {
        val view: View = LayoutInflater.from(context).inflate(R.layout.adapter_main, parent, false)
        return StickyHeaderHolder(view)
    }

    override fun getItemCount(): Int {
        return if (beans == null) {
            0
        }
        else {
            beans!!.size
        }
    }

    class StickyHeaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val tv_adapter: TextView = itemView.find(R.id.tv_adapter)
        val layout_adapter: LinearLayout = itemView.find(R.id.layout_adapter)
    }
}

然后我们先画Head线,这里我将高度写死了100px,实际开发中需要自己计算

class StickyHeader(val context: Context, val dividerDrawable: Drawable = ContextCompat.getDrawable(context, R.drawable.item_divider)) : RecyclerView.ItemDecoration() {

    var paint: Paint? = null

    init {
        paint = Paint()
        paint?.color = Color.BLACK
        paint?.textSize = 30.0f
        paint?.style = Paint.Style.FILL
    }

    override fun onDraw(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.onDraw(c, parent, state)

        drawTop(c, parent)
    }

    private fun drawTop(c: Canvas?, parent: RecyclerView?) {
        for (i in 0 until parent?.childCount!!) {
            val child = parent?.getChildAt(i)
            val params = child.layoutParams as RecyclerView.LayoutParams
            val bottom = child.top - params.topMargin + parent.getChildAt(i).translationY.toInt()
            val top = bottom - 100
            val left = child.left + parent.getChildAt(i).translationX.toInt()
            val right = child.right + parent.getChildAt(i).translationX.toInt()
            dividerDrawable?.bounds = Rect(left, top, right, bottom)
            if ((child.tag as SectionBean).start) {
                dividerDrawable?.draw(c)

                val fontMetrics: Paint.FontMetricsInt  = paint!!.fontMetricsInt
                val baseline = (dividerDrawable?.bounds.bottom + dividerDrawable?.bounds.top - fontMetrics.bottom - fontMetrics.top) / 2
                c?.drawText(((child as LinearLayout).getChildAt(0) as TextView).text.toString(), left.toFloat(), baseline.toFloat(), paint)
            }
        }
    }

    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        if ((view?.tag as SectionBean).start) {
            outRect?.set(0, 100, 0 ,0)
        }
        else {
            outRect?.set(0, 0, 0 ,0)
        }
    }
}

OK没问题

深入学习RecyclerView之ItemDecoration的使用_第6张图片
Header线

下面就是重点,我们需要在itemView的上方绘制Section,所以就需要使用到onDrawOver方法了。这时候我们在滚动过程中需要实时调整Section的位置,如果正好处于新老交替的时候,Section就要跟随老的尾巴移动到屏幕上方去,已被新的所取代,这时候Section的底部位置是不是就跟老itemView底部保持一致。一般情况下,Section的位置就是恒定不变的

    override fun onDrawOver(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.onDrawOver(c, parent, state)
        drawSection(c, parent)
    }

    private fun drawSection(c: Canvas?, parent: RecyclerView?) {
        val view0 = parent?.getChildAt(0)
        val view1 = parent?.getChildAt(1)

        if (view0 != null && view1 != null) {
            val left = view0.left
            val right = view1.right
            var top = 0
            var bottom = 100
            if (!(view0.tag as SectionBean).start && (view1.tag as SectionBean).start && view0?.bottom!! < 100) {
                bottom = view0?.bottom!!
                top = bottom - 100
            }
            dividerDrawable?.bounds = Rect(left, top, right, bottom)
            dividerDrawable?.draw(c)

            val fontMetrics: Paint.FontMetricsInt  = paint!!.fontMetricsInt
            val baseline = (dividerDrawable?.bounds.bottom + dividerDrawable?.bounds.top - fontMetrics.bottom - fontMetrics.top) / 2
            c?.drawText(((view0 as LinearLayout).getChildAt(0) as TextView).text.toString(), left.toFloat(), baseline.toFloat(), paint)
        }
    }
深入学习RecyclerView之ItemDecoration的使用_第7张图片
最终效果

其实这个也不是特别的完美,毕竟这里没有点击事件,还是蛮遗憾的

参考文章

ItemDecoration类的使用

你可能感兴趣的:(深入学习RecyclerView之ItemDecoration的使用)