SnapHelper,对RecyclerView的功能拓展

来源:互联网 发布:网络运营推广方案模板 编辑:程序博客网 时间:2024/05/19 05:01

前言

SnapHelper是Google发布的support v4包24.2.0版本出来的。
SnapHelper是对RecyclerView功能的一种拓展,使RecyclerView滑动行为类似ViewPager,无论怎么滑动最终停留在某页正中间。
ViewPager一次只能滑动一页,RecyclerView+SnapHelper方式可以一次滑动好几页,且最终都停留在某页正中间。非常实用和酷炫。
SnapHelper的实现原理是监听RecyclerView.OnFlingListener中的onFling接口。LinearSnapHelper是抽象类SnapHelper的具体实现。

实现效果

1.LinearSnapHelper是自带的实现效果

类似ViewPager,将某页居中显示,实现也是很简单,只要下面的两行代码:

 LinearSnapHelper mLinearSnapHelper = new LinearSnapHelper(); mLinearSnapHelper.attachToRecyclerView(recycleView);

这里写图片描述

我们来看下LinearSnapHelper是怎么实现SnapHelper的,其中主要实现3个方法:

1.calculateDistanceToFinalSnap()

    @Override    public int[] calculateDistanceToFinalSnap(            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {        int[] out = new int[2];        if (layoutManager.canScrollHorizontally()) {            out[0] = distanceToCenter(layoutManager, targetView,                    getHorizontalHelper(layoutManager));        } else {            out[0] = 0;        }        if (layoutManager.canScrollVertically()) {            out[1] = distanceToCenter(layoutManager, targetView,                    getVerticalHelper(layoutManager));        } else {            out[1] = 0;        }        return out;    }

当拖拽或滑动结束时会回调该方法,返回一个out = int[2],out[0]x轴,out[1] y轴 ,这个值就是需要修正的你需要的位置的偏移量 。

2.findSnapView()

    @Override    public View findSnapView(RecyclerView.LayoutManager layoutManager) {        if (layoutManager.canScrollVertically()) {            return findCenterView(layoutManager, getVerticalHelper(layoutManager));        } else if (layoutManager.canScrollHorizontally()) {            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));        }        return null;    }

看方法名就知道,找到对齐视图,就是上个方法的targetView。

3.findTargetSnapPosition()

    @Override    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,            int velocityY) {        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {            return RecyclerView.NO_POSITION;        }        final int itemCount = layoutManager.getItemCount();        if (itemCount == 0) {            return RecyclerView.NO_POSITION;        }        final View currentView = findSnapView(layoutManager);        if (currentView == null) {            return RecyclerView.NO_POSITION;        }        final int currentPosition = layoutManager.getPosition(currentView);        if (currentPosition == RecyclerView.NO_POSITION) {            return RecyclerView.NO_POSITION;        }        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;        // deltaJumps sign comes from the velocity which may not match the order of children in        // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to        // get the direction.        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);        if (vectorForEnd == null) {            // cannot get a vector for the given position.            return RecyclerView.NO_POSITION;        }        int vDeltaJump, hDeltaJump;        if (layoutManager.canScrollHorizontally()) {            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,                    getHorizontalHelper(layoutManager), velocityX, 0);            if (vectorForEnd.x < 0) {                hDeltaJump = -hDeltaJump;            }        } else {            hDeltaJump = 0;        }        if (layoutManager.canScrollVertically()) {            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,                    getVerticalHelper(layoutManager), 0, velocityY);            if (vectorForEnd.y < 0) {                vDeltaJump = -vDeltaJump;            }        } else {            vDeltaJump = 0;        }        int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;        if (deltaJump == 0) {            return RecyclerView.NO_POSITION;        }        int targetPos = currentPosition + deltaJump;        if (targetPos < 0) {            targetPos = 0;        }        if (targetPos >= itemCount) {            targetPos = itemCount - 1;        }        return targetPos;    }

滑动结束时,用于OnFling,返回目标对齐项position 。

2.自定义SnapHelper实现左对齐或右对齐

其实通过上面的分析,就会发现最主要的就是 calculateDistanceToFinalSnap 和 findSnapView 这两个函数。

在寻找目标View的时候,不像findCenterView那么简单。
以为需要考虑到最后item的边界情况。判断的不好就会出现,无论怎么滑动都会出现最后一个item无法完整显示的bug。

package com.example.myapplication.com.example;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.LinearSnapHelper;import android.support.v7.widget.OrientationHelper;import android.support.v7.widget.RecyclerView;import android.view.View;public class MySnapHelper extends LinearSnapHelper {    // 左对齐    public static final int TYPE_SNAP_START = 2;    // 右对齐    public static final int TYPE_SNAP_END = 3;    // default    private int type = TYPE_SNAP_START;    @Nullable    private OrientationHelper mVerticalHelper;    @Nullable    private OrientationHelper mHorizontalHelper;    public MySnapHelper(int type) {        this.type = type;    }    @Override    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {        if (type == TYPE_SNAP_START) {            return calculateDisOnStart(layoutManager, targetView);        } else if (type == TYPE_SNAP_END) {            return calculateDisOnEnd(layoutManager, targetView);        } else {            return super.calculateDistanceToFinalSnap(layoutManager, targetView);        }    }    @Override    public View findSnapView(RecyclerView.LayoutManager layoutManager) {        if (type == TYPE_SNAP_START) {            return findStartSnapView(layoutManager);        } else if (type == TYPE_SNAP_END) {            return findEndSnapView(layoutManager);        } else {            return super.findSnapView(layoutManager);        }    }    /**     * TYPE_SNAP_START     *     * @param layoutManager     * @param targetView     * @return     */    private int[] calculateDisOnStart(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {        int[] out = new int[2];        if (layoutManager.canScrollHorizontally()) {            out[0] = distanceToStart(layoutManager, targetView,                    getHorizontalHelper(layoutManager));        } else {            out[0] = 0;        }        if (layoutManager.canScrollVertically()) {            out[1] = distanceToStart(layoutManager, targetView,                    getVerticalHelper(layoutManager));        } else {            out[1] = 0;        }        return out;    }    /**     * TYPE_SNAP_END     *     * @param layoutManager     * @param targetView     * @return     */    private int[] calculateDisOnEnd(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {        int[] out = new int[2];        if (layoutManager.canScrollHorizontally()) {            out[0] = distanceToEnd(layoutManager, targetView,                    getHorizontalHelper(layoutManager));        } else {            out[0] = 0;        }        if (layoutManager.canScrollVertically()) {            out[1] = distanceToEnd(layoutManager, targetView,                    getVerticalHelper(layoutManager));        } else {            out[1] = 0;        }        return out;    }    /**     * calculate distance to start     *     * @param layoutManager     * @param targetView     * @param helper     * @return     */    private int distanceToStart(@NonNull RecyclerView.LayoutManager layoutManager,                                @NonNull View targetView, OrientationHelper helper) {        return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();    }    /**     * calculate distance to end     *     * @param layoutManager     * @param targetView     * @param helper     * @return     */    private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,                              @NonNull View targetView, OrientationHelper helper) {        return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();    }    /**     * find the start view     *     * @param layoutManager     * @return     */    private View findStartSnapView(RecyclerView.LayoutManager layoutManager) {        if (layoutManager.canScrollVertically()) {            return findStartView(layoutManager, getVerticalHelper(layoutManager));        } else if (layoutManager.canScrollHorizontally()) {            return findStartView(layoutManager, getHorizontalHelper(layoutManager));        }        return null;    }    /**     * 注意判断最后一个item时,应通过判断距离右侧的位置     *     * @param layoutManager     * @param helper     * @return     */    private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {        if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager            return null;        }        int childCount = layoutManager.getChildCount();        if (childCount == 0) {            return null;        }        View closestChild = null;        final int start = helper.getStartAfterPadding();        int absClosest = Integer.MAX_VALUE;        for (int i = 0; i < childCount; i++) {            final View child = layoutManager.getChildAt(i);            int childStart = helper.getDecoratedStart(child);            int absDistance = Math.abs(childStart - start);            if (absDistance < absClosest) {                absClosest = absDistance;                closestChild = child;            }        }        View firstVisibleChild = layoutManager.getChildAt(0);        if (firstVisibleChild != closestChild) {            return closestChild;        }        int firstChildStart = helper.getDecoratedStart(firstVisibleChild);        int lastChildPos = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();        View lastChild = layoutManager.getChildAt(childCount - 1);        int lastChildCenter = helper.getDecoratedStart(lastChild) + (helper.getDecoratedMeasurement(lastChild) / 2);        boolean isEndItem = lastChildPos == layoutManager.getItemCount() - 1;        if (isEndItem && firstChildStart < 0 && lastChildCenter < helper.getEnd()) {            return lastChild;        }        return closestChild;    }    /**     * find the end view     *     * @param layoutManager     * @return     */    private View findEndSnapView(RecyclerView.LayoutManager layoutManager) {        if (layoutManager.canScrollVertically()) {            return findEndView(layoutManager, getVerticalHelper(layoutManager));        } else if (layoutManager.canScrollHorizontally()) {            return findEndView(layoutManager, getHorizontalHelper(layoutManager));        }        return null;    }    private View findEndView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {        if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager            return null;        }        int childCount = layoutManager.getChildCount();        if (childCount == 0) {            return null;        }        if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == 0) {            return null;        }        View closestChild = null;        final int end = helper.getEndAfterPadding();        int absClosest = Integer.MAX_VALUE;        for (int i = 0; i < childCount; i++) {            final View child = layoutManager.getChildAt(i);            int childStart = helper.getDecoratedEnd(child);            int absDistance = Math.abs(childStart - end);            if (absDistance < absClosest) {                absClosest = absDistance;                closestChild = child;            }        }        View lastVisibleChild = layoutManager.getChildAt(childCount - 1);        if (lastVisibleChild != closestChild) {            return closestChild;        }        if (layoutManager.getPosition(closestChild) == ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()) {            return closestChild;        }        View firstChild = layoutManager.getChildAt(0);        int firstChildStart = helper.getDecoratedStart(firstChild);        int firstChildPos = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();        boolean isFirstItem = firstChildPos == 0;        int firstChildCenter = helper.getDecoratedStart(firstChild) + (helper.getDecoratedMeasurement(firstChild) / 2);        if (isFirstItem && firstChildStart < 0 && firstChildCenter > helper.getStartAfterPadding()) {            return firstChild;        }        return closestChild;    }    @NonNull    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {        if (mVerticalHelper == null) {            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);        }        return mVerticalHelper;    }    @NonNull    private OrientationHelper getHorizontalHelper(            @NonNull RecyclerView.LayoutManager layoutManager) {        if (mHorizontalHelper == null) {            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);        }        return mHorizontalHelper;    }}

最后只要用上我们自己的SnapHelper,就可以轻松搞定了。

MySnapHelper mySnapHelper = new MySnapHelper(2);mySnapHelper.attachToRecyclerView(recycleView);

ps:

上面代码中如果使用分隔线,在居中对齐和右对齐时,位移会有误差。
原因是:在计算偏移量时targetView包含item和分隔线。所以我们在计算偏移量时需要把分隔线宽度减掉,
以右对齐为例:在distanceToEnd()中把

private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,                              @NonNull View targetView, OrientationHelper helper) {        //无分隔线        return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();    }

改为

private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,                              @NonNull View targetView, OrientationHelper helper) {        //有分割线        return helper.getDecoratedStart(targetView) - helper.getEndAfterPadding() + targetView.getWidth();    }

如果使用的是居中对齐+分隔线,由于自带LinearSnapHelper无法更改,我们可以新建类继承SnapHelper,把LinearSnapHelper中代码全部copy过来,只需更改distanceToCenter()方法即可。

好了,基本就没问题了。
最后,个人建议使用此效果最好不要用分隔线……