StickHeaderItemDecoration--RecyclerView使用的固定头部装饰类

目录

概述

StickHeaderItemDecoration是用于显示固定头部的item装饰类,扩展来自系统的ItemDecoration.本文参考了一部分sticky-headers-recyclerview


原理

  • 绘制头部
    固定头部的ItemDecoration本质是在RecycleView上覆盖一个界面.该界面没有随着滑动变动所以看起来就像一个固定的头部.

  • 绘制item间隔
    ItemDecoration也可以实现每个item之间的间隔的绘制(比如分隔线之类的),这种情况下就不是在RecycleView上直接覆盖一个界面了,而是通过ItemDecoration的方法在每个item之间创建一个专门绘制item间隔元素的区域进行绘制.此时是先绘制的itemDecoration,再绘制的itemView,所以超出来绘制区域也不会有影响.


API方法

ItemDecoration中有6个方法,3个是以前的方法被废弃更新为新的三个方法,所以我们只看新的三个方法.

//将decoration绘制到canvas上,会优先于itemView进行绘制,所以超出绘制区域会被itemView覆盖,不会有影响(可以理解为绘制背景)
public void onDraw(Canvas c, RecyclerView parent, State state);
//作用同onDraw,但是晚于itemView的绘制,所以会覆盖在recycleView之上,是完整可见的(不超出 RecycleView 的情况下)
public void onDrawOver(Canvas c, RecyclerView parent, State state);
//将这个方法中获取的outRect插到padding或者margin中,扩大了itemView之间的间距,用于onDraw中绘制decoration
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state);

查看一下源码可知,onDrawonDrawOver都是在RecycleView绘制时直接调用绘制的,但是getItemOffsets是在measureChild的时候就已经计算好了添加到间隔中的.所以每一个item可以有不同的间距.
这里需要注意的是getItemOffsets中是使用Rect存储计算后的间距,虽然这里是一个矩形,但并不是将这个矩形放置到item的间距中,而是矩形的left/right/top/bottom分别表示item四个方向上需要预留的间距大小(像一个盒子一样加在item四周)


绘制流程

通过以上我们知道固定头部实际上就是绘制一个头部item在RecycleView上显示即可.所以我们需要关注的只有onDrawOver这个方法,另外两个我们都不需要用到.
绘制的流程大致如下:

  • 确定当前的显示的第一个item的位置(用于后面加载headerView)
  • 确定当前分组是否需要绘制固定头部(下面不考虑不需要的情况)
  • 计算需要绘制的headerView的宽高等信息,绑定头部数据
  • 绘制头部

主要流程就以上几个,非常简单和明确.重点是只在于流程中的某些细节,那就是一个又一个的坑啊…


固定头部接口IStickerHeaderDecoration

绘制固定头部时,StickHeaderItemDecoration完成固定头部的测量绘制,对于固定头部的获取/数据绑定等并不能进行处理,实际上该部分操作也不能由其处理,因此定义了一个接口,用于处理相关的固定头部的数据.

public interface IStickerHeaderDecoration {
    //判断当前位置的item是否为一个header
    public boolean isHeaderPosition(int position);

    //判断当前位置的item是否需要一个stick header view
    public boolean hasStickHeader(int position);

    //获取指定位置需要显示的headerView的标志,该标志用于缓存唯一的一个header类型的view.
    //不同的headerView应该使用不同的tag,否则会被替换
    public int getHeaderViewTag(int position, RecyclerView parent);

    //根据header标志或者position获取需要的headerView
    public View getHeaderView(int position, int headerViewTag, RecyclerView parent);

    //设置headerView显示的数据
    public void setHeaderView(int position, int headerViewTag, RecyclerView parent, View headerView);

    //判断当前渲染的header是否与上一次渲染的header为同一分组,若是可以不再测量与绑定数据
    //lastDecoratedPosition,上一次渲染stickHeader的位置
    //nowDecoratingPosition,当前需要渲染stickHeader的位置
    public boolean isBeenDecorated(int  lastDecoratedPosition, int nowDecoratingPosition);
}

通过方法说明可以看出,主要是根据位置确定相关的头部信息(包括是否显示头部,headerView的加载以及绑定数据),这些操作都由接口实现类去完成.这样可以保证StickHeaderItemDecoration所做的操作是独立的,任何时候只需要更换一个IStickHeaderDecoration都可以正常处理并绘制固定头部.

