TingTalk汀说 2018-02-01
该文详细的介绍了RecyclerView.ItemDecoration实现分组粘性头部的功能,让我们自己生产代码,告别代码搬运工的时代.另外文末附有完整Demo的连接.看下效果:
RecyclerView.ItemDecoration对于我们最熟悉的功能就是给RecyclerView实现各种各样自定义的分割线了,实现分割线的功能其实和实现粘性头部的功能大同小异,那我们就来看看这神奇的RecyclerView.ItemDecoration.
该类是RecyclerView的内部静态抽象类:
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间隔到底是那个间隔?
我们来看一张图.
我们知道getItemOffsets()第一个参数是一个矩形的对象,这个对象的left、 top、right、bottpm四个属性值分别表示图中的outRect.left、outRect.top、outRect.right、outRect.bottom四个线段所表示的空间.也就是说当RecyclerView的Item再确定自己的大小的时候会将getItemOffsets()里面的Rect对象的Left、Top、Right、Bottom属性取出来,看看需要再Item布局的四周留出多大的空间.我们来看下源码:
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之外留出空间的.
onDraw()和onDrawOver()方法应该用哪一个?
首先我们看过上面的代码之后知道,onDraw执行再Item的绘制之前,也就是ItemDecoration的onDraw方法先执行,再执行Item的onDraw方法,这样Item的内容就会覆盖在ItemDecoration的onDraw上面.ItemDecoration的onDrawOver()方法执行在Item的绘制之后,那就是onDrawOver()绘制的内容会覆盖再Item内容之上.这样就形成了层层遮盖的问题,那么我们平常的分割线通常绘制在ItemDecoration的onDraw()方法里面,为了避免Item的内容覆盖掉,我们就要getItemOffsets()为我们留出绘制的空间了.这样我们的思路不是不有了呢.
我们可以用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年的第一篇文章,之前太忙了也没好好的总结知识点.写的仓促希望大家多多指导文章出现的问题,谢谢大家的反馈,欢迎评论吐槽哦~
欢迎大家关注
我的安科网
我的CSDN
我的简书
Demo下载