美文网首页Android-recyclerviewAndroid 进阶之旅
Android 进阶学习(九) RecyclerView 学习

Android 进阶学习(九) RecyclerView 学习

作者: Tsm_2020 | 来源:发表于2020-11-18 16:36 被阅读0次

    说道粘性头部这个设计现在是越来越流行了,基本上每一个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()));
    

    相关文章

      网友评论

        本文标题:Android 进阶学习(九) RecyclerView 学习

        本文链接:https://www.haomeiwen.com/subject/brrciktx.html