RecyclerView ItemDecoration使用小记

记录RecyclerView ItemDecoration的用法

Posted by XYH on April 15, 2019

记录RecyclerView ItemDecoration的使用方法。

RecyclerView ItemDecoration。

本文记录如何使用ItemDecoration实现RecyclerView分割线跟如何使用ItemDecoration实现stickyHeader,俗称粘性Header。

前言

以前使用ListView的时候设置分割线是一件非常简单的事情,后来Google推出了RecyclerView,但是RecyclerView并没有直接设置分割线的API,不过好在RecyclerView提供的接口够多,其中ItemDecoration接口可以用于干预RecyclerView内部itemView绘制的api。

介绍ItemDecoration

ItemDecoration需要实现的方法只有3个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  public abstract static class ItemDecoration {
        /**
         * 这个方法会在RecyclerView绘制itemView之前调用,并且绘制的内容会在itemView下方。
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView
         */
        public void onDraw(Canvas c, RecyclerView parent, State state) {
           
        }
        /**
         * 这个方法会在RecyclerView绘制itemView之后调用,并且绘制的内容会覆盖在itemView上。
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView.
         */
        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
           
        }
        /**
         * 获取每个itemView的偏移量
         * @param outRect Rect to receive the output.
         * @param view    The child view to decorate
         * @param parent  RecyclerView this ItemDecoration is decorating
         * @param state   The current state of RecyclerView.
         */        
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
          
        }
    }

这些方法在RecyclerView中被调用的位置:

其中ItemDecoration#onDraw(Canvas c, RecyclerView parent, State state)会在RecyclerView

1
2
3
4
5
6
7
8
9
  @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

ItemDecoration#onDrawOver(Canvas c, RecyclerView parent, State state)RecyclerView中的

1
2
3
4
5
6
7
8
9
10
   @Override
    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        xxx
    }    

ItemDecoration#getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)RecyclerView中调用位置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

实现RecyclerView的分割线

简述了上述几个方法之后,如何绘制分割线应该是比较轻松的事情了,好记性不如烂笔头,下次看到这里的时候不要打脸了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
 class StickyItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
    /**
     * 分割线颜色
     */
    var dividerColor = Color.parseColor("#E8E8E8")
    /**
     * 分割线高度
     */
    var dividerHeight = context.dpToPixel(1)
    /**
     * 分割线画笔
     */
    private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
        it.color = dividerColor
    }
    
    /**
     * 这个方法会在RecyclerView绘制itemView之前调用,并且绘制的内容会在itemView下方。
     * @param c Canvas to draw into
     * @param parent RecyclerView this ItemDecoration is drawing into
     * @param state The current state of RecyclerView
     */
    override fun onDraw(c: Canvas?, parent: RecyclerView, state: RecyclerView.State?) {
        super.onDraw(c, parent, state)
        val childCount = parent.childCount
        for (i in 0 until childCount) {
            val itemView = parent.getChildAt(i)
            c?.drawRect(0F, (itemView.top - dividerHeight).toFloat(), itemView.right.toFloat(),
                    itemView.top.toFloat(), dividerPaint)
        }

    }

    /**
     * 这个方法会在RecyclerView绘制itemView之后调用,并且绘制的内容会覆盖在itemView上。
     * @param c Canvas to draw into
     * @param parent RecyclerView this ItemDecoration is drawing into
     * @param state The current state of RecyclerView.
     */
    override fun onDrawOver(c: Canvas?, parent: RecyclerView, state: RecyclerView.State?) {
        super.onDrawOver(c, parent, state)
    }

    /**
     * 获取每个itemView的偏移量
     * @param outRect Rect to receive the output.
     * @param view    The child view to decorate
     * @param parent  RecyclerView this ItemDecoration is decorating
     * @param state   The current state of RecyclerView.
     */
    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect?.top = dividerHeight
    }
}

设置RecyclerViewItemDecoration之后,效果图如下:

image

实现RecyclerView的分割线 + 分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
 class StickyItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
    /**
     * 分割线颜色
     */
    var dividerColor = Color.parseColor("#E8E8E8")
    /**
     * 分割线高度
     */
    var dividerHeight = context.dpToPixel(1)
    /**
     * sticky item高度
     */
    var stickyItemHeight = context.dpToPixel(30)
    /**
     * 绘制sticky
     */
    private val stickyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = 0xFFE8E8E8.toInt()
    }
    /**
     * 绘制sticky文本
     */
    private val stickyTextPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).also {
        it.color = 0xFF999999.toInt()
        it.textSize = context.spToPx(16F)
    }
    /**
     * sticky文本的rect
     */
    private val stickyTextRect = Rect()
    /**
     * 分割线画笔
     */
    private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
        it.color = dividerColor
    }
    /**
     * 获取sticky文本
     */
    private var textCallback: DividerTextCallback? = null


    init {

    }


    /**
     * 这个方法会在RecyclerView绘制itemView之前调用,并且绘制的内容会在itemView下方。
     * @param c Canvas to draw into
     * @param parent RecyclerView this ItemDecoration is drawing into
     * @param state The current state of RecyclerView
     */
    override fun onDraw(c: Canvas?, parent: RecyclerView, state: RecyclerView.State?) {
        super.onDraw(c, parent, state)
        val childCount = parent.childCount
        for (i in 0 until childCount) {
            val itemView = parent.getChildAt(i)
            val position = parent.getChildAdapterPosition(itemView)
            if (isFirstItemInGroup(position)) {
                val top = itemView.top - stickyItemHeight
                val bottom = itemView.top
                c?.drawRect(
                    0F, top.toFloat(), itemView.right.toFloat(),
                    bottom.toFloat(), stickyPaint
                )
                drawStickyText(textCallback?.getDividerText(position), c, top, bottom)
            } else {
                c?.drawRect(
                    0F, (itemView.top - dividerHeight).toFloat(), itemView.right.toFloat(),
                    itemView.top.toFloat(), dividerPaint
                )
            }
        }

    }

    /**
     * 这个方法会在RecyclerView绘制itemView之后调用,并且绘制的内容会覆盖在itemView上。
     * @param c Canvas to draw into
     * @param parent RecyclerView this ItemDecoration is drawing into
     * @param state The current state of RecyclerView.
     */
    override fun onDrawOver(c: Canvas?, parent: RecyclerView, state: RecyclerView.State?) {
        super.onDrawOver(c, parent, state)

    }

    /**
     * 获取每个itemView的偏移量
     * @param outRect Rect to receive the output.
     * @param view    The child view to decorate
     * @param parent  RecyclerView this ItemDecoration is decorating
     * @param state   The current state of RecyclerView.
     */
    override fun getItemOffsets(outRect: Rect?, view: View, parent: RecyclerView, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        val position = parent.getChildAdapterPosition(view)
        if (isFirstItemInGroup(position)) {
            outRect?.top = stickyItemHeight
        } else {
            outRect?.top = dividerHeight
        }
    }

    /**
     * 设置分组内容回调
     */
    fun setDividerTextCallback(action: (position: Int) -> String) {
        textCallback = object : DividerTextCallback {
            override fun getDividerText(position: Int): String = action(position)
        }
    }

    /**
     * 是否是分组的第一个item
     */
    private fun isFirstItemInGroup(position: Int): Boolean {
        return if (position == 0) {
            true
        } else {
            val currentText = textCallback?.getDividerText(position)
            val previewText = textCallback?.getDividerText(position - 1)
            currentText != previewText
        }
    }

    /**
     * 绘制sticky内容
     */
    private fun drawStickyText(text: String?, c: Canvas?, top: Int, bottom: Int) {
        stickyTextRect.left = context.dpToPixel(10)
        stickyTextRect.top = top
        stickyTextRect.right = stickyTextPaint.measureText(text).toInt()
        stickyTextRect.bottom = bottom
        val fontMetrics = stickyTextPaint.fontMetricsInt
        val baseline = (stickyTextRect.bottom + stickyTextRect.top - fontMetrics.bottom - fontMetrics.top) / 2
        c?.drawText(text, stickyTextRect.left.toFloat(), baseline.toFloat(), stickyTextPaint)
    }

    /**
     * 分组text
     */
    interface DividerTextCallback {
        /**
         * 获取分组text
         */
        fun getDividerText(position: Int): String
    }
}

效果图:

image

