RecyclerView 自定义ItemDecoration从入门到实现吸顶效果

RecyclerView性能和自由度相比ListView强大很多,但很恼人的是它没有像ListView一样默认提供分割线.

刚接触RecyclerView,用过才发现RecyclerView没有分割线过后,遂到网上搜解决办法才发现自定义一个ItemDecoration只要一条黑线还要写代码,好麻烦,不知道有没像我一样懒得折腾上网搜现成的,粘贴到项目直接用.

拖了很久才去解决这个问题,上网大致看了一下教程,其实不难而且自定义功能很强大.

首先新建一个类覆写ItemDecoration里面有三个方法:

public class SimpleItemDecoration extends RecyclerView.ItemDecoration {

    public SimpleItemDecoration(Context context) {

    }

    
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

    }


    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
    }
}
  • onDraw�名字很熟悉吧,和View中的onDraw一样,是用来画东西的, 在item上画分割线就靠这个方法了.
  • onDrawOver 英文Over的意思在...的上面 ,可以理解成是图层关系,item的内容和分割线是第一层(要在第一层画东西要调用onDraw),而onDrawOver是第二层,位于onDraw的上面
  • getItemOffsets 看名字可以知道是设置item的偏移值,其实效果和padding一样.

以上三个方法都是在RecylerView发生滑动的时候触发.

需要注意的是三个方法的都有一个RecyclerView parent,通过这个参数我们可以获取到RecyclerView的属性,例如 parent.getChildCount();获取子View的个数,但是这个并不是获取RecyclerView所有的item个数,而是当前屏幕可见的item个数.

所以画一条分割线需要的代码是这样的:


    private int wight;
    private int height;
    private int item_height;
    private int item_padding;
    private Paint paint;

public SimpleItemDecoration(Context context) {

        wight=context.getResources().getDisplayMetrics().widthPixels;
        height=context.getResources().getDisplayMetrics().heightPixels;
        paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
        paint.setColor(Color.BLACK);
        item_height=DensityUtil.dip2px(context, 1);
        item_padding=DensityUtil.dip2px(context, 10);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int count=parent.getChildCount();
        for (int i = 0; i < count; i++) {
            View view=parent.getChildAt(i);
            int top=view.getTop();
            int bottom=top+item_height;
            c.drawRect(0,top,wight,bottom,paint);

        }

         .....
    }

运行后得到如下图的效果.

纯黑色分割线

接着把 item_height=DensityUtil.dip2px(context, 1);的1改成30,你会发现item的内容和黑色的分割线重合了(黑人问号脸???)

item_height为30dp的黑色分割线

因为上面说了item和内容和onDraw中画的内容在同一图层,当然会被出现重合的情况.这个时候getItemOffsets就能派上用场了.只要在原来的item的加个偏移值(效果和在Adpater中为item设置padding的效果是一样的,只是在ItemDecoration统一处理比较合适)onDraw中画的分割线有多高,我就paddingBottom多少.

所以代码是是这样的:

  public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom=item_height;
    }

再次运行代码item被挡住的问题就解决了,RecyclerView的自定义ItemDecoration就是这么简单.有点自定义View经验的人理解起来都不会难

覆写getItemOffsets后的黑色分割线

分割线不要占满,要有和Left,Right有间距啊?

添加如下代码:


     private int wight;
    private int height;
    private int item_height;
    private Paint paint;
    private float item_padding;

    public SimpleItemDecoration(Context context) {

        wight=context.getResources().getDisplayMetrics().widthPixels;
        height=context.getResources().getDisplayMetrics().heightPixels;
        paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
        paint.setColor(Color.BLACK);
        item_height=DensityUtil.dip2px(context, 1);
        item_padding=DensityUtil.dip2px(context, 10);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int count=parent.getChildCount();
        for (int i = 0; i < count; i++) {
            View view=parent.getChildAt(i);
            int top=view.getBottom();
            int bottom=top+item_height;
            //这里把left和right的值分别增加item_padding,和减去item_padding.
            c.drawRect(item_padding,top,wight-item_padding,bottom,paint);

        }
          ....
    }

添加了左右间距的分割线效果

一般用到的分割线根据以上的代码再自己的按照需求稍微修改一下基本都能满足需求了.

之前在网上看到通过自定义ItemDecoration实现仿照旧版的instagram吸顶效果,感觉那种效果很好看,研究了一下发现只要理解了上面文章所说的几个方法实现起来并不难.

