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

说道粘性头部这个设计现在是越来越流行了,基本上每一个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 预留出来 头部高度的距离


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

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


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

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

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<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()));
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容