RecyclerView

ItemDecoration

RecyclerView.ItemDecoration注释是这样写的

 /**
     * An ItemDecoration allows the application to add a special drawing and layout offset
     * to specific item views from the adapter's data set. This can be useful for drawing dividers
     * between items, highlights, visual grouping boundaries and more.
     *
     * <p>All ItemDecorations are drawn in the order they were added, before the item
     * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
     * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
     * RecyclerView.State)}.</p>
     */

允许程序添加特殊的图形和布局偏移量到适配器中指定的项目视图,可以用于项目视图之间绘制分割线、高亮等等。还指出了在项目之前调用onDraw() 之后调用onDrawOver;
三个重要方法的重写

public class ItemDecoration extends RecyclerView.ItemDecoration{

    //通过该方法,在Canvas上绘制内容,在绘制Item之前调用。(如果没有通过getItemOffsets设置偏移的话,Item的内容会将其覆盖)
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }
    //通过该方法,在RecyclerView的Canvas上绘制内容,在Item之后调用。(画的内容会覆盖在item的上层)
    @Override
    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);
    }
}

常见的悬浮粘性头部

ItemDecoration.png
public abstract class GroupHeadItemDecoration<T> extends RecyclerView.ItemDecoration {

    private Context mContext;
    private List<T> tags;
    private int groupHeaderHeight;
    private int groupHeaderLeftPadding;
    private Paint mPaint;
    private TextPaint mTextPaint;
    private boolean isStickHead = true;//是否是粘性头部

    public GroupHeadItemDecoration(Context context, List<T> tags) {
        mContext = context;
        this.tags = tags;
        groupHeaderHeight = dp2px(context, 20);
        groupHeaderLeftPadding = dp2px(context,10);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.parseColor("#FFEEEEEE"));

        mTextPaint = new TextPaint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(Color.parseColor("#FF999999"));
        mTextPaint.setTextSize(sp2px(context, 14));
    }

    public abstract String getTag(T t);//实现该抽象方法,获得分类的头部

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        for (int i = 0;i<parent.getChildCount();i++){
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            String tag = getTag(tags.get(position));
            //如果位置等于0 或者头部的tag不等于上一个tag,绘制分组头部
            if(position == 0 || !tag.equals(getTag(tags.get(position-1)))){
                drawGroupHeader(c,parent,view,tag);
            }
        }
    }

    /*
    *该方法同样也是用来绘制的,但是它在ItemDecoration的onDraw()方法和ItemView的onDraw()完成后才执行。
    * 所以其绘制的内容会遮挡在RecyclerView上,因此我们可以在该方法中绘制分组索引列表中悬浮的GroupHeader,
    * 也就是在列表顶部随着列表滚动切换的GroupHeader
     */

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        if (!isStickHead) {
            return;
        }
        if(tags.size()==0){
            return;
        }
        int position = ((LinearLayoutManager)(parent.getLayoutManager())).findFirstVisibleItemPosition();
        String tag = getTag(tags.get(position));
        View view = parent.findViewHolderForAdapterPosition(position).itemView;

        boolean flag = false;
        if((position+1)<tags.size()&&!tag.equals(getTag(tags.get(position+1)))){
            if (view.getBottom() <= groupHeaderHeight) {
                c.save();
                flag = true;
                c.translate(0, view.getHeight() + view.getTop() - groupHeaderHeight);
            }
        }
        drawSuspensionGroupHeader(c, parent, tag);

        if(flag){
            c.restore();
        }
    }

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

        RecyclerView.LayoutManager manager = parent.getLayoutManager();

        //只处理线性垂直类型的列表
        if ((manager instanceof LinearLayoutManager)
                && LinearLayoutManager.VERTICAL != ((LinearLayoutManager) manager).getOrientation()) {
            return;
        }

        if(tags == null || tags.size() == 0){
            return;
        }

        int position = parent.getChildAdapterPosition(view);
        if(position == 0||!getTag(tags.get(position)).equals(getTag(tags.get(position-1)))){
            outRect.set(0,groupHeaderHeight,0,0);
        }
    
    private void drawGroupHeader(Canvas c, RecyclerView parent, View view, String tag){
        int[] params = getGroupHeaderCoordinate(parent, view);
        c.drawRect(params[0], params[1], params[2], params[3], mPaint);
        int x = params[0] + groupHeaderLeftPadding;
        int y = params[1] + (groupHeaderHeight + getTextHeight(mTextPaint, tag)) / 2;
        c.drawText(tag,x,y,mTextPaint);
    }

    public int[] getGroupHeaderCoordinate(RecyclerView parent, View view) {
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth()-parent.getPaddingRight();
        int bottom = view.getTop() - params.topMargin;
        int top = bottom - groupHeaderHeight;
        return new int[]{left,top,right,bottom};
    }


    public int[] getSuspensionGroupHeaderCoordinate(RecyclerView parent) {
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int bottom = groupHeaderHeight;
        int top = 0;
        return new int[]{left, top, right, bottom};
    }

    private void drawSuspensionGroupHeader(Canvas c, RecyclerView parent, String tag) {
        int[] params = getSuspensionGroupHeaderCoordinate(parent);
        c.drawRect(params[0], params[1], params[2], params[3], mPaint);
        int x = params[0] + groupHeaderLeftPadding;
        int y = params[1] + (groupHeaderHeight + getTextHeight(mTextPaint, tag)) / 2;
        c.drawText(tag, x, y, mTextPaint);
    }

    public static int getTextHeight(TextPaint textPaint, String text) {
        Rect bounds = new Rect();
        textPaint.getTextBounds(text, 0, text.length(), bounds);
        return bounds.height();
    }
