通过一个简单例子理解 RecyclerView.ItemDecoration

一、前言

RecyclerView 是从5.0推出的 MD 风格的控件。RecyclerView 之前有 ListView、GridView,但是功能很有限,例如 ListView 只能实现垂直方向上的滑动等。但是存在则合理,ListView 却没有被官方标记为 @Deprecated,有兴趣的同学可以去找下相关资料,主要看下 RecyclerView 和 ListView 的布局重用机制。在 ListView 文档上可以发现一句话

For a more modern, flexible, and performant approach to displaying lists, use RecyclerView

翻译为:要获得更现代、更灵活、更高效的列表显示方法,请使用 RecyclerView
就是说 RecyclerView 很牛逼

A flexible view for providing a limited window into a large data set

本文主题是 RecyclerView#ItemDecoration。Decoration:装饰,装潢;装饰品;装饰器。顾名思义就是给 Item 一些打扮的。ItemDecoration 允许应用程序从适配器的数据集中为特定的 ItemViews 添加特殊的图形和布局偏移量。这对于在 Item 之间绘制分隔线,突出显示,分组等等非常有用。
下面进入主题。

二、效果

看图

效果图.png

描述:RecyclerView 最上面有一个块红色的条,滚动是红色条也跟着向上滚;除了最后一个每个 Item 都有一条分割线,并且分割线距离左边有一定的距离;前个 Item 右边有一个图标。

三、实现步骤

ItemDecoration 是一个抽象类一共有6个方法,其中三个标记为 @Deprecated, 所以真正用的方法是以下三个:

  1. getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State)
  2. onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
  3. onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State)

1、方法介绍

按照执行顺序先后:
1. getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State)
为特定的 ItemView 设置偏移量,此方法在 RecyclerView 测量 ItemView 后执行。
参数说明:
1)outRect:ItemView 边界,可用理解为原来 ItemView padding。
例如:outRect.set(50, 50, 50, 50),参数顺序为 “左上右下”,原来的 ItemView 上下左右都会扩展 50 像素,如下图

outRect.set(50, 50, 50, 50)

outRect.set(0, 0, 100, 50)

 2)view:RecyclerView 的 ItemView(将被装饰的View),outRect.set() 设置的边界针对的是这个 View

 3)parent:RecyclerView

 4)state:当前 RecyclerView 的状态

2. onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
此方法在 RecyclerView 的 onDraw(Canvas c) 方法中调用,在 ItemView 下层绘制内容,绘制的内容可能会被 ItemView 遮挡住
1)c:画布,和自定义 View 那样把内容绘制在画布上。
如图:假设只有一个 ItemView, 红色区域是绘制的内容,大小是 100x100 像素从顶点开始绘制 c.drawRect(Rect(0, 0, 100, 100), mPaint),在 getItemOffsets 设置 outRect.set(50, 50, 0, 0)

ItemView覆盖绘制的内容

3. onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
此方法在 RecyclerView 的 draw(Canvas c) 方法中调用和 onDraw(Canvas c) 一样,区别在于此方法绘制的内容有可能会覆盖 ItemView。
还是上面的例子,如果 c.drawRect(Rect(0, 0, 100, 100), mPaint) 放在 onDrawOver() 效果如下图:

内容覆盖 ItemView

ItemView 的三个方法就简单讲到这里,下面上代码。

2、分割线代码

新建一个类 ItemLineDivider.kt, 贴出部分代码

class ItemLineDivider(@RecyclerView.Orientation var orientation: Int = VERTICAL) : RecyclerView.ItemDecoration() {

    //边界
    private val mBounds: Rect = Rect()
    private val mPaint = Paint()

    @ColorInt
    var dividerColor: Int = Color.GRAY
        set(value) {
            mPaint.color = value
        }

    private val defaultSize = 1//默认1像素

    var hasEndDivider = true//是否要最后一个item的分割线

    var dividerWidth = defaultSize//竖线宽度,单位px
    var dividerHeight = defaultSize//横线高度,单位px

    /**分割线左边间距*/
    var leftSpace: Int = 0
    /**分割线右边间距*/
    var topSpace: Int = 0
    /**分割线上方间距*/
    var rightSpace: Int = 0
    /**分割线下方间距*/
    var bottomSpace: Int = 0

    init {
        mPaint.color = dividerColor
        mPaint.isAntiAlias = true
    }