对于以上接口方法,需要部分说明.

  • hasStickHeader(int)
    该方法虽然是判断当前item是否需要显示header,但这个说法并不完全准确.
    这里的position其实并不是作为一个item项的位置作为判断的参考,接口类需要处理时应该是考虑该position位置的item所在的分组是否需要显示header.
    通常来说,参数position是RecycleView中显示的第一个childView对应的位置,某些情况下会是第二个childView的位置(后面会解释为什么是第二个,这个很重要)

  • getHeaderViewTag(int,RecyclerView)
    该方法为获取对应位置的headerView的标志tag,这里的tag主要是用于缓存headerView使用的.通过getHeaderView方法获取一个headerView之后,会使用其tag缓存起来,当缓存以后只要再得到的headerView的tag与之匹配就会复用此缓存的headerView.可以理解为headerView的类型
    这里是为了尽可能降低加载布局或者是其它获取布局所占用的时间和资源(实际上如何得到布局由接口实现类决定的)

  • isBeenDecorated(int,int)
    该方法是用于判断上一次渲染绘制固定头部的位置与当前位置是否在同一组(使用同一个固定头部),若是则不会再去绑定任何数据及测量工作,直接复用上一次的headerView.
    若不是则会调用setHeaderView绑定数据并测量

  • setHeaderView(int,int,RecycleView,View)
    根据以上的说明,此方法并不是一定会被调用的,当isBeenDecorated()返回false时才会调用此方法(为了尽可能复用已存在的资源)


绘制固定头部实现

根据以上的流程说明,下面按流程一步步完成.一些细节或者非重要点会部分忽略.
由于RecycleView有使用缓存,所以下面将用childView表示缓存的子view;itemView表示adapter中对应的位置的某个item显示的view.

获取RecycleView显示的第一项itemView位置

因为固定头部是用于显示当前RecycleView里显示的分组的第一项头部,所以确定绘制的头部即确定当前RecycleView显示内容对应的头部.
首先,获取RecycleView缓存的childView,得到其在adapter中对应的位置后,通过接口IStickHeaderDecoration进行判断当前位置的分组是否需要显示header,绘制header等.

View itemView = null;
//获取第一项View,注意此处的View不一定是第一项可见的View,可能是被缓存了的View(不可见)
itemView = parent.getChildAt(0);
//获取对应View的位置
position = parent.getChildAdapterPosition(itemView);

确定是否需要绘制头部

是否需要绘制头部并不由StickHeaderItemDecoration决定,上述有提及IStickHeaderDecoration接口,此接口就是为了提供由外部决定当前位置所在的分组是否需要显示头部,通过方法hasStickHeader(int position)确定.


计算并获取固定头部

获取头部headerView也是通过接口IStickHeaderDecoration完成的,通过方法即可获取到对应的headerView.StickHeaderItemDecoration对获取的头部做了缓存的处理,在不同分组但显示的头部headerView相同时,都会复用已有headerView,避免了反复的inflate加载view
这里虽然获取了headerView,但是并不能绘制到界面上.因为headerView并没有经过任何测量的情况下,宽高都是0不可见的.
然后绑定数据并测量headerView后即可绘制出固定头部了.

//获取headerView的标志tag
int headerTag = mHeaderHandler.getHeaderViewTag(position, parent);
//获取头部tag对应的缓存headerView
View headerView = mViewCacheMap.get(headerTag);
//不存在缓存view时加载headerView
if (headerView == null) {
    headerView = mHeaderHandler.getHeaderView(position, headerTag, parent);
    //保存到缓存中
    mViewCacheMap.put(headerTag, headerView);
}

绘制headerView

所有工作中反而是headerView的绘制工作最简单,通过view本身的方法View.draw(Canvas)即可将view绘制到指定的canvas上,当然这里会涉及到绘制的起点原点,这个地方会有其它考虑的事项.
但总的来说,绘制一个固定头部就以上的操作即可.

//将View绘制到canvas上
headerView.draw(c);

细节事项

从以上说明可知,绘制一个headerView的流程并不难,也不算复杂,但是以上仅仅只能绘制出固定headerView,需要其更完善地绘制还需要处理很多细节,比如:

  • 正确获取RecycleView第一项item的位置
  • 正确计算headerView的宽高大小
  • 正确计算headerView绘制区域以确保不同headerView的替换交互

下面将主要说明以上几个细节的处理方式.

关于正确获取RecycleView第一项item的位置

需要处理这个细节的原因是,在滑动过程中,被滑动出RecycleView的item可能还会短暂地存放在缓存中,所以会造成一种现象是:

  • RecycleView的第一个childView并不是可见的第一个childView,可见的第一个childView实际上是缓存的第二个childView.

设想一下,当第一个分组已经滑动出RecycleView显示的界面了,此时应该显示的是第二个分组的header了,但实际上并不会,因为在判断检测时还是使用的第一个分组的最后一个itemView的位置,因此此时显示的还是第一个分组的header,这种情况是我们不希望的.
解决方案是:
我们将遍历所有RecycleView当前缓存的childView,从中查找到第一个headerView,判断第一个headerView是否显示在第一项可见位置,若是则当前第一项的位置position将使用第一个headerView的位置而不是第一项childView的位置


判断第一项childView所在的位置

对于一个childView,可以通过下面的方法判断是否为第一个可见项.
只要其view.getTop()小于RecycleView顶部坐标,同时view.getBottom()大于RecycleView顶部坐标(比较都是顶部坐标),即说明该view在RecycleView的顶部第一项显示(即RecycleView顶部边界线夹在chidView上下边界坐标之间).

//RecycleView顶部边界线
int edgeOfCanSeen = parent.getPaddingTop();
//headerView顶部坐标
int edgeOfStart = firstHeaderView.getTop();
//headerView底部坐标
int edgeOfEnd =firstHeaderView.getBottom();
//recycleView边界线介于headerView之间即可
return edgeOfStart <= edgeOfCanSeen && edgeOfEnd >edgeOfCanSeen;

注意这里需要考虑RecycleView自身的padding值,实际的顶部边界线应该是除去paddingTop的值.


查找RecycleView的第一项可见childView

根据前面已经有如何判断某个childView是否为RecycleView的第一项可见view,所以可以直接遍历所有的childView来查找(实际上第一个可见childView必定在前1或者2的位置,不会真的遍历所有的childView)

//获取缓存的childView总数
int childCount = parent.getChildCount();
//逐一遍历
for (int i = 0; i < childCount; i++) {
    View childView = parent.getChildAt(i);
    if (isViewCanSeenAtFirstPosition(childView, parent, isHorizontal)) {
        //查找到第一个可见childView时保存其当前的childView位置(后面需要查找此位置之后的headerView)
        saveChildPositionToView(i);
        return childView;
    }
}
//若没有返回null,除非RecycleView不存在childView,不然正常情况下不会返回null
return null;

获取到了第一个可见的childView,然后就可以从这个view入手去判断对应的headerView绘制了.


示例图片

如果不检测第一项可见childView,则会有以下的现象,在头部交替时会有一瞬间变成上一个头部的数据,这就是因为看起来第一项是第二个头部,但实际上第一个头部项还是缓存着没有被回收,在childView(0)中,这里看到的”第一项”其实是childView(1).
第一次为不检测显示,第二次为切换正常状态
这里写图片描述


正确计算headerView的宽高大小

计算headerView宽高大小包括判断是否需要进行测量及测量工作

判断是否需要判断数据及测量工作

从以上IStickHeaderDecoration接口中已知,当确定当前渲染的固定头部是一个新的头部或者需要重新绑定数据时,才会回调绑定数据并测量.所以存在一个判断当前位置的固定头部是否需要绑定数据及测量的方法.

boolean isNeed = true;
//第一次渲染加载,必定进行测量
if (mIsFirstDecoration) {
    mIsHorizontal = isHorizontal;
    //取消第一次加载标识
    mIsFirstDecoration = false;
} else if (mIsHorizontal != isHorizontal) {
    //当前处理的布局方向与上一次处理的布局方向不同
    //重新测量加载
    mIsHorizontal = isHorizontal;
} else if (measureView.getWidth() <= 0 || measureView.getHeight() <= 0) {
    //当前view未被测量过
} else {
    //被加载过的情况下,不再需要进行绑定数据及测量
    //否则返回true进行绑定数据及测量
    isNeed = !mHeaderHandler.isBeenDecorated(mLastDecorationPosition, newPosition);
}

