SnapHelper

来源:互联网 发布:fc游戏制作软件 编辑:程序博客网 时间:2024/05/06 01:35

转载请注明出处:http://blog.csdn.net/crazy1235/article/details/53386286


SnapHelper 是 Android Support Library reversion 24.2.0 新增加的API。


SnapHelper 的应用

SnapHelper 是RecyclerView的一个辅助工具类。

它实现了RecyclerView.onFlingListener接口。而RecyclerView.onFlingListener 是一个用来响应用户手势滑动的接口。

SnapHelper是一个抽象类,官方提供了一个LinearSnapHelper子类,可以实现类似ViewPager的滚动效果,滑动结束之后让某个item停留在中间位置。

这里写图片描述

效果类似于Google Play主界面中item的滚动效果。




LinearSnapHelper的使用很简单,只需要调用 attachToRecyclerView(xxx) ,绑定上一个RecyclerView即可。

上一张自己的效果图:

这里写图片描述


LinearSnapHelper 源码分析

下面来分析一下 LinearSnapHelper

先从 attachToRecyclerView() 入手。

public void attachToRecyclerView(@Nullable RecyclerView recyclerView)            throws IllegalStateException {        if (mRecyclerView == recyclerView) {            return; // nothing to do        }        if (mRecyclerView != null) {            destroyCallbacks();        }        mRecyclerView = recyclerView;        if (mRecyclerView != null) {            setupCallbacks();            mGravityScroller = new Scroller(mRecyclerView.getContext(),                    new DecelerateInterpolator());            snapToTargetExistingView();        }    }

destoryCallback() 作用在于取消之前的RecyclerView的监听接口。

/** * Called when the instance of a {@link RecyclerView} is detached. */    private void destroyCallbacks() {        mRecyclerView.removeOnScrollListener(mScrollListener);        mRecyclerView.setOnFlingListener(null);    }

setupCallbacks() – 设置监听器

/**     * Called when an instance of a {@link RecyclerView} is attached.     */    private void setupCallbacks() throws IllegalStateException {        if (mRecyclerView.getOnFlingListener() != null) {            throw new IllegalStateException("An instance of OnFlingListener already set.");        }        mRecyclerView.addOnScrollListener(mScrollListener);        mRecyclerView.setOnFlingListener(this);    }

此时可以看到,如果当前RecyclerView已经设置了OnFlingListener,会抛出一个 状态异常


snapToTargetExistingView()

/** * 找到居中显示的view,计算它的位置,调用smoothScrollBy使其居中 */void snapToTargetExistingView() {        if (mRecyclerView == null) {            return;        }        LayoutManager layoutManager = mRecyclerView.getLayoutManager();        if (layoutManager == null) {            return;        }        View snapView = findSnapView(layoutManager);        if (snapView == null) {            return;        }        // 计算目标View需要移动的距离        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);        if (snapDistance[0] != 0 || snapDistance[1] != 0) {            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);        }    }

该方法中显示调用 findSnapView() 找到目标View(需要居中显示的View),然后调用 calculateDistanceToFinalSnap() 来计算该目标View需要移动的距离。这两个方法均需要LinearSnapHelper重写。


SnapHelper.java 中有三个抽象函数需要LinearSnapHelper 重写。

/** * 找到那个“snapView” */public abstract View findSnapView(LayoutManager layoutManager);
/** * 计算targetView需要移动的距离 * 该方法返回一个二维数组,分别表示X轴、Y轴方向上需要修正的偏移量 */public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,            @NonNull View targetView);
/** * 根据速度找到将要滑到的position */public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,            int velocityY);

在setupCallbacks() 方法中可以看到对RecyclerView 设置了 OnScrollListener OnFlingListener 两个监听器。

查看SnapHelper可以发现:

// Handles the snap on scroll case.    private final RecyclerView.OnScrollListener mScrollListener =            new RecyclerView.OnScrollListener() {                boolean mScrolled = false;                @Override                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {                    super.onScrollStateChanged(recyclerView, newState);                    if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {                        mScrolled = false;                        snapToTargetExistingView();                    }                }                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {                    if (dx != 0 || dy != 0) {                        mScrolled = true;                    }                }            };    @Override    public boolean onFling(int velocityX, int velocityY) {        LayoutManager layoutManager = mRecyclerView.getLayoutManager();        if (layoutManager == null) {            return false;        }        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();        if (adapter == null) {            return false;        }        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)                && snapFromFling(layoutManager, velocityX, velocityY);    }

当滚动结束是,会调用 snapToTargetExistingView() 方法。

而当手指滑动触发onFling() 函数时,会根据X轴、Y轴方向上的速率加上 snapFromFling() 方法的返回值综合判断。


看一下 snapFromFling()

/**     * Helper method to facilitate for snapping triggered by a fling.     *     * @param layoutManager The {@link LayoutManager} associated with the attached     *                      {@link RecyclerView}.     * @param velocityX     Fling velocity on the horizontal axis.     * @param velocityY     Fling velocity on the vertical axis.     *     * @return true if it is handled, false otherwise.     */    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,            int velocityY) {        if (!(layoutManager instanceof ScrollVectorProvider)) {            return false;        }        // 创建SmoothScroll对象        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);        if (smoothScroller == null) {            return false;        }        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);        if (targetPosition == RecyclerView.NO_POSITION) {            return false;        }        smoothScroller.setTargetPosition(targetPosition);        layoutManager.startSmoothScroll(smoothScroller);        return true;    }

接下来看LinearSnapHelper.java 复写的三个方法

@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;        }        // 重点在findSnapView()        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;        }        // ...省略若干代码        return targetPos;    }

省略的若干代码主要是根据手势滑动的速率计算目标item的位置。具体算法不用多研究。

可以看到方法内部又调用了 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;    }

这里根据LayoutManager的方向做个判断,进而调用 findCenterView() 方法。

/**     * 返回距离父容器中间位置最近的子View     */    @Nullable    private View findCenterView(RecyclerView.LayoutManager layoutManager,            OrientationHelper helper) {        int childCount = layoutManager.getChildCount();        if (childCount == 0) {            return null;        }        View closestChild = null;        final int center; // 中间位值        if (layoutManager.getClipToPadding()) {            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;        } else {            center = helper.getEnd() / 2;        }        int absClosest = Integer.MAX_VALUE;        for (int i = 0; i < childCount; i++) {  // 循环判断子View中间位值距离父容器中间位值的差值            final View child = layoutManager.getChildAt(i);            int childCenter = helper.getDecoratedStart(child) +                    (helper.getDecoratedMeasurement(child) / 2);            int absDistance = Math.abs(childCenter - center);            /** if child center is closer than previous closest, set it as closest  **/            if (absDistance < absClosest) {                absClosest = absDistance;                closestChild = child;            }        }        return closestChild; // 返回距离父容器中间位置最近的子View    }

然后来看 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;    }

定义一个二维数组,根据LayoutManager的方向来判断进行赋值。

private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,            @NonNull View targetView, OrientationHelper helper) {        final int childCenter = helper.getDecoratedStart(targetView) +                (helper.getDecoratedMeasurement(targetView) / 2);        final int containerCenter;        if (layoutManager.getClipToPadding()) {            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;        } else {            containerCenter = helper.getEnd() / 2;        }        return childCenter - containerCenter;    }

该方法的目的即是 计算目标View距离父容器中间位值的差值


至此,流程已经分析完毕。

总结如下:

  1. 有速率的滑动,会触发onScrollStateChanged() onFling() 两个方法。

    • onScrollStateChanged() 方法内部调用 findSnapView() 找到对应的View,然后据此View在调用calculateDistanceToFinalSnap() 来计算该目标View需要移动的距离,最后通过RecyclerView.smoothScrollBy() 来移动View。

    • onFling() 方法内部调用 snapFromFling(), 然后在此方法内部首先创建了一个SmoothScroller 对象。接着调用 findTargetSnapPosition() 找到目标View的position,然后对smoothScroller设置该position,最后通过LayoutManager.startSmoothScroll() 开始移动View。

  2. 没有速率的滚动只会触发 onScrollStateChanged() 函数。


扩展

LinearSnapHelper 类的目的是将某个View停留在正中间,我们也可以通过这种方式来实现每次滑动结束之后将某个View停留在最左边或者最右边。

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

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

且看我的代码:

/**     * 注意判断最后一个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;    }

对于“反向的”同样要考虑边界情况。

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;    }

效果图如下:

这里写图片描述

这里写图片描述


完整代码,请移步:

JackSnapHelper.java


完毕,谢谢支持~~

6 1
原创粉丝点击