该文详细的介绍了RecyclerView.ItemDecoration实现分组粘性头部的功能,让咱们本身生产代码,告别代码搬运工的时代.另外文末附有完整Demo的链接.看下效果:
json
RecyclerView.ItemDecoration对于咱们最熟悉的功能就是给RecyclerView实现各类各样自定义的分割线了,实现分割线的功能其实和实现粘性头部的功能大同小异,那咱们就来看看这神奇的RecyclerView.ItemDecoration.bash
该类是RecyclerView的内部静态抽象类:ide
public abstract static class ItemDecoration {
/**
* 绘制*除Item内容*之外的布局,这个方法是再****Item的内容绘制以前****执行的,
* 因此呢若是两个绘制区域重叠的话,Item的绘制区域会覆盖掉该方法绘制的区域.
* 通常配合getItemOffsets来绘制分割线等.
*
* @param c Canvas 画布
* @param parent RecyclerView
* @param state RecyclerView的状态
*/
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
/**
* 绘制*除Item内容*之外的东西,这个方法是在****Item的内容绘制以后****才执行的,
* 因此该方法绘制的东西会将Item的内容覆盖住,既显示在Item之上.
* 通常配合getItemOffsets来绘制分组的头部等.
*
* @param c Canvas 画布
* @param parent RecyclerView
* @param state RecyclerView的状态
*/
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
/**
* @deprecated
* Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
/**
* @deprecated
* Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
*/
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
/**
* 设置Item的布局四周的间隔.
*
* @param outRect 肯定间隔 Left Top Right Bottom 数值的矩形.
* @param view RecyclerView的ChildView也就是每一个Item的的布局.
* @param parent RecyclerView自己.
* @param state RecyclerView的各类状态.
*/
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
复制代码
这里面呢有个问题必定要明白几个问题:布局
getItemOffsets这个方法设置的Item间隔究竟是那个间隔?测试
咱们来看一张图. ui
咱们知道getItemOffsets()第一个参数是一个矩形的对象,这个对象的left、 top、right、bottpm四个属性值分别表示图中的outRect.left、outRect.top、outRect.right、outRect.bottom四个线段所表示的空间.也就是说当RecyclerView的Item再肯定本身的大小的时候会将getItemOffsets()里面的Rect对象的Left、Top、Right、Bottom属性取出来,看看须要再Item布局的四周留出多大的空间.咱们来看下源码:this
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);
//这里呢mTempRect就是咱们再getItemOffsets()里面的第一个Rect的对象,咱们再实现类的方法里面给mTempRect赋值.
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再测量每一个Child的大小的时候都把insets这个矩形的l t r b 数值都加上了.insets就是方法getItemDecorInsetsForChild()返回的矩形对象.
/**
* Measure a child view using standard measurement policy, taking the padding
* of the parent RecyclerView and any added item decorations into account.
*
* <p>If the RecyclerView can be scrolled in either dimension the caller may
* pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
*
* @param child Child view to measure
* @param widthUsed Width in pixels currently consumed by other views, if relevant
* @param heightUsed Height in pixels currently consumed by other views, if relevant
*/
public void measureChild(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() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
复制代码
源码的讲解过于粗糙,但愿你们见谅,目的就是为了让你们知道这个getItemOffsets()方法是怎么让RecyclerView再Item以外留出空间的.spa
onDraw()和onDrawOver()方法应该用哪个?.net
首先咱们看过上面的代码以后知道,onDraw执行再Item的绘制以前,也就是ItemDecoration的onDraw方法先执行,再执行Item的onDraw方法,这样Item的内容就会覆盖在ItemDecoration的onDraw上面.ItemDecoration的onDrawOver()方法执行在Item的绘制以后,那就是onDrawOver()绘制的内容会覆盖再Item内容之上.这样就造成了层层遮盖的问题,那么咱们日常的分割线一般绘制在ItemDecoration的onDraw()方法里面,为了不Item的内容覆盖掉,咱们就要getItemOffsets()为咱们留出绘制的空间了.这样咱们的思路不是不有了呢.3d
咱们能够用onDrawOver()和getItemOffsets()方法一块儿使用来实现Item的粘性头部和顶部悬浮的效果.
咱们要作的是区域分组显示,每一个分组的开始要有一个粘性头部.如图所示:
首前后台返回的数据必定要有组类区分,每一个分组的标记不能同样,最好是咱们方便处理的.该Demo采用的标记位是int类型的标记tag,每组的标记以此+1,每五个城市分为一组,每组的第一个城市当作头部局显示的内容.咱们的分组头部的高度为40dp.
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (citiList == null || citiList.size() == 0) {
return;
}
int adapterPosition = parent.getChildAdapterPosition(view);
RecBean.CitiListBean beanByPosition = getBeanByPosition(adapterPosition);
if(beanByPosition == null){
return;
}
int preTage = -1;
int tage = beanByPosition.getTage();
//必定要记住这个 >= 0
if(adapterPosition - 1 >= 0) {
RecBean.CitiListBean nextBean = getBeanByPosition(adapterPosition - 1);
if (nextBean == null) {
return;
}
preTage = nextBean.getTage();
}
if(preTage != tage){
outRect.top = headHeight;
}else {
//这个目的是留出分割线
outRect.top = lineHeight;
}
}
复制代码
这样下来咱们给分组头部的空间就预留出来了.接下来绘制分组头部,由于分割线我直接显示的背景色因此就不用去绘制分割线了.
上代码:
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if(citiList == null || citiList.size() == 0){
return;
}
int parentLeft = parent.getPaddingLeft();
int parentRight = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
int tag = -1;
int preTag;
for (int i = 0; i <childCount; i++) {
View childView = parent.getChildAt(i);
if(childView == null){
continue;
}
int adapterPosition = parent.getChildAdapterPosition(childView);
当前Item的Top
int top = childView.getTop();
int bottom = childView.getBottom();
preTag = tag;
tag = citiList.get(adapterPosition).getTage();
//判断下一个是否是分组的头部
if(preTag == tag){
continue;
}
//这里面我把每一个分组的头部显示的文字列表单独提出来了,为了测试方便用,
String name = index.get((tag - 1 ) < 0 ? 0 : (tag -1));
int height = Math.max(top,headHeight);
//判断下一个Item是不是分组的头部
if(adapterPosition + 1 < citiList.size()){
int nextTag = citiList.get(adapterPosition + 1).getTage();
if(tag != nextTag){
//这里就是实现渐变效果的地方
//由于若是遍历到
height = bottom;
}
}
paint.setColor(Color.parseColor("#ffffff"));
c.drawRect(parentLeft,height - headHeight,parentRight,height,paint);
paint.setColor(Color.BLACK);
paint.getTextBounds(name, 0, name.length(), rectOver);
c.drawText(name, dip2px(10), height - (headHeight - rectOver.height()) / 2, paint);
}
}
复制代码
到这里咱们的功能已经结束了,咱们要知道getItemOffsets()会提早执行,每一个Item的回收和出现都会执行一次.onDraw或者onDrawOver再屏幕中的Item发生变化的时候都会执行,只要发生变化.咱们的Head会不停的绘制.
这是2018年的第一篇文章,以前太忙了也没好好的总结知识点.写的仓促但愿你们多多指导文章出现的问题,谢谢你们的反馈,欢迎评论吐槽哦~