先来看最终效果图:


最终实现的效果图

要实现吸顶的效果需要完成这些步骤:

  1. 首先需要画一条高度足够容下文字和图片的分割线.
  2. 因为是吸顶效果,所以分割线和传统的分割线一样应该是在每个item的上方而不是下方
  3. 当前屏幕可见的第一个item的Bottom<=item_height(分割线的高度) 说明可见的第一个item的底部已经超出了分割线的高度,这个时候就应该让第一条分割线随着RecyclerView向上滑动直到滑出屏幕,这个时候第二个item就取代了第一个item变成了第一个item,否则分割线一直固定不动.
  4. 判断当前屏幕的第一个可见的item是哪个
  5. 把当前屏幕可见的item进行对比,如果item的内容第一个字相同,则把它们归为一组,用一条分割线显示即可.

先来实现1和2的要求,主要代码部分如下:


    private int wight;
    private int height;
    private int item_height;
    private Paint paint;
    private float item_padding;

    public SimpleItemDecoration(Context context) {

        wight=context.getResources().getDisplayMetrics().widthPixels;
        height=context.getResources().getDisplayMetrics().heightPixels;
        paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
        //更改画笔颜色为自定义的颜色
        paint.setColor(context.getResources().getColor(R.color.itemColor));
        item_height=DensityUtil.dip2px(context, 30);
        item_padding=DensityUtil.dip2px(context, 10);
    }


@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        //因为分割线是在item的上方,所以需要为每个item腾出一条分割线的高度
        outRect.top=item_height;

    }

@Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int count=parent.getChildCount();
        for (int i = 0; i < count; i++) {
            View view=parent.getChildAt(i);
            //分割线不能和item的内容重叠,所以把分割线画在getItemOffsets为item腾出来的位置上.
            //所以top需要上移item_height
            int top=view.getTop()-item_height;
            //bottom同理
            int bottom=top+item_height;
            c.drawRect(0,top,wight,bottom,paint);
        }
    }

....

最终实现的效果如图:

顶部分割线效果

注意看陈奕迅这个item的分割线是在item的上面的,并且分割线的高度已经足够容下我们稍后要绘制的内容了.

接着来实现3,怎么样才能让分割线在满足条件的时候动,不满足的时候固定?

这个时候就需要用到代码中一直没覆写的onDrawOver方法了,先来实现固定不动的分割线,代码也是非常的简单,在原来的代码上覆写onDrawOver方法
(这里new了新的画笔paint2,把固定的分割线用半透明红色来作为背景,方便理解效果):


        paint2=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
        paint2.setColor(Color.parseColor("#52ff0000"));
  
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
       


    }
固定的半透明分割线

接着来实现实现: 当前屏幕可见的第一个item的Bottom<=item_height(分割线的高度)让第一条分割线随着RecyclerView向上滑动直到滑出屏幕,代码如下:

@Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        View child0 = parent.getChildAt(0);

        //如果第一个item的Bottom<=分割线的高度
        if (child0.getBottom() <= item_height) {
            //随着RecyclerView滑动 分割线的top=固定为0不动,bottom则赋值为child0的bottom值.
            c.drawRect(0, 0, wight,child0.getBottom() , paint2);
        } else {
            //固定不动
            c.drawRect(0, 0, wight, item_height, paint2);

        }
    }
加了判断语句的半透明分割线

可以看到滑动时当第二item的顶部和第一个item的底部相互接触到后继续滑动的话第一个item就会慢慢向上滑动,直到第一个item完全画出屏幕,固定分割线立马回到最开始的位置和item2分割线重叠了在一起
,现在可以把paint2换回paint效果会更直观,不上效果图了,可以自己去测试.

对第一次接触ItemDecoration的人来说,难点都已经讲完了,剩下的就是在分割线范围计算出合适的位置调动drawText和drawBitmap画下文字和图片,直接贴上完整的源码:

(在源码注释里面已经把没有讲到的方法大致提了一下实现的原理)

自定义ItemDecoration的代码:



/**
 * Created by Lipt0n on 2017/8/26.
 */

public class SimpleItemDecoration extends RecyclerView.ItemDecoration {