源码分析

RecyclerView的itemView的一些测量小细节,会通过getItemDecorInsetsForChild(child)调用装饰物的getItemOffsets,获得区域大小,累加宽高数值,然后完成测量

 public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
           // 累加当前ItemDecoration 4个属性值
            widthUsed += insets.left + insets.right;
            heightUsed += insets.top + insets.bottom;

            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight()
                            + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
                    canScrollHorizontally());
            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom()
                            + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
                    canScrollVertically());
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
                child.measure(widthSpec, heightSpec);
            }
        }


Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
           // 获取getItemOffsets() 中设置的值
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

ItemTouchHelper

RecyclerView通过系统的API就可以实现拖拽排序或者删除的效果

ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
            @Override
            public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
                return 0;
            }

            @Override
            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
                return false;
            }

            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {

            }
        });
        helper.attachToRecyclerView(rv);

实现 ItemTouchHelper.Callback 接口后有三个方法需要重写
1.getMovementFlags:设置滑动类型的标记
需要设置两种类型的 flag ,即 dragFlags 和 swipeFlags,拖拽标记和滑动标记
最后需要调用 makeMovementFlags(dragFlags, swipeFlags) 方法来合成返回
2.onMove: 当用户拖拽列表某个 item 时会回调。很明显,拖拽排序的代码应该在这个方法中实现。
3.onSwiped:当用户滑动列表某个 item 时会回调。所以侧滑删除的代码应该在这个方法中实现。

ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
            //通过返回值来设置是否处理某次拖曳或者滑动事件
            @Override
            public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
                if(recyclerView.getLayoutManager() instanceof GridLayoutManager){
                    int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN
                             | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
                    int swipeFlags = 0;
                    return makeMovementFlags(dragFlags,swipeFlags);
                }else {
                    int dragFlags = ItemTouchHelper.UP|ItemTouchHelper.DOWN;
                    int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
                    return makeMovementFlags(dragFlags,swipeFlags);
                }

            }

            @Override
            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
                int fromposition = viewHolder.getAdapterPosition();
                int toposition = target.getAdapterPosition();

                if(fromposition < toposition){
                    for(int i = fromposition;i<toposition;i++){
                        Collections.swap(list,i,i+1);
                    }
                }else {
                    for(int i = fromposition;i > toposition;i--){
                        Collections.swap(list,i,i-1);
                    }
                }
                mAdapter.notifyItemMoved(fromposition,toposition);
                return true;
            }

            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
                //滑动删除的回调 只要是linearlayoutmanager的时候

                int adapterPosition = viewHolder.getAdapterPosition();
                mAdapter.notifyItemRemoved(adapterPosition);
                list.remove(adapterPosition);
                //同时也不要忘了修改一下 getMovementFlags() 方法,以便能够相应滑动事件 
                //int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;

            }

            //我们发现还有一些不完美的地方:比如当用户在拖拽排序的时候,可以改变当前拖拽 item 的透明度,这样就可以和其他 item 区分开来了。
            // 那么,我们需要去重写 onSelectedChanged
           @Override
            public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
                //当长按 item 刚开始拖曳的时候调用
                if(actionState!=ItemTouchHelper.ACTION_STATE_IDLE){//拖拽或删除结束
                    viewHolder.itemView.setBackgroundColor(Color.YELLOW);
                }
                super.onSelectedChanged(viewHolder, actionState);
            }


            @Override
            public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
                //当完成拖曳手指松开的时候调用
                super.clearView(recyclerView, viewHolder);
                viewHolder.itemView.setBackgroundColor(getResources().getColor(R.color.colorAccent));
            }
}

SnapHelper

RecyclerView在24.2.0版本中新增了SnapHelper这个辅助类,用于辅助RecyclerView在滚动结束时将Item对齐到某个位置。特别是列表横向滑动时,很多时候不会让列表滑到任意位置,而是会有一定的规则限制,这时候就可以通过SnapHelper来定义对齐规则了。

