如果我们正常使用RecyclerView的话,那我们实现的效果就应该和上面的一样:当我们进行滑动的时候,RecyclerView中的Item停止的位置是取决于你滑动时的速度(Fling),可能会出现的情况是最终我们停止的位置可能位于两个item之间,当然也有可能正好位于某个item的中间。
如果这个时候我们的产品经理提出最终停止的位置一定要在某个Item上,不能出现位于两个之间的情况;或者产品经理说滑动的时候我们要一个一个滑过去,不能一下滑好多个…
那这个时候我们怎么办呢?
我们先来看看效果。
效果:滑动停止后Item的中心会被附加到RecyclerView的中心,说的直白点就是滑动停止后会显示一个完整的Item。
效果:滑动时一个一个滑动,类似于ViewPager滑动效果。
实现上面两种效果用到的类就是SnapHelper。
SnapHelper的作用是什么呢?
Class intended to support snapping for a RecyclerView.
SnapHelper tries to handle fling as well but for this to work
properly, the RecyclerView.LayoutManager must implement the
RecyclerView.SmoothScroller.ScrollVectorProvider interface or you
should override onFling(int, int) and handle fling manually.
SnapHelper的作用其实和重写onFling()的效果一样,只不过不是用监听的方式了,而是采用一个专门的类来处理。
什么是Snapping呢?谷歌翻译的意思有卡,断,骤,折的意思。感觉好像不搭边啊。我的理解是:Snap代表的是一种状态,这种状态是处于动与静之间的过渡状态。对于RecyclerView来说就是处理滑动后停止时的状态。比如我们实现的第一个效果,当RecyclerView处于静止的时候,SnapHelper处理的方式就是把停止后的Item的中心依附于RecyclerView的中心。第二种效果SnapHelper处理的方式就是滑动后把下一个Item的中心依附于RecyclerView。
SnapHelper是一个抽象类,继承自RecyclerView.OnFlingListener,OnFlingListener中只有一个抽象方法onFling(int velocityX, int velocityY)。onFling()方法主要用来处理fling效果。SnapHelper有两个直接子类:LinerSnapHelper , PagerSnapHelper。LinerSnapHelper 主要用来实现第一个效果的,PagerSnapHelper用来实现类似于ViewPager效果。它们俩的使用也非常简单。
LinearSnapHelper linearSnapHelper = new LinearSnapHelper();
linearSnapHelper.attachToRecyclerView(mRecyclerView);
PagerSnapHelper pagerSnapHelper = new PagerSnapHelper();
pagerSnapHelper.attachToRecyclerView(mRecyclerView);
是不是很简单,只需要两行代码就可以实现想要的效果。
我们以LinearSnapHelper为例来讲解SnapHelper实现的原理。
我们通过默认构造器生成LinearSnapHelper实例,然后调用attachToRecyclerView(),把RecyclerView实例传进去就ok了。
我们先看一下attachToRecyclerView()方法。
有三行代码需要注意:(一)setupCallbacks()设置回调;(二)mGravityScroller = new Scroller(mRecyclerView.getContext(), new DecelerateInterpolator());生成一个插值器为Decelerate的Scroller;(三)snapToTargetExistingView()给目标view设置snap效果;我们一个一个看。
第一行setupCallbacks()里面主要是给RecyclerView设置两个监听。
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
第二行生成一个Scroller,我们猜测RecyclerView的滑动就是基于Scroller实现的。
第三行是调用了一个方法:snapToTargetExistingView();其实这个方法在设置回调中的addOnScrollListener里面也是有调用的。
很显然snapToTargetExistingView()方法肯定就是我们要关注的重点了。那我们就来看看吧。
有几行代码需要注意:通过findSnapView()来获取snapView(),然后把snapView传入calculateDistanceToFinalSnap()方法;然后通过调用calculateDistanceToFinalSnap(layoutManager, snapView)来得到一个int[]数组。在得到snapDistance后如果snapDistance数组中只要有一个不为0那么就调用RecyclerView的smoothScrollBy()方法。
我们先看一下findSnapView()方法。这个方法是需要子类去复写的。LinearSnapHelper中的代码如下:
LinearSnapHelper通过判断layoutManager能否垂直(水平)滑动来分别创建垂直的OrientationHelper和水平的OrientationHelper,然后通过调用findCenterView来查找SnapView。
红色部分查找最接近RecyclerView中心的View的逻辑:取childView的中心值和RecyclerView的中心值得差值得绝对值和上次相比,如果比上次还小说明越接近RecyclerView的中心。这样就可以得到snapView了。
现在我们回到calculateDistanceToFinalSnap()方法,这个方法也是需要子类复写。
在这个方法主要调用了distanceToCenter()方法来计算。
distanceToCenter()需要传入三个参数:layoutManager , targetView 以及OrientationHelper。在前面的findSnapView()中的findCenterView()方法中也是需要传入OrientationHelper。那我们就先来看看OrientationHelper。
OrientationHelper是LayoutManager的一个帮助类,它根据View的方向来进行抽象测量。这个方法里面包含了很多抽象的测量方法。这个类里面也提供了生成水平方向和垂直方向的OrientationHelper方法。
总共有三个静态方法:第一个是根据传入的方向生成该方向的Helper。另外两个分别是水平和垂直Helper的生成方法。我们以createHorizontalHelper(),你会发现其内部就是调用layoutmanager方法的。具体每个方法返回的值是什么这里我们就不细究了。
我们通过调用OrientationHelper对应的方法来获取Distance,然后通过计算得到SnapView到中心的距离,最后我们调用mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);来实现对齐的效果。
简单的总结一下:前面我们讲解了SnapHelper是如何实现把找到最接近RecyclerView中心的View依附于RecyclerView的中心了。原理就是找到需要依附的View(findSnapView()),然后计算出RecyclerView的中心到SnapView之间的距离,然后调用RecyclerView的smoothScrollBy()方法进行scroll.
看到现在不知道你有没有一个疑惑:为什么LinearSnapHelper和PagerSnapHelper实现的效果不一样呢?具体是哪不一样呢?
其实经过上面的讲解不知道你有没有这样的想法:无论LinearSnapHelper还是PagerSnapHelper在找到需要移动的View后并把它移动中心的这段操作应该都是一样的原理。还有一点就是当我们在滑动的时候,手指离开屏幕,由于惯性RecyclerView会继续滑动,那最终停止时的position对于LinearSnapHelper,PagerSnapHelper肯定不一样。前者应该是RecyclerView停止时屏幕中显示的View的位置,而后者应该是滑动时的View的position加减1.
刚开始的时候我们讲过:SnapHelper是继承自OnFlingListener,而OnFlingListener中有个抽象方法onFling(),而且SnapHelper就是为了处理Fling效果的。那么SnapHelper的两个直接子类实现的效果不一样,那可以猜测它们在实现Fling的效果时是不一样的。
对于LinearSnapHelper,PagerSnapHelper需要重写另外一个方法findTargetSnapPosition();根据方法名我们就知道了这个方法的作用是得到target的position。
我们先看一下PagerSnapHelper,详细的代码就不贴出来了,感兴趣的可以自己查看。PagerSnapHelper重写findTargetSnapPosition()返回值是这样的。
return reverseLayout
? (forwardDirection ? centerPosition - 1 : centerPosition)
: (forwardDirection ? centerPosition + 1 : centerPosition);
和我们之前写的一样,但PagerSnapHelper做了更多的情况处理(reverseLayut是否为真)。先判断reverseLayout(设置布局方向为相反,如果为true,布局方向从end到start,默认为false,布局从start到end)是否为真,然后在判断forwardDirection(判断滑动方向是否是向前)是否为真,然后在进行对应的处理。
我们在看看LinearSnapHelper:
LinearSnapHelper最终返回的值是targetPos,而targetPos = currentPosition + deltaJump;
currentPosition 是滑动时view的position,是通过findSnapView来得到currentView ,然后调用layoutManager.getPosition(currentView)来得到currentPosition 。
deltaJump分为两种情况:vDeltaJump和hDeltaJump,分别对应垂直方向和水平方向。然后调用estimateNextPositionDiffForFling()来获取SnapHelper将要Fling到的位置。
我们来看一下estimateNextPositionDiffForFling():
这个方法的作用是估算出Fling后停止view的position。返回值是distance,distance的来源于Math.round(distance / distancePerChild),拿最终可能停止的位置的distance除以每一个child的距离,然后就得到需要Fling多少个position了。distance又是等于Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1],通过calculateScrollDistance()返回一个int类型数组,数组里面存放的是X轴方向需要移动的距离和Y轴方向移动的距离,然后根据它们的绝对值大小进行比较取较大的。
在calculateScrollDistance()方法中,通过调用scroller的getFinalX()和getFinalY()来获得水平方向和垂直方向最终Fling的距离。
distancePerChild是通过computeDistancePerChild()方法来获取的。
第一个红色方框的作用是找到minPosView和maxPosView,我在看这部分代码的时候其实是有个疑惑的:为什么不这样写呢?
官方的源码上在循环赋值之前是做了一个判断:
if (pos == RecyclerView.NO_POSITION) {
continue;
}
如果pos等于NO_POSITION则跳过,不进行赋值。
方框2中通过OrientationHelper中提供的方法获取到view的起始值(包括decoration和margin),然后distance除以个数就得到了每一个child的距离了。
现在我们终于搞明白了LinearSnapHelper和PagerSnapHelper的实现原理。
最后我们简单的总结一下:SnapHelper是用来处理Fling效果,它有两个直接子类:LinearSnapHelper和PagerSnapHelper,它们需要复写父类的三个方法:calculateDistanceToFinalSnap(),findSnapView(),findTargetSnapPosition()通过复写这三个方法我们可以得到不同的效果。
至此,SnapHelper相关的部分就已经讲完了,我们大概梳理了LinearSnapHelper和PagerSnapHelper的实现原理,具体的代码细节还要请各位读者自行查看。如果文章有需要更正的地方欢迎在评论区指出。