说道粘性头部这个设计现在是越来越流行了,基本上每一个app都会涉及到,RecyclerView 的强哥不仅仅是他运行的高效率,还有非常好的兼容性,今天我就利用RecyclerView.ItemDecoration 来实现一个粘性头部
创建一个类继承自RecyclerView.ItemDecoration,由于数据的多样性,我们这里使用泛型
public class TsmItemDecoration<T> extends RecyclerView.ItemDecoration {
/**
* 这个是伴随着drawItem一直绘制的,
*/
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
}
/**
* 该方法是绘制完item后绘制的,可以覆盖在item之上
*/
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
/**
* 为需要添加头部的item设置padding,
*/
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
}
我们看到有三个比较重要的方法onDraw onDrawOver getItemOffsets 这三个方法,单是这三个方法的具体用途是什么呢,
getItemOffsets
他的定义类似用一个容器包裹item,这个item 可以放在容器的任意一个位置,他的用处就是决定了如果当前的item需要添加头部,那么我们则需要给当前需要添加头部的item 预留出来 头部高度的距离
onDraw
在绘制item的过程中,我们已经利用getItemOffsets 为需要添加头部的item预留出来了控件,onDraw 就可以利用这个空间在需要添加头部的item绘制的同时绘制这个头部,这个头部就属于item的一部分了,
onDrawOver
这个方法就是在绘制完item的时候可以绘制任意一个位置绘制一个图像,并且覆盖在item之上,
知道了这几个方法我们来写一个简单的效果
首先我们定义几个方法,便于将这个类作为父类,可以在整个app中复用,省时省力
前面我们一直在说在需要的item之上添加头部,那么哪个item需要呢,绘制的内容是什么,我们也可能遇到前面几个不需要这个header,跳过前面几个
/**
*跳过前面几个
*/
private int start_offset;
/**
* 方法一: 决定当天item是否需要绘制头部
*/
public abstract boolean isNeedAddHeaer(T data1,T data2);
/**
* 获取当前header 绘制的内容
*/
public abstract String getHeaderContent(T data);
此时我们修改过后的类就变成
public abstract class TsmItemDecoration<T> extends RecyclerView.ItemDecoration {
private Context context;
private List<T> dataList;
private int start_offset;
private int header_height=100;
public TsmItemDecoration(Context context, List<T> dataList){
this.context=context;
this.dataList=dataList;
start_offset=0;
}
public TsmItemDecoration(Context context, List<T> dataList,int start_offset){
this.context=context;
this.dataList=dataList;
this.start_offset=start_offset;
}
/**
* 这个是伴随着drawItem一直绘制的,
* @param c
* @param parent
* @param state
*/
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
}
/**
* 该方法是绘制完item后绘制的,可以覆盖在item之上
* @param c
* @param parent
* @param state
*/
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
/**
* 为需要添加头部的item设置padding,
* @param outRect
* @param view
* @param parent
* @param state
*/
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if(dataList==null&&dataList.size()==0)//没有数据不添加
return;
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
if(position<start_offset)///当前位置小于偏移量不添加
return;
if(position>(dataList.size()-1))//越界不添加
return;
if(position==start_offset){///偏移量其实必须要添加
outRect.set(0,header_height,0,0);
return;
}
if(needAddHeader(position,position-1)){//当前是否需要添加需要和前一个做判断
outRect.set(0,header_height,0,0);
return;
}
}
/**
* 根据位置决定当天item是否需要绘制头部
* @param p1
* @param p2
* @return
*/
public boolean needAddHeader(int p1,int p2){
return isNeedAddHeader(dataList.get(p1),dataList.get(p2));
}
/**
* 方法一: 根据内容决定当天item是否需要绘制头部
*/
public abstract boolean isNeedAddHeader(T data1,T data2);
/**
* 获取当前header 绘制的内容
*/
public abstract String getHeaderContent(T data);
}
这些判断我写的很清楚了,大家可以借鉴一下,随便写了一个子类, 上个图片吧,就不贴代码了,
很简单每一条都添加header,运行看一下效果,
前面那个是没添加header的,后面那个是添加header的
给需要添加头部的item预留出来距离,我们就需要在这个预留出来的距离里面绘制我们的数据了,当然了这个方法就是onDraw,如果大家看了上一篇文章应该就知道我们需要给哪些item绘制这个header,没错,就是在屏幕内recylcerview所持有的item,即getChildCount所包含的数据
/**
* 这个是伴随着drawItem一直绘制的,
* @param c
* @param parent
* @param state
*/
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
if(dataList==null&&dataList.size()==0)//没有数据不添加
return;
int count = parent.getChildCount();///不是所有的child都绘制,只绘制在屏幕上的view
for (int i=0;i<count;i++){
View child = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(child);
if(position<start_offset)///当前位置小于偏移量不添加
continue;
if(position>(dataList.size()-1))//越界不添加
continue;
if(position==start_offset){///偏移起始实必须要添加
drawHeader(c,parent,child,position);
continue;
}
if(needAddHeader(position,position-1)){//当前是否需要添加需要和前一个做判断
drawHeader(c,parent,child,position);
continue;
}
}
}
protected void drawHeader(Canvas c, RecyclerView parent, View child,int position){
////先画背景
c.drawRect(parent.getPaddingLeft(),child.getTop()-header_height,parent.getWidth()-parent.getPaddingRight(),child.getTop(),mBgPaint);
///再画文字
String text=getHeaderContent(dataList.get(position));
mTextPaint.getTextBounds(text, 0, text.length(), mBounds);
c.drawText(text,child.getPaddingLeft(), child.getTop() - (header_height / 2 - mBounds.height() / 2),mTextPaint);
}
这里我们再继续上一张效果图
可以看到我们已经绘制出来了头部,这里我们修改一下,让数据里面包含有5个给一个header,这样看起来更清楚
我们再来说一下这个粘性头部的原理,不要觉得他是下面那个推着上面那个一起走,他只是你看起来的效果,真正的效果是
图片中的header1 和header2 是跟随者列表一起滑动的,而悬浮的头部则始终在item之上,当header2 的item滑动到距离顶部还有一个header的距离时,让悬浮header 跟随了列表一起开始偏移,当header2的item滑动到最顶端时,则悬浮条目重新绘制到item之上,他是通过这样的方式来达到看起来推动的效果的,
在getItemOffsets 和 onDraw 方法中,我们判断是否需要添加这个header,都是和他前面做比较,但是在onDarwOver中,这个条目是否需要和列表一起滚动,是由他和他下一个数据来判断的,这里大家需要注意一下
onDrawOver
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
int pos = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
if(pos<offset_index){
return;
}
if (list == null || list.isEmpty()) {
return;
}
View child = parent.findViewHolderForLayoutPosition(pos).itemView;
if ((pos + 1) >= list.size()) {
return;
}
if (TextUtils.isEmpty(getShowItemData(list.get(pos))) ) {
return;
}
boolean flag = false;
if ((pos + 1) < list.size()) {
if (needAddData(list.get(pos+1),list.get(pos))) {
if (child.getHeight() + child.getTop() < SECTION_HEIGHT) {
c.save();
flag = true;
c.translate(0, child.getHeight() + child.getTop() - SECTION_HEIGHT);
}
}
}
c.drawRect(parent.getPaddingLeft(),
parent.getPaddingTop(),
parent.getRight() - parent.getPaddingRight(),
parent.getPaddingTop() + SECTION_HEIGHT, mBgPaint);
String text=getShowItemData(list.get(pos));
mTextPaint.getTextBounds(text, 0, text.length(), mBounds);
setText(c, text, child.getPaddingLeft(), parent.getPaddingTop() + SECTION_HEIGHT - (SECTION_HEIGHT / 2 - mBounds.height() / 2),mTextPaint);
if (flag) {
c.restore();
}
}
这里我就不注释了,写了很多遍了,太麻烦
最后整体的代码贴一下,方便大家使用
public abstract class TsmItemDecoration<T> extends RecyclerView.ItemDecoration {
private Context context;
private List<T> dataList;
private int start_offset;
private int header_height=100;
private TextPaint mTextPaint;
private Rect mBounds;
private Paint mBgPaint;
private int text_padding_left;
public TsmItemDecoration(Context context, List<T> dataList){
this.context=context;
this.dataList=dataList;
start_offset=0;
init();
}
public TsmItemDecoration(Context context, List<T> dataList,int start_offset){
this.context=context;
this.dataList=dataList;
this.start_offset=start_offset;
init();
}
public void init (){
header_height= DisplayUtil.dip2px(context,50);
text_padding_left=DisplayUtil.dip2px(context,20);
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(DisplayUtil.sp2px(context,16));//标题大小
mTextPaint.setColor(Color.RED);//字体颜色
mBounds = new Rect();
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBgPaint.setColor(Color.GRAY);//标题背景色
}
/**
* 这个是伴随着drawItem一直绘制的,
* @param c
* @param parent
* @param state
*/
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
if(dataList==null&&dataList.size()==0)//没有数据不添加
return;
int count = parent.getChildCount();///不是所有的child都绘制,只绘制在屏幕上的view
for (int i=0;i<count;i++){
View child = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(child);
if(position<start_offset)///当前位置小于偏移量不添加
continue;
if(position>(dataList.size()-1))//越界不添加
continue;
if(position==start_offset){///偏移起始实必须要添加
drawHeader(c,parent,child,position);
continue;
}
if(needAddHeader(position,position-1)){//当前是否需要添加需要和前一个做判断
drawHeader(c,parent,child,position);
continue;
}
}
}
protected void drawHeader(Canvas c, RecyclerView parent, View child,int position){
////先画背景
c.drawRect(parent.getPaddingLeft(),child.getTop()-header_height,parent.getWidth()-parent.getPaddingRight(),child.getTop(),mBgPaint);
///再画文字
String text=getHeaderContent(dataList.get(position));
mTextPaint.getTextBounds(text, 0, text.length(), mBounds);
drawText(c,text,child.getPaddingLeft(), child.getTop() - (header_height / 2 - mBounds.height() / 2),mTextPaint);
}
/**
* 该方法是绘制完item后绘制的,可以覆盖在item之上
* @param c
* @param parent
* @param state
*/
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
int pos = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
if(pos<start_offset){
return;
}
if (dataList == null || dataList.isEmpty()) {
return;
}
View child = parent.findViewHolderForLayoutPosition(pos).itemView;
if ((pos + 1) >= dataList.size()) {
return;
}
if (TextUtils.isEmpty(getHeaderContent(dataList.get(pos))) ) {
return;
}
boolean flag = false;
if ((pos + 1) < dataList.size()) {
if (needAddHeader(pos+1,pos)) {
if (child.getHeight() + child.getTop() < header_height) {
c.save();
flag = true;
c.translate(0, child.getHeight() + child.getTop() - header_height);
}
}
}
c.drawRect(parent.getPaddingLeft(),parent.getPaddingTop(),parent.getRight() - parent.getPaddingRight(),parent.getPaddingTop() + header_height, mBgPaint);
String text=getHeaderContent(dataList.get(pos));
mTextPaint.getTextBounds(text, 0, text.length(), mBounds);
drawText(c,text, child.getPaddingLeft(), parent.getPaddingTop() + header_height - (header_height / 2 - mBounds.height() / 2),mTextPaint);
if (flag) {
c.restore();
}
}
/**
* 为需要添加头部的item设置padding,
* @param outRect
* @param view
* @param parent
* @param state
*/
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if(dataList==null&&dataList.size()==0)//没有数据不添加
return;
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
if(position<start_offset)///当前位置小于偏移量不添加
return;
if(position>(dataList.size()-1))//越界不添加
return;
if(position==start_offset){///偏移量其实必须要添加
outRect.set(0,header_height,0,0);
return;
}
if(needAddHeader(position,position-1)){//当前是否需要添加需要和前一个做判断
outRect.set(0,header_height,0,0);
return;
}
}
/**
* 根据位置决定当天item是否需要绘制头部
* @param p1
* @param p2
* @return
*/
public boolean needAddHeader(int p1,int p2){
return isNeedAddHeader(dataList.get(p1),dataList.get(p2));
}
/**
* 方法一: 根据内容决定当天item是否需要绘制头部
*/
public abstract boolean isNeedAddHeader(T data1,T data2);
/**
* 获取当前header 绘制的内容
*/
public abstract String getHeaderContent(T data);
/**
* 这么做方便给文字设置padding ,
* @param c
* @param content
* @param dx
* @param dy
* @param paint
*/
public void drawText(Canvas c,String content,int dx,int dy,TextPaint paint){
c.drawText(content,dx+text_padding_left,dy,paint);
}
}
子类实现方法
public class TsmItemDecorationImpl extends TsmItemDecoration<String> {
public TsmItemDecorationImpl(Context context, List<String> dataList) {
super(context, dataList);
}
@Override
public boolean isNeedAddHeader(String data1, String data2) {
return data1.contains("5");
}
@Override
public String getHeaderContent(String data) {
return data;
}
}
用起来非常方便,只要用你的model实现父类就好了,
recyclerView.addItemDecoration(new TsmItemDecorationImpl(this,getList()));