RecyclerView学习

RecyclerView

0、添加依赖、基本使用

compile 'com.android.support:recyclerview-v7:24.2.1'

版本就跟着你的

compile 'com.android.support:appcompat-v7:24.2.1'

版本走

0.1、recycler

本意是recycler,回收的意思,RecyclerView就是回收复用View

0.2、组成

主要由以下几部分组成:
RecyclerView.Adapter:处理数据绑定视图
ViewHolder:持有所有的用于绑定数据或者需要操作的View
LayoutManager:负责摆放视图等相关操作
ItemDecoration:分割线
ItemAnimator:增删动画

1、局部刷新

2、添加分割线

2.1、padding/margin

最简单的就是在单个item中添加padding或者margin,只是这样不友好;就像别人想要一栋新房子,但是你把自己的破房子刷一遍漆然后说是新房子,说不新吧,但看起来确实是新的,说新房子吧,又觉得恶心。

2.2、DividerItemDecoration

这个可以简单使用

RecyclerView添加分割线的简便方法
我参考的这个(题外话,本来我是想把源码翻一遍的,但是又觉得可能气候不到,那就再熟练运用一下,然后去翻源码,或许效果会好一点)

简单来说:

//添加Android自带的分割线
        recyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));

这个呈现的效果就是普通的效果,但是我这边呈现的不太一样,分割线特别粗,颜色好像是系统中的某个资源颜色,这种方式也就适合简单的看一下,并不适合直接使用,可以细化一下:

DividerItemDecoration divider = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
divider.setDrawable(ContextCompat.getDrawable(this, R.drawable.item_divider));
recyclerView.addItemDecoration(divider);

这里是自己定义一个资源文件,然后直接使用
item_divider.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <gradient
        android:centerColor="#ff00ff00"
        android:endColor="#ff0000ff"
        android:startColor="#ffff0000"
        android:type="linear" />
    <size android:height="3dip" />

</shape>

这里写图片描述

效果还可以,但是有两个问题,一就是最后一行也添加了分割线,不太好;二就是他只能适用于LinearLayoutManager这个原始ListView列表的呈现方式,想Grid或者StaggerGrid就不行了。

2.3、ItemDecoration

这就到了第三种方式了,实现RecyclerView的内部抽象动画类(抽象类里面为什么不放抽象方法呢?因为如果定义为普通类则实例化后没用,但是方法体已经定义好了,只能继承重写,所以定义为没有抽象方法的抽象类):

public abstract static class ItemDecoration {
        /**
         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
         * Any content drawn by this method will be drawn before the item views are drawn,
         * and will thus appear underneath the views.
         *
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView
         */
        public void onDraw(Canvas c, RecyclerView parent, State state) {
            onDraw(c, parent);
        }

        /**
         * @deprecated
         * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
         */
        @Deprecated
        public void onDraw(Canvas c, RecyclerView parent) {
        }

        /**
         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
         * Any content drawn by this method will be drawn after the item views are drawn
         * and will thus appear over the views.
         *
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView.
         */
        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
            onDrawOver(c, parent);
        }

        /**
         * @deprecated
         * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
         */
        @Deprecated
        public void onDrawOver(Canvas c, RecyclerView parent) {
        }


        /**
         * @deprecated
         * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
         */
        @Deprecated
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        /**
         * Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
         * the number of pixels that the item view should be inset by, similar to padding or margin.
         * The default implementation sets the bounds of outRect to 0 and returns.
         *
         * <p>
         * If this ItemDecoration does not affect the positioning of item views, it should set
         * all four fields of <code>outRect</code> (left, top, right, bottom) to zero
         * before returning.
         *
         * <p>
         * If you need to access Adapter for additional data, you can call
         * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
         * View.
         *
         * @param outRect Rect to receive the output.
         * @param view    The child view to decorate
         * @param parent  RecyclerView this ItemDecoration is decorating
         * @param state   The current state of RecyclerView.
         */
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

精简出来:

public abstract static class ItemDecoration {

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

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

        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

去掉过期方法就剩这三个方法了,这三个方法可就大有来头了:

RecyclerView之ItemDecoration由浅入深
(谁不是踩着大神的脚印往前走呢?)

onDraw:绘制内容背景,内容在上面;
onDrawOver:覆盖在内容上;
getItemOffsets:类似padding的效果;

public class DividerDecoration extends RecyclerView.ItemDecoration {

    private int dividerHeight;

    public DividerDecoration(int dividerHeight) {
        this.dividerHeight = dividerHeight;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.set(0, 0, 0, dividerHeight);
    }
}

使用:

recyclerView.addItemDecoration(new DividerDecoration(20));

但是这样分割线的颜色就是背景颜色了,不能够定制成自己需要的,或者就是要指定RecyclerView的背景色才行,这感觉又是个破房子刷漆的做法;

这次用getItemOffsets和onDraw结合的方法:


public class DividerDecoration extends RecyclerView.ItemDecoration {

