Android RecyclerView-使用Itemdecoration实现粘性头部功能,详细到具体步骤

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.
  • 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.
  • @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.

  • getItemOffsets()
    该方法再recyclerView的每个Item测量大小的时候都会被调用到, 我们要在该方法里面判断出那个HeadItem并且给HeadItem留出绘制的空间,这里有两种方式.
    第一种方式:
    给Item 的Top留出空间,也就是outRect.top属性赋值.
    第二种方式:
    给Item 的Bottom留出空间也就是outRect.bottpm属性赋值.
    因为我们在列表一开始的时候就要绘制一次Head,也就是说我们要留出Head的空间,那么我们只能选择第一种方法去预留空间了. 当你选择方式1的时候,给outRect.top赋值,这样的话我们判断是否是HeadItem的话就要拿当前Item的标记跟前一个Item的标记判断了.如果用第二种的话就要用当前的标记跟下一个Item的标记判断了.
    下面我来解释下第一种方式,第二种方式雷同:
    a b c d e f g h i
    分组1 abc
    分组2 def
    分组3 ghi
    如果 a d g 是HeadItem . a的tag = 1 , b的tag = 1, c 的tag = 1…d的tag = 2,e的tag = 2 ,f的tag = 2,g的tag = 3…等等 .
    前一个Item的tag用 preTag 来表示 ,初始值为 -1.
    假如当前的Item为a,当前tag = 1,那么它的前一个Item为空,也就是发现preTag和a的tag不一样,那么a就是分组的头部.
    假如当前的Item为b,当前tag = 1,那么它前一个preTag 也就是a的tag = 1,发现一样那就是是同一组的.
    假如当前的Item为d,当前tag = 2,那么它前一个preTag 也就是c的tag = 1,发现前一个的tag跟当前的不一样,那么当前的就是新分组的第一个头部Item.代码是最有说服力的,下面来看代码:

@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;
}

}

这样下来我们给分组头部的空间就预留出来了.接下来绘制分组头部,因为分割线我直接显示的背景色所以就不用去绘制分割线了.

  • onDrawOver()
    这个方法里面我们要做的不只是绘制Head,当列表滑动的时候RecyclerView会不断的加载之后的Item,布局发生复用,我们要在不断的变化中去重新绘制我们的HeadItem的布局.这个方法当每个Item消失或者出现的时候都会被调用,我们在这里去绘制HeadItem的区域.所以在该方法里面我们会遍历所有可见的Item去重新判断那个Item有Head,然后去绘制.
    1.判断头布局绘制头布局 ?
    那么我们在这里呢还是需要判重新去判断哪个Item是有Head.按照getItemOffsets里面的我们需要跟之前的Item的tag做比较.但是有个问题就是我们再这里并不能拿到Item的布局或者别的东西,只能遍历所有已经显示的Item,也就是只能一个个的将RecyclerView的ChildView拿出来.这样的话我们的前一个preTag就需要我们自己去定义,然后用preTag来记录我们遍历过的ChildView的Tag,当遍历到下个Item的Tag跟之前的preTag一样的话,那就继续遍历不去绘制头布局,当遍历到Item的tag跟preTag不一样的时候就去绘制有布局.因为滑动的Item都会作为RecyclerView的第0个ChildView出现,我们拿不到它之前的Item的tag.
    2.怎么让头布局悬停在顶部 ?
    这个问题其实拿一个场景去说明是最好的了.当我们HeadItem正好出现在屏幕的顶部的时候,我们继续滑动列表HeadItem就会渐渐的消失,也就是Item的getTop距离会小于我们HeadItem的Head的高度,当出现这种情况的时候, 我们就让Item的getTop和Head的高度中去选择一个最大值.这样就好保证当HeadItem画出屏幕的时候Head布局一直留在顶部.
    3.下个头部来的时候怎么替换呢 ?
    当顶部有一个头部局在悬停的时候,我们滑动列表时下个头部肯定会和当前悬停的头部相遇.我们再这里做的是当前悬浮的头布局跟下个头布局相遇发生交替的时候有个渐变的效果.因为该方法在每一个Item出现或者消失的时候都会执行,每当执行的时候都要遍历一遍当前已经显示的Item布局,那么一定会出现,当前遍历的第0个Item正好是屏幕中的第一个Item,它的下一个Item正好是分组的头布.这样的话再往前滑的时候就会出现头部交替的情况.我们这里就需要判断下一个Item是不是有头布局的Item,比较的方法就是用当前Item数据的tag跟下一个Item数据的nextTag比较,如果不同的话那下个Item就是有头布局的.如果一样的话就continue继续遍历.再列表滑动的时候回一直绘制所有可见Item的Head布局.
    4.渐变效果呢?
    上面我们知道了当下个HeadItem跟屏幕顶部的Head相遇的时候就要发生交替.交替的时候有个渐变效果,也就是之前再屏幕顶部悬停的Head要随着一个Item的消失而消失.下个Head要滑动到屏幕之后停在那里.那就好办了当onDrawOver()方法执行的时候,RecyclerView的第0个ChildView正好是屏幕顶部的Item,当它的下一个Item有个Head的时候,我们只需要将当前Item的getTop数值赋值给绘制Head的矩形的bottpm属性就可以了.我们一定要明白当出现Item出现消失的时候Head是再不断的绘制的.

上代码:

@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下载

喜欢文字的同学也可以关注该公众号,与你一起同游文字的海洋~

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上我搜集整理的2019-2021BATJ 面试真题解析,我把大厂面试中常被问到的技术点整理成了PDF,包知识脉络 + 诸多细节。

节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?

1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

qThU9G-1713723564065)]

腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析

[外链图片转存中…(img-eHjcalTQ-1713723564066)]

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值