实现sticky效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
class StickyItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
    /**
     * 分割线颜色
     */
    var dividerColor = Color.parseColor("#E8E8E8")
    /**
     * 分割线高度
     */
    var dividerHeight = context.dpToPixel(1)
    /**
     * sticky item高度
     */
    var stickyItemHeight = context.dpToPixel(30)
    /**
     * 绘制sticky
     */
    private val stickyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = 0xFFE8E8E8.toInt()
    }
    /**
     * 绘制sticky文本
     */
    private val stickyTextPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG).also {
        it.color = 0xFF999999.toInt()
        it.textSize = context.spToPx(16F)
    }
    /**
     * sticky文本的rect
     */
    private val stickyTextRect = Rect()
    /**
     * 分割线画笔
     */
    private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
        it.color = dividerColor
    }
    /**
     * 获取sticky文本
     */
    private var textCallback: DividerTextCallback? = null


    init {

    }


    /**
     * 这个方法会在RecyclerView绘制itemView之前调用,并且绘制的内容会在itemView下方。
     * @param c Canvas to draw into
     * @param parent RecyclerView this ItemDecoration is drawing into
     * @param state The current state of RecyclerView
     */
    override fun onDraw(c: Canvas?, parent: RecyclerView, state: RecyclerView.State?) {
        super.onDraw(c, parent, state)
        val childCount = parent.childCount
        for (i in 0 until childCount) {
            val itemView = parent.getChildAt(i)
            val position = parent.getChildAdapterPosition(itemView)
            if (isFirstItemInGroup(position)) {
                val top = itemView.top - stickyItemHeight
                val bottom = itemView.top
                c?.drawRect(
                    0F, top.toFloat(), itemView.right.toFloat(),
                    bottom.toFloat(), stickyPaint
                )
                drawStickyText(textCallback?.getDividerText(position), c, top, bottom)
            } else {
                c?.drawRect(
                    0F, (itemView.top - dividerHeight).toFloat(), itemView.right.toFloat(),
                    itemView.top.toFloat(), dividerPaint
                )
            }
        }

    }

    /**
     * 这个方法会在RecyclerView绘制itemView之后调用,并且绘制的内容会覆盖在itemView上。
     * @param c Canvas to draw into
     * @param parent RecyclerView this ItemDecoration is drawing into
     * @param state The current state of RecyclerView.
     */
    override fun onDrawOver(c: Canvas?, parent: RecyclerView, state: RecyclerView.State?) {
        super.onDrawOver(c, parent, state)
        val childCount = parent.childCount

        if (childCount > 0) {
            //sticky效果其实就是处理第一个itemView,然后让悬浮内容置于第一个itemView之上。
            val firstView = parent.getChildAt(0)

            val position = parent.getChildAdapterPosition(firstView)
            val text = textCallback?.getDividerText(position)

            if (firstView.bottom <= stickyItemHeight && isFirstItemInGroup(position + 1)) {
                c?.drawRect(0F, 0F, firstView.width.toFloat(), firstView.bottom.toFloat(), stickyPaint)
                drawStickyText(text, c, firstView.bottom - stickyItemHeight, firstView.bottom)
            } else {
                c?.drawRect(0F, 0F, firstView.width.toFloat(), stickyItemHeight.toFloat(), stickyPaint)
                drawStickyText(text, c, 0, stickyItemHeight)
            }
        }
    }

    /**
     * 获取每个itemView的偏移量
     * @param outRect Rect to receive the output.
     * @param view    The child view to decorate
     * @param parent  RecyclerView this ItemDecoration is decorating
     * @param state   The current state of RecyclerView.
     */
    override fun getItemOffsets(outRect: Rect?, view: View, parent: RecyclerView, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        val position = parent.getChildAdapterPosition(view)
        if (isFirstItemInGroup(position)) {
            outRect?.top = stickyItemHeight
        } else {
            outRect?.top = dividerHeight
        }
    }

    /**
     * 设置分组内容回调
     */
    fun setDividerTextCallback(action: (position: Int) -> String) {
        textCallback = object : DividerTextCallback {
            override fun getDividerText(position: Int): String = action(position)
        }
    }

    /**
     * 是否是分组的第一个item
     */
    private fun isFirstItemInGroup(position: Int): Boolean {
        return if (position == 0) {
            true
        } else {
            val currentText = textCallback?.getDividerText(position)
            val previewText = textCallback?.getDividerText(position - 1)
            currentText != previewText
        }
    }

    /**
     * 绘制sticky内容
     */
    private fun drawStickyText(text: String?, c: Canvas?, top: Int, bottom: Int) {
        stickyTextRect.left = context.dpToPixel(10)
        stickyTextRect.top = top
        stickyTextRect.right = stickyTextPaint.measureText(text).toInt()
        stickyTextRect.bottom = bottom
        val fontMetrics = stickyTextPaint.fontMetricsInt
        val baseline = (stickyTextRect.bottom + stickyTextRect.top - fontMetrics.bottom - fontMetrics.top) / 2
        c?.drawText(text, stickyTextRect.left.toFloat(), baseline.toFloat(), stickyTextPaint)
    }

    /**
     * 分组text
     */
    interface DividerTextCallback {
        /**
         * 获取分组text
         */
        fun getDividerText(position: Int): String
    }
}

效果图:

image

源码地址

stickyItemDecoration