Android中使用RecyclerView + SnapHelper实现相似ViewPager效果

1 . 前言

在一些特定的场景下,如照片的浏览,卡片列表滑动浏览,咱们但愿当滑动中止时能够将当前的照片或者卡片停留在屏幕中央,以吸引用户的焦点。在Android 中,咱们可使用RecyclerView + Snaphelper来实现,SnapHelper旨在支持RecyclerView的对齐方式,也就是经过计算对齐RecyclerView中TargetView 的指定点或者容器中的任何像素点(包括前面说的显示在屏幕中央)。本篇文章将详细介绍SnapHelper的相关知识点。本文目录以下:java

目录.png

2 . SnapHelper 介绍

Google 在 Android 24.2.0 的support 包中添加了SnapHelper,SnapHelper是对RecyclerView的拓展,结合RecyclerView使用,能很方便的作出一些炫酷的效果。SnapHelper到底有什么功能呢?SnapHelper旨在支持RecyclerView的对齐方式,也就是经过计算对齐RecyclerView中TargetView 的指定点或者容器中的任何像素点。,可能有点很差理解,看了后文的效果和原理分析就好理解了。看一下文档介绍:
git

SnapHealper 介绍.png

SnapHelper继承自RecyclerView.OnFlingListener,并实现了它的抽象方法onFling, 支持SnapHelper的RecyclerView.LayoutManager必须实现RecyclerView.SmoothScroller.ScrollVectorProvider接口,或者你本身实现onFling(int,int)方法手动处理。SnapHeper 有如下几个重要方法:github

  • attachToRecyclerView: 将SnapHelper attach 到指定的RecyclerView 上。数组

  • calculateDistanceToFinalSnap: 复写这个方法计算对齐到TargetView或容器指定点的距离,这是一个抽象方法,由子类本身实现,返回的是一个长度为2的int 数组out,out[0]是x方向对齐要移动的距离,out[1]是y方向对齐要移动的距离。 ide

  • calculateScrollDistance: 根据每一个方向给定的速度估算滑动的距离,用于Fling 操做。spa

  • findSnapView:提供一个指定的目标View 来对齐,抽象方法,须要子类实现3d

  • findTargetSnapPosition:提供一个用于对齐的Adapter 目标position,抽象方法,须要子类本身实现。code

  • onFling:根据给定的x和 y 轴上的速度处理Fling。cdn

3 . LinearSnapHelper & PagerSnapHelper

上面讲了SnapHelper的几个重要的方法和做用,SnapHelper是一个抽象类,要使用SnapHelper,须要实现它的几个方法。而 Google 内置了两个默认实现类,LinearSnapHelperPagerSnapHelper ,LinearSnapHelper可使RecyclerView 的当前Item 居中显示(横向和竖向都支持),PagerSnapHelper看名字可能就能猜到,使RecyclerView 像ViewPager同样的效果,每次只能滑动一页(LinearSnapHelper支持快速滑动), PagerSnapHelper也是Item居中对齐。接下来看一下使用方法和效果。对象

(1) LinearSnapHelper
LinearSnapHelper 使当前Item居中显示,经常使用场景是横向的RecyclerView, 相似ViewPager效果,可是又能够快速滑动(滑动多页)。代码以下:

LinearLayoutManager manager = new LinearLayoutManager(getContext());
 manager.setOrientation(LinearLayoutManager.VERTICAL);
 mRecyclerView.setLayoutManager(manager);
// 将SnapHelper attach 到RecyclrView
 LinearSnapHelper snapHelper = new LinearSnapHelper();
 snapHelper.attachToRecyclerView(mRecyclerView);复制代码

代码很简单,new 一个SnapHelper对象,而后 Attach到RecyclerView 便可。

效果以下:

LineSnapHelper_竖直方向.gif

上面的效果为LayoutManager的方向为VERTICAL,那么接下来看一下横向效果,很简单,和上面的区别只是更改一下LayoutManager的方向,代码以下:

LinearLayoutManager manager = new LinearLayoutManager(getContext());
 manager.setOrientation(LinearLayoutManager.HORIZONTAL);
 mRecyclerView.setLayoutManager(manager);
// 将SnapHelper attach 到RecyclrView
 LinearSnapHelper snapHelper = new LinearSnapHelper();
 snapHelper.attachToRecyclerView(mRecyclerView);复制代码

效果以下:

LineSnapHelper_水平方向.gif

如上图所示,简单几行代码就能够用RecyclerView 实现一个相似ViewPager的效果,而且效果更赞。能够快速滑动多页,当前页剧中显示,而且显示前一页和后一页的部分。若是使用ViewPager来作仍是有点麻烦的。除了上面的效果外,若是你想要和ViewPager 同样,限制一次只让它滑动一页,那么你就可使用PagerSnapHelper了,接下来看一下PagerSnapHelper的使用效果。

(2) PagerSnapHelper (在Android 25.1.0 support 包加入的)
PagerSnapHelper的展现效果和LineSnapHelper是同样的,只是PagerSnapHelper 限制一次只能滑动一页,不能快速滑动。代码以下:

PagerSnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);复制代码

PagerSnapHelper效果以下:

PagerSnapHelper.gif

上面展现的是PagerSnapHelper水平方向的效果,竖直方向的效果和LineSnapHelper竖直的方向的效果差很少,只是不能快速滑动,就不在介绍了,感兴趣的能够把它们的效果都试一下。

上面就是LineSnapHelperPagerSnapHelper的使用和效果展现,了解了它的使用方法和效果,接下来咱们看一下它的实现原理。

4 . SnapHelper原码分析

上面介绍了SnapHelper的使用,那么接下来咱们来看一下SnapHelper究竟是怎么实现的,走读一下源码:

(1) 入口方法,attachToRecyclerView
经过attachToRecyclerView方法将SnapHelper attach 到RecyclerView,看一下这个方法作了哪些事情:

/** * * 1,首先判断attach的RecyclerView 和原来的是不是同样的,同样则返回,不同则替换 * * 2,若是不是同一个RecyclerView,将原来设置的回调所有remove或者设置为null * * 3,Attach的RecyclerView不为null,先2设置回调 滑动的回调和Fling操做的回调, * 初始化一个Scroller 用于后面作滑动处理,而后调用snapToTargetExistingView * * */
    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();
        }
    }复制代码

(2) snapToTargetExistingView :这个方法用于第一次Attach到RecyclerView 时对齐TargetView,或者当Scroll 被触发的时候和fling操做的时候对齐TargetView 。在attachToRecyclerViewonScrollStateChanged中都调用了这个方法。

/** * * 1,判断RecyclerView 和LayoutManager是否为null * * 2,调用findSnapView 方法来获取须要对齐的目标View(这是个抽象方法,须要子类实现) * * 3,经过calculateDistanceToFinalSnap 获取x方向和y方向对齐须要移动的距离 * * 4,最后经过RecyclerView 的smoothScrollBy 来移动对齐 * */
    void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }复制代码

(3) Filing 操做时对齐:SnapHelper继承了 RecyclerView.OnFlingListener,实现了onFling方法。

/** * fling 回调方法,方法中调用了snapFromFling,真正的对齐逻辑在snapFromFling里 */
@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);
    }
 /** *snapFromFling 方法被fling 触发,用来帮助实现fling 时View对齐 * */
 private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) {
       // 首先须要判断LayoutManager 实现了ScrollVectorProvider 接口没有,
      //若是没有实现 ,则直接返回。
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }
      // 建立一个SmoothScroller 用来作滑动到指定位置
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
        // 根据x 和 y 方向的速度来获取须要对齐的View的位置,须要子类实现。
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
       // 最终经过 SmoothScroller 来滑动到指定位置
        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }复制代码

其实经过上面的3个方法就实现了SnapHelper的对齐,只是有几个抽象方法是没有实现的,具体的对齐规则交给子类去实现。

接下来看一下LinearSnapHelper 是怎么实现剧中对齐的:主要是实现了上面提到的三个抽象方法,findTargetSnapPositioncalculateDistanceToFinalSnapfindSnapView

(1) calculateDistanceToFinalSnap : 计算最终对齐要移动的距离,返回一个长度为2的int 数组out,out[0] 为 x 方向移动的距离,out[1] 为 y 方向移动的距离。

@Override
    public int[] calculateDistanceToFinalSnap(
            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
       // 若是是水平方向滚动的,则计算水平方向须要移动的距离,不然水平方向的移动距离为0
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }
 // 若是是竖直方向滚动的,则计算竖直方向须要移动的距离,不然竖直方向的移动距离为0
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

   // 计算水平或者竖直方向须要移动的距离
    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;
    }复制代码

(2) findSnapView: 找到要对齐的View

// 找到要对齐的目标View, 最终的逻辑在findCenterView 方法里
// 规则是:循环LayoutManager的全部子元素,计算每一个 childView的
//中点距离Parent 的中点,找到距离最近的一个,就是须要居中对齐的目标View
 @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;
    }复制代码

(3) findTargetSnapPosition : 找到须要对齐的目标View的的Position

@Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
...
// 前面代码省略
        int vDeltaJump, hDeltaJump;
       // 若是是水平方向滚动的列表,估算出水平方向SnapHelper响应fling 
       //对齐要滑动的position和当前position的差,不然,水平方向滚动的差值为0.
        if (layoutManager.canScrollHorizontally()) {
            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
            if (vectorForEnd.x < 0) {
                hDeltaJump = -hDeltaJump;
            }
        } else {
            hDeltaJump = 0;
        }
      // 若是是竖直方向滚动的列表,估算出竖直方向SnapHelper响应fling 
       //对齐要滑动的position和当前position的差,不然,竖直方向滚动的差值为0.
        if (layoutManager.canScrollVertically()) {
            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getVerticalHelper(layoutManager), 0, velocityY);
            if (vectorForEnd.y < 0) {
                vDeltaJump = -vDeltaJump;
            }
        } else {
            vDeltaJump = 0;
        }

 // 最终要滑动的position 就是当前的Position 加上上面算出来的差值。

//后面代码省略
...
}复制代码

以上就分析了LinearSnapHelper 实现滑动的时候居中对齐和fling时居中对齐的源码。整个流程仍是比较简单清晰的,就是涉及到比较多的位置计算比较麻烦。熟悉了它的实现原理,从上面咱们知道,SnapHelper里面实现了对齐的流程,可是怎么对齐的规则就交给子类去处理了,好比LinearSnapHelper 实现了居中对齐,PagerSnapHelper 实现了居中对齐,而且限制只能一次滑动一页。那么咱们也能够继承它来实现咱们本身的SnapHelper,接下来看一下本身实现一个SnapHelper。

5 . 自定义 SnapHelper

上面分析了SnapHelper 的流程,那么这节咱们来自定义一个SnapHelper , LinearSnapHelper 实现了居中对齐,那么咱们来试着实现Target View 开始对齐。 固然了,咱们不用去继承SnapHelper,既然LinearSnapHelper 实现了居中对齐,那么咱们只要更改一下对齐的规则就行,更改成开始对齐(计算目标View到Parent start 要滑动的距离),其余的逻辑和LinearSnapHelper 是同样的。所以咱们选择继承LinearSnapHelper,具体代码以下:

/** * Created by zhouwei on 17/3/30. */

public class StartSnapHelper extends LinearSnapHelper {

    private OrientationHelper mHorizontalHelper, mVerticalHelper;

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

    private int distanceToStart(View targetView, OrientationHelper helper) {
        return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
    }

    @Nullable
    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager instanceof LinearLayoutManager) {

            if (layoutManager.canScrollHorizontally()) {
                return findStartView(layoutManager, getHorizontalHelper(layoutManager));
            } else {
                return findStartView(layoutManager, getVerticalHelper(layoutManager));
            }
        }

        return super.findSnapView(layoutManager);
    }



    private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        if (layoutManager instanceof LinearLayoutManager) {
            int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
            //须要判断是不是最后一个Item,若是是最后一个则不让对齐,以避免出现最后一个显示不彻底。
            boolean isLastItem = ((LinearLayoutManager) layoutManager)
                    .findLastCompletelyVisibleItemPosition()
                    == layoutManager.getItemCount() - 1;

            if (firstChild == RecyclerView.NO_POSITION || isLastItem) {
                return null;
            }

            View child = layoutManager.findViewByPosition(firstChild);

            if (helper.getDecoratedEnd(child) >= helper.getDecoratedMeasurement(child) / 2
                    && helper.getDecoratedEnd(child) > 0) {
                return child;
            } else {
                if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()
                        == layoutManager.getItemCount() - 1) {
                    return null;
                } else {
                    return layoutManager.findViewByPosition(firstChild + 1);
                }
            }
        }

        return super.findSnapView(layoutManager);
    }


    private OrientationHelper getHorizontalHelper( @NonNull RecyclerView.LayoutManager layoutManager) {
        if (mHorizontalHelper == null) {
            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
        }
        return mHorizontalHelper;
    }

    private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        if (mVerticalHelper == null) {
            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
        }
        return mVerticalHelper;

    }
}复制代码

使用的时候,更改成使用StartSnapHelper,代码以下:

StartSnapHelper snapHelper = new StartSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);复制代码

效果以下:

StartSnaphelper 效果.gif

以上就实现了一个Start对齐的效果,此外,在Github上发现一个实现了好几种Snap 效果的库,好比,start对齐、end对齐,top 对齐等等。有兴趣的能够去弄来玩一下,地址:[Snap 效果库]。(github.com/rubensousa/…)

6 . 总结

SnapHelper 是对RecyclerView 的一个扩展,能够很方便的实现相似ViewPager的效果,比ViewPager效果更好,当咱们要实现卡片式的浏览或者图库照片浏览时,使用RecyclerView + SnapHelper 的效果要比ViewPager的效果好不少。所以掌握SnapHelper 的使用技巧,能帮助咱们方便的实现一些滑动交互效果,以上就是对Snapuhelper的总结,若有问题,欢迎留言交流。本文Demo已上传GithubAndroidTrainingSimples

参考:
Using SnapHelper in RecyclerView

相关文章
相关标签/搜索