    /**
     * 分割线绘制在ItemView 的下层,
     * 如果 getItemOffsets 中 outRect 四个参数都是 0, 则 ItemView 有背景的情况会把分割线遮挡
     */
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        i("onDraw")
        if (orientation == VERTICAL) {
            drawVertical(c, parent)
        } else {
            drawHorizontal(c, parent)
        }
    }

    /**
     * 为分割线腾出位置
     * [outRect] ItemView 边距
     */
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        i("getItemOffsets")
        if (orientation == VERTICAL) {
            outRect.set(0, 0, 0, dividerHeight)
        } else {
            outRect.set(0, 0, dividerWidth, 0)
        }
    }

    /**
     * 绘制水平分割线
     */
    private fun drawVertical(c: Canvas, parent: RecyclerView) {
        c.save()
        val left: Int
        val right: Int
        if (parent.clipToPadding) {
            left = parent.paddingLeft + leftSpace //左边坐标
            right = if (dividerWidth != defaultSize) {//右边坐标
                left + dividerWidth//设置宽度,以设置的宽度优先
            } else {
                parent.width - parent.paddingEnd - rightSpace
            }
            c.clipRect(left, parent.paddingTop, right, parent.height - parent.paddingBottom)
        } else {
            left = leftSpace
            right = if (dividerWidth != defaultSize) {
                left + dividerWidth
            } else {
                parent.width - rightSpace
            }
        }

        var childCount = parent.childCount
        if (!hasEndDivider) {//最后一个 Item 不绘制分割线
            childCount -= 1
        }
        for (i in 0 until childCount) {
            val child = parent.getChildAt(i)
            parent.getDecoratedBoundsWithMargins(child, mBounds)
            val bottom: Int = mBounds.bottom + Math.round(child.translationY)
            val top: Int = bottom - dividerHeight
            val rect = Rect(left, top, right, bottom)
            c.drawRect(rect, mPaint)
        }
        c.restore()
    }

}

3、顶部条块代码

新建一个类 VerticalItemStartLine.kt

class VerticalItemStartLine : RecyclerView.ItemDecoration() {

   private val mBound = Rect()
   private val mPaint = Paint()
   private val defaultSize = 1
   var lineWidth = defaultSize
   var lineHeight = defaultSize
   @ColorInt
   var color = Color.GRAY
       set(value) {
           mPaint.color = value
       }

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

   private fun drawVertical(c: Canvas, parent: RecyclerView) {
       c.save()
       val child = parent.getChildAt(0)
       val childIndex = parent.getChildAdapterPosition(child)
       if (childIndex == 0) {
           parent.getDecoratedBoundsWithMargins(parent.getChildAt(0), mBound)
           val left = mBound.left
           val right = if (lineWidth == defaultSize) {
               parent.width
           } else {
               lineWidth
           }
           val top = mBound.top
           val bottom = lineHeight + top
           c.drawRect(Rect(left, top, right, bottom), mPaint)
           i(mBound.toShortString() + "\nleft=$left, top=$top, right=$right, bottom=$bottom")
       }
       c.restore()
   }

   override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
       if (parent.getChildAdapterPosition(view) == 0) {//只在第一个头上添加
           outRect.set(0, lineHeight, 0, 0)
       }
   }

}

4、右边标签代码

新建一个类 TopThreeItemDrawOver.kt


class TopThreeItemDrawOver(val drawable: Drawable) : RecyclerView.ItemDecoration() {

    private val width = 100
    private val height = 100

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        i(drawable.bounds.toShortString())
        for (i in 0..2) {//把 drawable 画到前三个 itemView 上
            val child = parent.getChildAt(i)
            val index = parent.getChildAdapterPosition(child)
            val left = parent.width - 50 - width
            val right = left + width
            val space = (child.height - height) / 2
            val top = child.top + space
            val bottom = child.bottom - space
            if (index < 3) {
                drawable.setBounds(left, top, right, bottom)
                drawable.draw(c)
            }

        }

    }

}

5、把上面三个 ItemDecoration 添加到 RecyclerView

private fun init() {
        val myAdapter = MyAdapter(this, getData())

        val layoutManager = LinearLayoutManager(this)

        val itemDecoration = ItemLineDivider(RecyclerView.VERTICAL)

        itemDecoration.apply {
            dividerHeight = 5
            leftSpace = 140
            hasEndDivider = false
        }

        val startItemDecoration = VerticalItemStartLine()
        startItemDecoration.apply {
            lineHeight = 100
            color = Color.RED
        }

        val drawOver = TopThreeItemDrawOver(resources.getDrawable(R.drawable.ic_swap_horiz))

        recycler_view.apply {
            addItemDecoration(startItemDecoration)//头部条块
            addItemDecoration(itemDecoration)//分割线
            addItemDecoration(drawOver)//右边标签
            setHasFixedSize(true)
            setLayoutManager(layoutManager)
            adapter = myAdapter
        }

        myAdapter.notifyDataSetChanged()

 }

四、总结

通过一个简单的例子,可以很好的理解高大上的 ItemDecoration,什么分割线啊也不用在 xml 布局文件里设置了。ItemDecoration 还可以实现时间轴、黏附等效果,这里就不举例了,根据上面的方法解析和例子,再加上自己的想法,可以在 RecyclerView 实现很多效果。我觉得刚开始的话重点去理解 Rect,坐标,偏移量,就可以很好把一个内容绘制到指定位置了。

参考:
RecyclerView 文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容