    private int dividerHeight;
    private Paint mPaint;

    public DividerDecoration(int dividerHeight) {
        this.dividerHeight = dividerHeight;
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#6500ff"));
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.set(0, 0, 0, dividerHeight);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int childCount = parent.getChildCount();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();

        for (int i = 0; i < childCount - 1; i++) {
            View view = parent.getChildAt(i);
            float top = view.getBottom();
            float bottom = view.getBottom() + dividerHeight;
            c.drawRect(left, top, right, bottom, mPaint);
        }
    }
}

这样的话就能简单实现分割线了,还能定制宽度和颜色,而且也去掉了最后一个item的分割线。
这里写图片描述
那还有一个onDrawOver,这个能干什么呢?
我看到的博客上是可以做标签:我也想试一下
首先做一个item左边侵占一部分:

public class LeftDevider extends RecyclerView.ItemDecoration {
    private int leftWidth;
    private Paint leftPaint;

    public LeftDevider(int leftWidth) {
        this.leftWidth = leftWidth;
        leftPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        leftPaint.setColor(Color.parseColor("#662d17"));
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            float left = child.getLeft();
            float right = left + leftWidth;
            float top = child.getTop();
            float bottom = child.getBottom();
            c.drawRect(left, top, right, bottom, leftPaint);
        }
    }
}

这里写图片描述

这个发现没有分割线,但是很神奇的是他可以叠加使用,而不用去一个自定义类里面堆砌逻辑。

recyclerView.addItemDecoration(new DividerDecoration(5));
recyclerView.addItemDecoration(new LeftDevider(100));

这里写图片描述

这个就很吊了嘛!自己完全可以定义一堆,但是同时也引发一个问题:当页面销毁后重新加载时又叠加了,比如你用ViewPager组合Fragment,将列表应用于Fragment中,当间隔好像是两个的时候,就视图销毁重建了,但是这个状态信息还保留着,然后就再一次叠加了。

我在下面再模拟一下

粘性头部,这个是让我很激动的事情,我一直觉得这是一个很牛逼的东西,但是我不会,大部分又看不懂,或者没信心看下去,实在是头疼病,其实早前这个博客我就看过,但是看到一大堆代码,作者注解写得少,我就放弃了,这次我是专门学习RecyclerView偶然翻到的这个博客,再着重看了一下,RecyclerView果然吊

public class SectionDecoration1 extends RecyclerView.ItemDecoration {
    private DecorationCallBack callBack;
    private TextPaint textPaint;
    private Paint paint;
    private int topGap;

    public SectionDecoration1(Context mContext, DecorationCallBack callBack) {
        this.callBack = callBack;
        paint = new Paint();
        paint.setColor(Color.parseColor("#357da2"));

        textPaint = new TextPaint();
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(80);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextAlign(Paint.Align.LEFT);
        topGap = 60;
    }


    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int pos = parent.getChildAdapterPosition(view);
        long groupId = callBack.getGroupId(pos);
        if (groupId < 0) return;
        if (pos == 0 || isFirstInGroup(pos)) {
            outRect.top = topGap;
        } else {
            outRect.top = 0;
        }
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            long groupId = callBack.getGroupId(position);
            if (groupId < 0) return;
            String textLine = callBack.getGroupFirstLine(position).toUpperCase();
            if (position == 0 || isFirstInGroup(position)) {
                float top = view.getTop() - topGap;
                float bottom = view.getTop();
                c.drawRect(left, top, right, bottom, paint);
                c.drawText(textLine, left, bottom, textPaint);
            }
        }
    }

    private boolean isFirstInGroup(int pos) {
        if (pos == 0) {
            return true;
        } else {
            long prevGroupId = callBack.getGroupId(pos - 1);
            long groupId = callBack.getGroupId(pos);
            return prevGroupId != groupId;
        }
    }

    public interface DecorationCallBack {
        long getGroupId(int position);

        String getGroupFirstLine(int position);
    }
}

这里写图片描述

这不到一百行代码,其实真看起来也没什么,前面的初始化都是常识,主要就是4个方法:
接口方法:一个是获取首字母,用于比较;另一个是获取首字母用于显示;
isFirstInGroup:前提是大家都清楚,这个列表肯定是以组为单位的,如果是第一个item,不用说肯定要显示的,然后是前后比较,相同则什么都不处理,不同则添加一个头部;

getItemOffsets:根据是否添加头部的条件添加一个padding高度

onDraw:这个就是根据条件绘制头部了,条件也很简单,看一眼就能明白;主要是这是个不带粘性头部的,下面来一个带粘性头部的;

