RecyclerView之ItemDecoration详解
来源:互联网 发布:java工程师招聘北京 编辑:程序博客网 时间:2024/04/29 13:43
关于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>
为了方便,这里尺寸都使用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(); // 累加当前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); } }
可以看到测量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; }
第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); }}
如果想让第一个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); } } }
我们再玩一个文本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); }}
什么?你觉得这个太简单了?那咱再玩一个。。更简单的:
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); } }}
我们可以通过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)。
Thanks:
Piasy' blog
- RecyclerView之ItemDecoration详解
- RecyclerView之ItemDecoration详解
- RecyclerView之ItemDecoration详解
- RecyclerView之ItemDecoration详解(下)
- RecyclerView之ItemDecoration详解(上)
- recyclerView-自定义itemDecoration详解
- RecyclerView之ItemDecoration
- RecyclerView之ItemDecoration由浅入深
- RecyclerView之ItemDecoration由浅入深
- RecyclerView之ItemDecoration
- RecyclerView之ItemDecoration由浅入深(转载)
- Android-->RecyclerView.ItemDecoration分割线详解
- 从头开始学 RecyclerView(五) ItemDecoration 详解
- RecyclerView.ItemDecoration
- Android学习之RecyclerView(三)-ItemDecoration
- RecyclerView之ItemDecoration由浅入
- RecyclerView源码详解(第二篇ItemDecoration源码详解)
- Android——RecyclerView入门学习之ItemDecoration
- openTLD学习之路(1)
- 【Android】Eclipse自动编译NDK/JNI的三种方法
- printf封装
- 点-圆-圆柱类族的设计
- java中的值传递和引用传递
- RecyclerView之ItemDecoration详解
- 滑动条
- hdoj
- 安装windows系统时识别不了盘符
- mongodb的高效性
- 仿果壳网手机登陆界面源代码
- 学习笔记,记录的开始~^_^
- u3d中如何读取txt文件中的信息
- 九宫格问题