您的位置:首页 > 其它

RecyclerView之ItemDecoration详解

2017-03-17 21:08 267 查看
关于RecyclerView的ItemView装饰,之前一直用官方Demo的
DividerItemDecoration
,并没有认真地去理解
ItemDecoration
的用法,也没能体会到
ItemDecoration
的强大,直到要用到横向的RecyclerView,而且最左边的和最右边的Item要留出间隔(虽然clip结合padding可以实现),才认真地理解一下
ItemDecoration
。 
RecyclerView
可以多次调用
addItemDecoration(ItemDecoration
decor)
addItemDecoration(ItemDecoration decor, int index)
方法有序地为RecyclerView添加ItemDecoration,ItemDecoration会影响每一个ItemView的测量和绘制。 

先看一下不加ItemDecoration时的RecyclerView: 



<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.frank.lollipopdemo.MainActivity">

<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="100px"
android:layout_height="500px"
android:background="#CCCCCC" />

</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100px"
android:layout_height="100px"
android:background="#99FFFF00"
android:gravity="center"
android:orientation="horizontal">

<ImageView
android:id="@+id/iv_logo"
android:layout_width="50px"
android:layout_height="50px" />

<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/>

</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

为了方便,这里尺寸都使用px,建议使用dp。

RecyclerView宽100px高500px,背景为灰色#CCCCCC。每个ItemView宽100px高100px,背景为黄色#99FFFF00。 
ItemDecoration
RecyclerView
的静态内部类,用来向ItemView绘制一些装饰以及调整ItemView的偏移。它有只有三个方法:
getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)

onDraw(Canvas c, RecyclerView parent, State state)

onDrawOver(Canvas c, RecyclerView parent, State state)


getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
方法,直观一点说,就是用来设置ItemView的inset(内嵌偏移)的,类似于
InsetDrawable
,可以看成在ItemView的外面包裹一层偏移。 

我们先让每个ItemView下面空出50px来: 



public class ItemDecor extends RecyclerView.ItemDecoration {

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.set(0, 0, 0, 50);
}

}
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8

让它上下左右都空出50px来: 



public class ItemDecor extends RecyclerView.ItemDecoration {

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.set(50, 50, 50, 50);
}

}
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8

可以看到,我们通过设置outRect的left, top, right, bottom属性值就可以让ItemView产生相应的偏移(内嵌),那RecyclerView是怎么根据outRect的这四个属性值设置ItemView的inset的呢? 
RecyclerView
是一个自定义的
ViewGroup
,每个ItemView都是它的child,那它又是怎样通过LayoutManager测量并布局ItemView的呢? 

看一下
LayoutManager
measureChildWithMargins
方法(
measureChild
方法与之类似,只是没有ItemView的margin而已):
/**
* 使用标准测量策略测量ItemView,
* 把RecyclerView的padding、所有已经添加的ItemDecoration尺寸、ItemView的margin都算在内
*
* <p>如果RecyclerView可以在两个维度滚动,那么调用者可能会传0给widthUsed和heightUsed</p>
*
* @param child 要测量的子view(ItemView)
* @param widthUsed 已经被其它ItemDecoration占用的宽度(px)
* @param heightUsed 已经被其它ItemDecoration占用的高度(px)
*/
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

// 累加当前ItemDecoration宽高值
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;

final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() +
lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() +
lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
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
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

可以看到测量child时,需要调用
RecyclerView
getItemDecorInsetsForChild(View
child)
方法获得ItemDecoration的inset:
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 如果不是dirty数据,直接返回
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
// 如果是dirty数据,重新计算
final Rect insets = lp.mDecorInsets;
// 重置inset
insets.set(0, 0, 0, 0);
//将inset设置为所有已添加的ItemDecoration的getItemOffsets的累加(因为RecyclerView可能添加了多个ItemDecoration)
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第15行,调用了
ItemDecoration
getItemOffsets(Rect
outRect, View view, RecyclerView parent, State state)
方法,所以我们在
getItemOffsets()
方法中对outRect的设置会被当做ItemView的inset进行测量,inset就像padding和margin一样,会影响view的尺寸和位置。 

好了,我们大概知道了我们对outRect的设置是怎样对ItemView产生影响的(RecyclerView和LayoutManager共同协调测量ItemView的逻辑有点复杂,有时间要认真看一下),接下来,我们就可以利用outRect随便调整ItemView的位置,就像我们平时自定义ViewGroup时写
onLayout()
方法layout子View一样,是不是很灵活啊。 

然后就是绘制ItemDecoration了。
onDraw()
的绘制会先于ItemView的绘制,所以如果你在
onDraw()
方法中绘制的东西在ItemView边界内,就会被ItemView盖住。而
onDrawOver()
会在ItemView绘制之后再绘制,所以如果你在
onDrawOver()
方法中绘制的东西在ItemView边界内,就会盖住ItemView。简单点说,就是先执行ItemDecoration的
onDraw()
、再执行ItemView的
onDraw()
、再执行ItemDecoration的
onDrawOver()
。由于和RecyclerView使用的是同一个Canvas,所以你想在Canvas上画什么都可以,就像我们平时自定义View时写
onDraw()
方法一样。 

我们先在RecyclerView上边画一个圆形: 



