Android 进阶学习(九) RecyclerView 学习 (三) RecyclerView 粘性头部

说道粘性头部这个设计现在是越来越流行了,基本上每一个app都会涉及到,RecyclerView 的强哥不仅仅是他运行的高效率,还有非常好的兼容性,今天我就利用RecyclerView.ItemDecoration 来实现一个粘性头部

创建一个类继承自RecyclerView.ItemDecoration,由于数据的多样性,我们这里使用泛型

public class TsmItemDecoration 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 预留出来 头部高度的距离


image.png

onDraw

在绘制item的过程中,我们已经利用getItemOffsets 为需要添加头部的item预留出来了控件,onDraw 就可以利用这个空间在需要添加头部的item绘制的同时绘制这个头部,这个头部就属于item的一部分了,

image.png

onDrawOver

这个方法就是在绘制完item的时候可以绘制任意一个位置绘制一个图像,并且覆盖在item之上,


image.png

知道了这几个方法我们来写一个简单的效果
首先我们定义几个方法,便于将这个类作为父类,可以在整个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 extends RecyclerView.ItemDecoration {
   private Context context;
   private List dataList;
   private int start_offset;
   private int header_height=100;


   public TsmItemDecoration(Context context, List dataList){
       this.context=context;
       this.dataList=dataList;
       start_offset=0;
   }
   public TsmItemDecoration(Context context, List 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(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);
}

这些判断我写的很清楚了,大家可以借鉴一下,随便写了一个子类, 上个图片吧,就不贴代码了,


image.png

很简单每一条都添加header,运行看一下效果,


image.png

前面那个是没添加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(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);
   }

这里我们再继续上一张效果图

image.png

可以看到我们已经绘制出来了头部,这里我们修改一下,让数据里面包含有5个给一个header,这样看起来更清楚

我们再来说一下这个粘性头部的原理,不要觉得他是下面那个推着上面那个一起走,他只是你看起来的效果,真正的效果是


image.png

图片中的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= 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 extends RecyclerView.ItemDecoration {
   private Context context;
   private List 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 dataList){
       this.context=context;
       this.dataList=dataList;
       start_offset=0;
       init();
   }
   public TsmItemDecoration(Context context, List 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(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= 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(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 {

   public TsmItemDecorationImpl(Context context, List 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()));

你可能感兴趣的:(Android 进阶学习(九) RecyclerView 学习 (三) RecyclerView 粘性头部)