//不管如何处理,最终必定会将当前渲染的位置保存起来
mLastDecorationPosition = newPosition;
return isNeed;

需要测量的情况有以下几种:

  • 第一次加载
  • 切换RecycleView布局方向
  • 当前headerView还未进行过任何测量工作
  • 接口回调确定当前固定头部必须重新绑定数据

绑定数据并测量headerView工作

 if (isNeedToBindAndMeasureView(headerView, isHorizontal, position)) {
    //设置headerView的数据显示
    mHeaderHandler.setHeaderView(position, headerTag, parent, headerView);
    /**
     * 测量工作必须在这里处理,因为默认是布局处理的布局是wrap_content,需要设置数据之后再进行测量计算工作
     * 否则如果布局中某些view是wrap_content,当不存在数据时该view大小将为0,即无法显示
     * **/
    measureHeaderView(parent, headerView, isHorizontal);
}

测量工作

上面我们已经得到了headerView,但有时会出现某些控件显示不正常或者是headerView并没有预想一样显示,问题就在没有对headerView的大小进行计算.
headerView由于是通过接口回调加载出来的,并不能确定该view的大小是否已经计算过(如果是inflate出来的新view则并不会进行measure),因此我们需要对其大小进行计算.计算的方式可以参考所有自定义view的measure方式.

//不存在layoutparams的view添加默认布局参数
if (headerView.getLayoutParams() == null) {
    headerView.setLayoutParams(new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}

int widthSpec;
int heightSpec;

//测量处理
widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
heightSpec =View.MeasureSpec.makeMeasureSpec(parent.getHeight(),View.MeasureSpec.UNSPECIFIED);

//测量并计算headerView宽高
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
        parent.getPaddingLeft() + parent.getPaddingRight(), headerView.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
        parent.getPaddingTop() + parent.getPaddingBottom(), headerView.getLayoutParams().height);

//测量并设置headerView宽高
//这里是直接将计算得到的宽高提交给view使用
headerView.measure(childWidth, childHeight);
//刷新layout布局
headerView.layout(0, 0,headerView.getMeasuredWidth(),headerView.getMeasuredHeight());

前半部分是设置并计算headerView的宽高模式(match_parent/wrap_content或者固定值等),后面通过View.measure(widthMeasureSpec,heightMeasureSpec)通知view进行自身的测量(因为我们并不知道该headerView只是一个普通的view还是一个viewGroup),最后再通过view进行自身的布局layout(原因同上).
通过以上的方式就可以完成headerView的测量工作了.这种情况下就不会出现当headerView中有某些控件是wrap_content时,没有填充数据或内容时无法正常显示该控件(如TextView)


关于测量headerView的时机

我们知道如果headerView是通过inflate方式加载出来的,那么默认情况下不具有宽高信息,我们也不可见.通过measureHeaderView之后可以得到headerView的宽高信息,此时headerView就是可见的了.
但是这里要注意measure的时机.前面我们提到绑定headerView的数据,所以对于某些控件可能存在wrap_content的宽高设置,不存在数据时不管怎么measure都是0,所以应该先绑定数据,再进行headerView的测量工作.

//设置headerView的数据显示
mHeaderHandler.setHeaderView(position, headerTag, parent, headerView);
/**
 * 测量工作必须在这里处理,因为默认是布局处理的布局是wrap_content,需要设置数据之后再进行测量计算工作
 * 否则如果布局中某些view是wrap_content,当不存在数据时该view大小将为0,即无法显示
 * **/
measureHeaderView(parent, headerView,isHorizontal);

并且这里还需要注意的是,尽管同一个recycleView的数据中固定头部的布局显示很可能是一样的,但是还是需要每一次绘制时进行一次measure,这是因为当切换数据时,永远不知道下一次绑定的数据会不会影响到界面的显示(如文字长度不同等)


关于计算固定头部的绘制区域

通过以上的操作,固定头部是可以正常进行绘制的.但是运行一下会发现,当下一个头部需要替换上一个头部的时候,就显得不正常了.因为这时两个头部会叠加在一起,原因是本身RecycleView正常显示的头部在底下,绘制的固定头部一直保持在上面直接覆盖上,所以就叠加在一起了.
解决这个问题可以使用网上常见的处理方式,让下一个头部将固定头部给顶上去,最终替换整个固定头部,使用这种方式会更加平滑和提升用户体验.


示例图片

不处理头部交互的情况下,在切换头部时会直接替换头部,看起来是是重叠的效果,用户体验很不友好…
这里写图片描述


如何仅绘制一部分头部

我们已经知道所谓的固定头部其实也只是绘制出来的一个静态界面,现在我们需要将固定头部顶上去,实际上不可能是像RecycleView一样将item滑动上去,所以只能是通过计算绘制重新绘制固定头部,并且只需要绘制一部分的头部.

  • canvas的显示范围
    首先,肯定是绘制到canvas上,这里的canvas可以认为是RecycleView背景,那么能显示的范围最多也是RecycleView的大小(理论上canvas可以无限大).

  • 指定canvas的绘制区域
    canvas是可以通过指定一个绘制区域,使得以该区域作为绘制的范围,同时该区域的左上角为原点.超过绘制的范围最终将不可见.

  • 转换canvas的区域
    canvas还可以通过转换调整绘制的起点位置,从而改变绘制的界面的显示位置.

//canvas的部分方法
//设置canvas中的绘制区域
canvas.clipRect(Rect);
//调整X/Y轴的偏移量,相当于移动了整个坐标轴
canvas.translate(float x,float y);

根据以上canvas的使用,我们可以确定出如何完全绘制一部分的固定头部流程了.

  • 计算正常情况下绘制头部的区域drawRect
  • 计算下一个头部已经滑动占据了固定头部的区域位置
  • 更新绘制头部的区域(占据部分不再绘制)
  • 绘制固定头部

计算绘制头部的区域

头部绘制区域是比较容易计算的,根据headerView的宽高大小及RecycleView的相关坐标数据即可得到需要绘制的区域.

//获取可开始绘制的位置
int drawLeft = parent.getPaddingLeft();
int drawTop = parent.getPaddingTop();

int drawRight = 0;
int drawBottom = 0;

//竖向布局
//宽填充整个parent
//高根据view处理
drawRight = drawLeft + (parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight());
drawBottom = drawTop + headerView.getHeight();

//获取headerView的layout参数
ViewGroup.LayoutParams params = headerView.getLayoutParams();
//判断headerView是否存在margin
ViewGroup.MarginLayoutParams marginParams = null;
if (params instanceof ViewGroup.MarginLayoutParams) {
    //存在margin时,绘制时的区域需要去除margin的部分
    marginParams = (ViewGroup.MarginLayoutParams) params;
    drawLeft += marginParams.leftMargin;
    drawTop += marginParams.topMargin;
    drawRight -= marginParams.rightMargin;
}
//设置绘制的区域
outRect.set(drawLeft, drawTop, drawRight,drawBottom);

以上需要注意要考虑RecycleView的padding间距,除去padding间距后的才是正常显示的范围.


计算下一个头部占据的位置

在滑动过程中,下一个头部会占据一部分顶部固定头部的位置,这时就需要计算占据的位置是多少,从而达到将固定头部绘制区域减小的,这样在滑动时就可以实现下一个头部将固定头部顶出界面的效果了.
代码中用到了childPosition,这里的childPosition就是在查找第一个可见childView中保存的saveChildPositionToView(int).

//childPosition是前面查找到的第一项可见childView,+1是从其后开始查找最近的一个headerView
//这是因为第一项View可能是一个headerView,也可能是某一组分组中的子项,若是上一个分组的子项,则此时需要显示的还是该分组的headerView,否则需要显示下一个分组的header
//从第二项开始查找是为了查找最近的一个headerView
//该headerView可能是当前正在替换旧headerView的头部,也可能是远未达到顶部的headerView
View headerView =
this.searchFirstHeaderView(childPosition+1, state.getItemCount(), parent);

int offsetX = 0;
int offsetY = 0;
if (headerView != null) {
    //当前下一个headerView正处于固定头部区域内,即此时会占据一部分绘制区域
    if (headerView.getTop() < rect.bottom && headerView.getTop() > rect.top) {
        //如果查找得到的headerView已经在替换当前的stick headerView
        //计算出需要处理的偏移量,否则不处理(即不存在偏移量,返回0)
        offsetY = rect.bottom -headerView.getTop();
    }
}
//偏移量是负值,因为绘制区域将向上或者向左移动出界面
return new Point(offsetX * -1, offsetY * -1);

需要注意的是,计算后的值应该是负的,因为顶上去的过程中绘制区域是向上偏移,所以偏移量应该是负的.得到的Point包括了X/Y的轴方向的偏移值.


计算最终绘制时的头部区域
前面已经计算得到完全显示的头部的绘制区域大小,再计算得到偏移量,下面就是把两个值给结合起来计算出最终绘制头部时的区域大小了.

//获取原始区域的宽高
int width = outRect.width();
int height = outRect.height();
//将宽高处理偏移量
width += offset.x;
height += offset.y;
//重新计算其绘制区域(一般为缩小了)
//此处是改变绘制区域的大小而不是调整绘制区域的位置
int newRight = outRect.left + width;
int newBottom = outRect.top + height;
outRect.set(outRect.left, outRect.top, newRight,newBottom);

这里实际上就是把原本的绘制区域给变小了,变小的部分就是被下一个头部占据的区域.然后就可以设置到canvas中确定仅在此区域内绘制有效.

//指定canvas的有效绘制区域
canvas.clipRect(outRect);

更新canvas的绘制原点

默认情况下canvas还是从(0,0)的坐标位置进行绘制,但是这里我们需要注意,虽然固定头部有一部分绘制区域被下一个头部占用了,但是我们要的效果应该是下一个头部把固定头部顶出界面,所以剩下的固定头部需要绘制的区域应该是绘制固定头部的后半部分.
为了绘制后半部分在可见的绘制区域(前面已经设置了canvas的绘制区域),所以需要把canvas的绘制原点向上移动,而移动的距离也刚好是与下一个头部占用的区域偏移量相同.
可以想像成整个固定头部向上移动一部分区域绘制,同时只是显示了后半部分

//计算正常情况下的绘制起点位置
int drawLeft =  parent.getPaddingLeft();
int drawTop =  parent.getPaddingTop();
outRect.set(drawLeft, drawTop, 0, 0);
//更新偏移量
outRect.offset(offset.x, offset.y);

偏移绘制的原点,将整个绘制原点向上移动,这样绘制出来的就只会看到后半部分了.不偏移的情况下只能看到前半部分.这里只是计算出绘制原点需要的偏移量,还需要设置到canvas中才有效.
最后再将固定头部绘制出来即可.

//调整canvas的绘制起点
canvas.translate(outRect.left,outRect.top);
headerView.draw(canvas);

其它

根据以上的流程和细节处理后,一个完整地固定头部的绘制就可以完成了.但是实际上还有很多的工作可以做.
以上仅仅只是处理了RecycleView的方向为vertical的情况,还有horizontal的情况,其实是跟竖直方向相似的,只是很多东西是从上下的检测和计算替换成左右的检测和计算.这里不再说明举例,代码中已经实现了自动适配竖直与水平的方向兼容.
另外一个是,前面提到只需要是实现了IStickerHeaderDecoration接口的都可以使用此固定头部的处理类,同系列与RecycleView相关的HeaderRecycleAdapter也已经实现了此接口,所以可以直接使用此类进行固定头部的装饰.
建议使用HeaderRecycleAdapter与此StickHeaderItemDecoration配合实现固定头部

  • 支持竖直与水平方向
  • 支持仅有头部无子item的情况
  • 暂时不支持反转(reverseLayout)

小结

事实说明,其实看起来可能有点复杂的功能实现起来并没有想像中那么难,但是当需要把他做得更加完美或者兼容大部分的情况甚至是做成一个引用库时,需要处理的细节就很多了.
希望可以帮到有类似需求的人~~

示例图片

  • 普通情况
    这里写图片描述

  • 连续多个头部的情况
    这里写图片描述

GitHub地址

https://github.com/CrazyTaro/RecycleViewAdatper

资源下载

不想下载github项目的,或者不使用AS只需要类文件的,可以到以下下载地址直接下载类文件:
http://download.csdn.net/detail/u011374875/9556686

回到目录

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值