    private Bitmap bitmap;
    private Paint.FontMetrics fontMetrics;
    private int wight;
    private int itemDecorationHeight;
    private Paint paint;
    private ObtainTextCallback callback;
    private float itemDecorationPadding;
    private TextPaint textPaint;
    private Rect text_rect=new Rect();
    public SimpleItemDecoration(Context context, ObtainTextCallback callback) {

        wight=context.getResources().getDisplayMetrics().widthPixels;
        paint=new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
        paint.setColor(context.getResources().getColor(R.color.itemColor));
        itemDecorationHeight=DensityUtil.dip2px(context, 30);
        itemDecorationPadding=DensityUtil.dip2px(context, 10);
        this.callback = callback;



        textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.LEFT);
        textPaint.setTextSize(DensityUtil.dip2px(context, 25));
        fontMetrics = new Paint.FontMetrics();
        textPaint.getFontMetrics(fontMetrics);

        bitmap= BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher_round);
        ScaleBitmap();
    }

    //bitmap的大小和itemDecorationHeight进行比较对图片进行缩放操作(对性能有追求可以在加载到内存的时候进行压缩)
    private void ScaleBitmap() {
        Matrix matrix=new Matrix();
        float scale=bitmap.getWidth()>itemDecorationHeight?Float.valueOf(itemDecorationHeight)/Float.valueOf(bitmap.getHeight()):Float.valueOf(bitmap.getHeight())/Float.valueOf(itemDecorationHeight);
        matrix.postScale(scale,scale);
        bitmap= Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,false);
    }



    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int count=parent.getChildCount();
        for (int i = 0; i < count; i++) {
            View view=parent.getChildAt(i);
            int top=view.getTop()-itemDecorationHeight;
            int bottom=top+itemDecorationHeight;


            int position = parent.getChildAdapterPosition(view);
            String content = callback.getText(position);
            textPaint.getTextBounds(content,0, content.length(),text_rect);

            if(isFirstInGroup(position)) {
                c.drawRect(0,top,wight,bottom,paint);
                c.drawText(content, itemDecorationPadding+bitmap.getWidth(), bottom-fontMetrics.descent, textPaint);
                c.drawBitmap(bitmap,itemDecorationPadding,bottom-bitmap.getHeight(),paint);
            }
        }
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

        View child0=parent.getChildAt(0);
        int position = parent.getChildAdapterPosition(child0);
        String content = callback.getText(position);
        if(child0.getBottom()<=itemDecorationHeight&&isFirstInGroup(position+1)){
            c.drawRect(0, 0, wight, child0.getBottom(), paint);
            c.drawText(content, itemDecorationPadding+bitmap.getWidth(), child0.getBottom()-fontMetrics.descent, textPaint);
            c.drawBitmap(bitmap,itemDecorationPadding,child0.getBottom()-bitmap.getHeight(),paint);
        }
        else {
            c.drawRect(0, 0, wight, itemDecorationHeight, paint);
            c.drawText(content, itemDecorationPadding+bitmap.getWidth(), itemDecorationHeight-fontMetrics.descent, textPaint);
            c.drawBitmap(bitmap,itemDecorationPadding,itemDecorationHeight-bitmap.getHeight(),paint);
        }
    }


    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position= parent.getChildAdapterPosition(view);
        //如果不是在同一组就腾出分割线需要的高度
        if(isFirstInGroup(position)){
            outRect.top=itemDecorationHeight;
        }

    }

    //回调接口,通过该回调获取item的内容的第一个文字
    public interface ObtainTextCallback {
        String getText(int position);
    }

    //判断当前item和下一个item的第一个文字是否相同,如果相同说明是同一组,不需要画分割线
    private boolean isFirstInGroup(int pos) {
       //如果是adapter的第一个position直接return,因为第一个item必须有分割线
        if (pos == 0) {
            return true;
        } else {
             //否者判断前一个item的字符串 与 当前item字符串 是否相同
            String prevGroupId = callback.getText(pos - 1);
            String groupId = callback.getText(pos);          
            if (prevGroupId.equals(groupId)) {
                return false;
            } else {
                return true;
            }
        }
    }
}

Activity中调用的代码:

recyclerView.addItemDecoration(new SimpleItemDecoration(this, new SimpleItemDecoration.ObtainTextCallback() {
            @Override
            public String getText(int position) {
                return dataList.get(position).substring(0,1);
            }
        }));

只要理解了最开始提到的ItemDecoration 的3个主要方法,再处理一下文字分组的逻辑实现起来不会太难,花点耐心还是能写出来的.

贴上github源码地址

以上就是文章全部的内容,如果你有什么疑问或建议可以在下面留言.

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

推荐阅读更多精彩内容