public class SectionDecoration2 extends RecyclerView.ItemDecoration {
    private DecorationCallBack callBack;
    private TextPaint textPaint;
    private Paint paint;
    private int topGap;

    public SectionDecoration2(Context mContext, DecorationCallBack callBack) {
        this.callBack = callBack;
        paint = new Paint();
        paint.setColor(Color.parseColor("#357da2"));

        textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setTextSize(Common.dip2px(mContext, 14));
        textPaint.setColor(Color.BLACK);
        textPaint.setTextAlign(Paint.Align.LEFT);
        topGap = Common.dip2px(mContext, 40);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int pos = parent.getChildAdapterPosition(view);
        long groupId = callBack.getGroupId(pos);
        if (groupId < 0) return;
        if (pos == 0 || isFirstInGroup(pos)) {
            outRect.top = topGap;
        } else {
            outRect.top = 0;
        }
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int itemCount = state.getItemCount();
        int childCount = parent.getChildCount();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();

        long preGroupId, groupId = -1;
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            preGroupId = groupId;
            groupId = callBack.getGroupId(position);

            if (groupId < 0 || groupId == preGroupId) continue;

            String textLine = callBack.getGroupFirstLine(position).toUpperCase();
            if (TextUtils.isEmpty(textLine)) continue;

            int viewBottom = view.getBottom();

            float textY = Math.max(topGap, view.getTop());
            if (position + 1 < itemCount) {
                long nextGroupId = callBack.getGroupId(position + 1);
                if (nextGroupId != groupId && viewBottom < textY) {
                    textY = viewBottom;
                }
            }

            int baseline = (int) (topGap / 2 + (textPaint.ascent() + textPaint.descent()) / 2);

            c.drawRect(left, textY - topGap, right, textY, paint);
            c.drawText(textLine, left + 20, textY - baseline, textPaint);
        }
    }

    private boolean isFirstInGroup(int pos) {
        if (pos == 0) {
            return true;
        } else {
            long prevGroupId = callBack.getGroupId(pos - 1);
            long groupId = callBack.getGroupId(pos);
            return prevGroupId != groupId;
        }
    }

    public interface DecorationCallBack {
        long getGroupId(int position);

        String getGroupFirstLine(int position);
    }

这里写图片描述

和上面比起来唯一的区别就是onDraw换成了onDrawOver,毕竟根据需求是要显示在View之上的;
这个前面的都差不多,主要就是

int viewBottom = view.getBottom();

            float textY = Math.max(topGap, view.getTop());
            if (position + 1 < itemCount) {
                long nextGroupId = callBack.getGroupId(position + 1);
                if (nextGroupId != groupId && viewBottom < textY) {
                    textY = viewBottom;
                }
            }

主要思想就是当没有下一组到来时,始终死守着topGap这个高度,显示在顶部,当下一组到来时头部则跟着View往上走,就是这么个逻辑,需要自己琢磨一下。

怎么使用:

recyclerView.addItemDecoration(new SectionDecoration2(this, new SectionDecoration2.DecorationCallBack() {
            @Override
            public long getGroupId(int position) {
                return Character.toUpperCase(datas.get(position).getName().charAt(0));
            }

            @Override
            public String getGroupFirstLine(int position) {
                return datas.get(position).getName().substring(0, 1).toUpperCase();
            }
        }));

再说一下上面提到的那个叠加使用的问题,我模拟一下,那就是上面的添加两边:

recyclerView.addItemDecoration(new SectionDecoration2(this, new SectionDecoration2.DecorationCallBack() {
            @Override
            public long getGroupId(int position) {
                return Character.toUpperCase(datas.get(position).getName().charAt(0));
            }

            @Override
            public String getGroupFirstLine(int position) {
                return datas.get(position).getName().substring(0, 1).toUpperCase();
            }
        }));
recyclerView.addItemDecoration(new SectionDecoration2(this, new SectionDecoration2.DecorationCallBack() {
            @Override
            public long getGroupId(int position) {
                return Character.toUpperCase(datas.get(position).getName().charAt(0));
            }

            @Override
            public String getGroupFirstLine(int position) {
                return datas.get(position).getName().substring(0, 1).toUpperCase();
            }
        }));

这里写图片描述

这里要慎重使用,需要考虑清楚

这里面每个方法都有一个过期方法,主要的区别就是多了一个状态信息State

RecyclerView机制分析: State

因为RecyclerView将功能进行了子模块化,还需要传递某些信息到特定子模块来完成功能/通信,RecyclerView把这部分职责集中到了State模块中。State内部聚合了所需的各项状态信息,扮演了状态上下文角色

3、item增删动画

这个我来来回回看了半天,但还是没有看懂,只能拖延了,应该是功底不够,那就等功底够得时候再来研究。
先发现一个可以直接用的库,先用着。