LinearSnapHelper&PagerSnapHelper是抽象类SnapHelper的具体实现。
LinearSnapHelper 可以滑动多页
PagerSnapHelper 每次只能滑动一页

代码比较简单

LinearSnapHelper linearSnapHelper = new LinearSnapHelper();
linearSnapHelper.attachToRecyclerView(mRv)

源码分析

SnapHelper是一个抽象类

public abstract class SnapHelper extends RecyclerView.OnFlingListener{
        /**
     * Override this method to snap to a particular point within the target view or the container
     * view on any axis.
     * <p>
     * This method is called when the {@link SnapHelper} has intercepted a fling and it needs
     * to know the exact distance required to scroll by in order to snap to the target view.
     *
     * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
     *                      {@link RecyclerView}
     * @param targetView the target view that is chosen as the view to snap
     *
     * @return the output coordinates the put the result into. out[0] is the distance
     * on horizontal axis and out[1] is the distance on vertical axis.
     */
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
            @NonNull View targetView);

    /**
     * Override this method to provide a particular target view for snapping.
     * <p>
     * This method is called when the {@link SnapHelper} is ready to start snapping and requires
     * a target view to snap to. It will be explicitly called when the scroll state becomes idle
     * after a scroll. It will also be called when the {@link SnapHelper} is preparing to snap
     * after a fling and requires a reference view from the current set of child views.
     * <p>
     * If this method returns {@code null}, SnapHelper will not snap to any view.
     *
     * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
     *                      {@link RecyclerView}
     *
     * @return the target view to which to snap on fling or end of scroll
     */
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract View findSnapView(LayoutManager layoutManager);

    /**
     * Override to provide a particular adapter target position for snapping.
     *
     * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
     *                      {@link RecyclerView}
     * @param velocityX fling velocity on the horizontal axis
     * @param velocityY fling velocity on the vertical axis
     *
     * @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION}
     *         if no snapping should happen
     */
    public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
            int velocityY);
}

三个主要的抽象方法

findTargetSnapPosition

该方法会根据触发Fling操作的速率(参数velocityX和参数velocityY)来找到RecyclerView需要滚动到哪个位置,该位置对应的ItemView就是那个需要进行对齐的列表项。我们把这个位置称为targetSnapPosition,对应的View称为targetSnapView。如果找不到targetSnapPosition,就返回RecyclerView.NO_POSITION。

findSnapView

该方法会找到当前layoutManager上最接近对齐位置的那个view,该view称为SanpView,对应的position称为SnapPosition。如果返回null,就表示没有需要对齐的View,也就不会做滚动对齐调整。

calculateDistanceToFinalSnap

这个方法会计算第二个参数对应的ItemView当前的坐标与需要对齐的坐标之间的距离。该方法返回一个大小为2的int数组,分别对应x轴和y轴方向上的距离。

SnapHelper实现了OnFlingListener这个接口,该接口中的onFling()方法会在RecyclerView触发Fling操作时调用。在onFling()方法中判断当前方向上的速率是否足够做滚动操作,如果速率足够大就调用snapFromFling()方法实现滚动相关的逻辑。在snapFromFling()方法中会创建一个SmoothScroller,并且根据速率计算出滚动停止时的位置,将该位置设置给SmoothScroller并启动滚动。而滚动的操作都是由SmoothScroller全权负责,它可以控制Item的滚动速度(刚开始是匀速),并且在滚动到targetSnapView被layout时变换滚动速度(转换成减速),以让滚动效果更加真实。

让你明明白白的使用RecyclerView——SnapHelper详解

DiffUtil

它主要是为了配合 RecyclerView 使用,通过比对新、旧两个数据集的差异,生成旧数据到新数据的最小变动,然后对有变动的数据项,进行局部刷新。

需要关注的是两个类DiffUtil.Callback,DiffUtil.DiffResult
基本的使用方法是

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(mDiffCallback);
diffResult.dispatchUpdatesTo(mAdapter);

代码上可已看出更多的是diffcallback的实现,四个重写方法

  @Override
            public int getOldListSize() {
                return 0;//旧数据集的size
            }

            @Override
            public int getNewListSize() {
                return 0;//新数据集的size
            }

            @Override
            public boolean areItemsTheSame(int i, int i1) {
                return false;//同一个Item
            }

            @Override
            public boolean areContentsTheSame(int i, int i1) {
                return false;//如果是通一个Item,此方法用于判断是否同一个 Item 的内容也相同。
            }

不过,需要注意是子线程中去计算oldlist与newlist的差异

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

推荐阅读更多精彩内容