public class ItemDecor extends RecyclerView.ItemDecoration {

Paint mPaint;

public ItemDecor() {
mPaint = new Paint();
mPaint.setColor(0x99FF0000);
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
c.drawCircle(50, 30, 30, mPaint);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

只需要在Canvas中你想要绘制的位置绘制你想画的东西就行了,什么矩形、渐变、Bitmap等等,没有做不到只有你想不到。 

那我想绘制分隔线,怎么知道每个ItemView的位置呢?很简单,遍历一下RecyclerView的child就行了: 



public class ItemDecor extends RecyclerView.ItemDecoration {

Paint mPaint;

public ItemDecor() {
mPaint = new Paint();
mPaint.setColor(0x99FF0000);
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin +
Math.round(ViewCompat.getTranslationY(child));
final int bottom = top + 50;
c.drawRect(left, top, right, bottom, mPaint);
}
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.set(0, 0, 0, 50);
}
}
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
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

如果想让第一个ItemView之前也有一个红色分割线怎么办?也很简单,先给第一个ItemView设置insetTop和insetBottom内嵌,其它的ItemView只设置insetBottom内嵌:
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int itemPosition = parent.getChildAdapterPosition(view);
int dataSize = parent.getAdapter().getItemCount();
if (itemPosition == 0) {
outRect.set(0, 50, 0, 50);
} else {
outRect.set(0, 0, 0, 50);
}
}
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10

然后绘制的时候,在第一个ItemView的insetTop区域再绘制一个分割线就行了:
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top1 = child.getTop() - params.bottomMargin -
Math.round(ViewCompat.getTranslationY(child)) - 50;
final int bottom1 = top1 + 50;
final int top2 = child.getBottom() + params.bottomMargin +
Math.round(ViewCompat.getTranslationY(child));
final int bottom2 = top2 + 50;
c.drawRect(left, top2, right, bottom2, mPaint);
if (i == 0) {
c.drawRect(left, top1, right, bottom1, mPaint);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


 

我们再玩一个文本ItemDecoration: 



public class ItemDecor extends RecyclerView.ItemDecoration {

Paint mPaint;

public ItemDecor() {
mPaint = new Paint();
mPaint.setColor(0x99FF0000);
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
final int left = parent.getPaddingLeft();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getTop() - params.bottomMargin -
Math.round(ViewCompat.getTranslationY(child)) - 30;
mPaint.setTextSize(20f);
final int adapterPosition = parent.getChildAdapterPosition(child);
c.drawText("item:" + adapterPosition, left + 5, top + 20, mPaint);
}
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.set(0, 30, 0, 0);
}
}
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
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 class SkillRatingDistributionItemDecor extends RecyclerView.ItemDecoration {

private Paint mPaint;
private Paint mValuePaint;
private PathEffect mDashPathEffect;

public SkillRatingDistributionItemDecor() {
mPaint = new Paint();
mValuePaint = new Paint();
mDashPathEffect = new DashPathEffect(new float[]{5, 5, 5, 5}, 1);
mPaint.setAntiAlias(true);
mValuePaint.setAntiAlias(true);
}

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
final int rv_left = parent.getLeft();
final int rv_top = parent.getTop();
final int rv_right = parent.getRight();
final int rv_bottom = parent.getHeight();
final int rv_top_line = rv_top + 42;
final int rv_bottom_line = rv_bottom - 20;
final int y_spacing = (int) ((rv_bottom_line - rv_top_line) / 4f);
for (int i = 0; i < 5; i++) {
if (i == 0 || i == 4) {
mPaint.setPathEffect(null);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0xFFE1E6EB);
mPaint.setStrokeWidth(2f);
} else {
mPaint.setPathEffect(null);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0x7FE1E6EB);
mPaint.setStrokeWidth(1f);
}
c.drawLine(rv_left, rv_top_line + i * y_spacing, rv_right, rv_top_line + i * y_spacing, mPaint);
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
SkillRatingDistributionObj skillRatingDistributionObj = (SkillRatingDistributionObj) child.getTag();
if (skillRatingDistributionObj != null) {
int skill_rating = Integer.parseInt(skillRatingDistributionObj.getSkill_rating());
if (skill_rating % 25 != 0 && skill_rating != 1) {
continue;
}
final int child_left = child.getLeft();
mValuePaint.setColor(0xFF000000);
mValuePaint.setTextSize(18f);
mValuePaint.setTextAlign(Paint.Align.CENTER);
c.drawText(skillRatingDistributionObj.getSkill_rating(), child_left + 8, 30f, mValuePaint);
mPaint.setPathEffect(mDashPathEffect);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0xFFE1E6EB);
mPaint.setStrokeWidth(2f);
c.drawLine(child_left + 8, rv_top_line, child_left + 8, rv_bottom_line, mPaint);
}
}
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int itemPosition = parent.getChildAdapterPosition(view);
int dataSize = parent.getAdapter().getItemCount();
if (itemPosition == 0) {
outRect.set(20, 42, 0, 22);
} else if (itemPosition == dataSize - 1) {
outRect.set(0, 42, 20, 22);
} else {
outRect.set(0, 42, 0, 22);
}
}
}
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
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

我们可以通过outRect控制ItemView上下左右的“偏移”,可以通过onDraw随便往RecyclerView/ItemView中画东西,简直不能再灵活啊。RecyclerView类由于大量的静态内部类,代码行数一万多行了,和LayoutManager类的交互也很复杂,有时间研究一下代码可以学到很多东西。 

文章中的代码已经很完整了,如果想直接看一下demo,可以clone一下我在Git上的Demo

ListView: 

如果ListView高度为wrap_content,那么无论Item总高度多少,都不会在底部添加分隔线。 

如果ListView高度为match_parent或固定高度,那么当Item总高度小于ListView高度时会添加底部分隔线,否则不会添加底部的分割线。 
Android:headerDividersEnabled
android:footerDividersEnabled
只能决定ListView的HeaderView和FooterView分隔线是否绘制(默认为true绘制),并不能消除分隔线导致的Item偏移,即HeaderView/FooterView底部分隔线的空间始终存在,如果设置为false只是不会绘制分割线样式而已(每一个HeaderView/FooterView其实都是一个ItemView)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: