关于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>
<?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); }}
让它上下左右都空出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); }}
可以看到,我们通过设置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(); 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(); if (!lp.mInsetsDirty) { 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; }
- 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); }}
只需要在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); } }
然后绘制的时候,在第一个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)。
0 0