https://github.com/wasabeef/recyclerview-animators/

4、列表滑动动画

5、缓存机制

6、item布局

6.1、item布局-Linear、Grid、瀑布流

LinearLayoutManager 线性管理器,支持横向、纵向。
GridLayoutManager 网格布局管理器
StaggeredGridLayoutManager 瀑布式布局管理器

这几个布局是常见的,尤其是LinearLayoutManager,是用来替换ListView的根本,其实这几个使用起来还是比较简单的,傻瓜式的装填就可以,基本上都不出三行代码。

//layoutManager-LinearLayoutManager
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(manager);

这里写图片描述

//layoutManager-GridLayoutManager
GridLayoutManager manager = new GridLayoutManager(this, 3);
manager.setOrientation(GridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(manager);

这里写图片描述

//layoutManager-StaggeredGridLayoutManager
StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(manager);

这里写图片描述

怎么样?用起来还是超级简单的吧!

前面两个或许还比较好用,第三个就要设置不同的高度了,这个我开始是在onBindViewHolder中获取控件的LayoutManager,然后生成一个随机高度给他,但是当手势往下滑的时候,会出现item的跳动,找了半天没找到原因,后来打印才发现是滑动时不停的调用onBindViewHolder这个方法,每次又重新生成随机高度,所以会跳动(主要是因为了解的不多,不然怎么会犯这种错误)。所以要提前生成随机数然后传参进去,这样在复用View的时候就不会改变随机高度了。

6.2、头

有的稍微讲究的布局会添加一个头部,以前我接触到的是ListView,他本身是有添加头部这个方法的,简单易用,但是到了RecyclerView就没了,什么都得自己来。
之前也接触过,无非就是在RecyclerView上嵌套一个ViewGroup,用来在顶部填充一个头部,这个是可以的,到了这里我们可以用另一种方法,虽然这个方法是我非常抵制的,我很不喜欢,那就是将第一个item识别出来然后将它作为头部,剩下的item的角标依次-1,这样就可以了,然后在布局上,可以设置一个VIEWTYPE,当position=1的时候就采用头布局,剩下的就采用普通内容布局。

RecyclerView添加Header的正确方式

Linearlayoutmanager:

这里写图片描述

GridLayoutManager:

这里写图片描述

这里用到了一个方法来控制第一个position占据两列

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if (manager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) manager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    return getItemViewType(position) == HEAD ? gridManager.getSpanCount() : 1;
                }
            });
        }
    }

这里的GridLayoutManager要注意,之前我使用怎么都成功不了,然后debug之后发现这里的manager==null,怎么都想不明白,怎么会是null,我明明设置了的,后来看了一下函数名,才发现应该把setAdapter放到最后,起码放到设置manager之后,不然捕获到的manager就是null

StaggerGridLayoutManager:

这里也用到了一个方法setFullSpan

@Override
    public void onViewAttachedToWindow(LayoutManagerViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
        if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
            StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
            p.setFullSpan(holder.getLayoutPosition() == 0);
        }
    }

这里写图片描述

7、自定义点击事件

这个比较简单,网上普遍的做法就是自定义接口然后逻辑调用

    private OnClickListener onClickListener;

    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    public interface OnClickListener {
        void onClick(View v, int position);

        void onLongClick(View v, int position);
    }

在onBindViewHolder中设置

if (onClickListener != null) {
            holder.imageView.setOnClickListener(v -> {
                int pos = holder.getLayoutPosition();
                onClickListener.onClick(v, pos);
            });
            holder.imageView.setOnLongClickListener((v) -> {
                int pos = holder.getLayoutPosition();
                onClickListener.onLongClick(v, pos);
                return false;
            });
        }

然后就可以简单使用了。像普通点击事件一样使用,记住这个长按事件要返回false,不然根据触摸机制,当返回true的时候,点击事件就没有了,所以要返回false。

adapter.setOnClickListener(new LayoutManagerAdapter.OnClickListener() {
            @Override
            public void onClick(View v, int position) {
                Toast.makeText(LayoutManagerAty.this, "点击:" + position, Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onLongClick(View v, int position) {
                Toast.makeText(LayoutManagerAty.this, "长按:" + position, Toast.LENGTH_SHORT).show();
            }
        });

8、常用api

findFirstVisibleItemPosition() 返回当前第一个可见Item的position
findFirstCompletelyVisibleItemPosition() 返回当前第一个完全可见Item的position
findLastVisibleItemPosition() 返回当前最后一个可见Item的position
findLastCompletelyVisibleItemPosition() 返回当前最后一个完全可见Item的position

9、源码解读

10、封装

参考:
深入理解 RecyclerView 系列之一:ItemDecoration
自定义RecyclerView.ItemDecoration,实现Item的等间距分割以及分割线效果
RecyclerView机制分析: State


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值