1、简介
SnapHelper是RecyclerView的辅助类,可用来控制在滑动结束后,RecyclerView中item的对齐方式。
SnapHelper继承自RecyclerView.OnFlingListener
,并实现了onFling
,支持SnapHelper的RecyclerView.LayoutManager
必须实现了RecyclerView.SmoothScroller.ScrollVectorProvider
接口,或者自己实现onFling(int,int)
方法手动处理。SnapHeper 有以下几个重要方法:
- attachToRecyclerView:将SnapHelper attach 到指定的RecyclerView 上。
- calculateDistanceToFinalSnap:复写这个方法指定滑动到TargetView或容器指定点的距离。这是一个抽象方法,由子类实现,返回一个长度为2的int数组out,out[0]表示x方向上对齐要移动的距离,out[1]表示y方向上对齐要移动的距离。抽象方法,需要子类实现。
- calculateScrollDistance:
用于Fling操作
,根据每个方向上指定的速度计算出滑动的距离。 - findSnapView:提供一个指定的目标View来对齐。抽象方法,需要子类实现。
- findTargetSnapPosition:提供一个用于对齐的Adapter 目标position,抽象方法,需要子类自己实现。
- onFling:根据给定的x和 y 轴上的速度处理Fling。
2、 LinearSnapHelper & PagerSnapHelper
SnapHelper是一个抽象类,系统内置了两个默认实现类:LinearSnapHelper & PagerSnapHelper。
- LinearSnapHelper:使RecyclerView的item居中显示。
- PagerSnapHelper:使RecyclerView像ViewPager一样,每次只能滑动一页,PagerSnapHelper也是Item居中对齐。
接下来看下使用方法和效果。
2.1、LinearSnapHelper
LinearSnapHelper 使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动(滑动多页)。代码如下:
LinearLayoutManager manager = new LinearLayoutManager(getContext());
manager.setOrientation(LinearLayoutManager.HORIZONTAL);
mRecyclerView.setLayoutManager(manager);
// 将SnapHelper attach 到RecyclrView
LinearSnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);
代码很简单,new 一个SnapHelper对象,然后 Attach到RecyclerView 即可。
效果如下:
如上图所示,简单几行代码就可以用RecyclerView 实现一个类似ViewPager的效果,并且效果更赞。可以快速滑动多页,当前页居中显示,并且显示前一页和后一页的部分。除了上面的效果外,如果你想要和ViewPager 一样,限制一次只让它滑动一页,那么你就可以使用PagerSnapHelper了,接下来看一下PagerSnapHelper的使用效果。
2.2、PagerSnapHelper
PagerSnapHelper的展示效果和LineSnapHelper是一样的,只是PagerSnapHelper 限制一次只能滑动一页,不能快速滑动。代码如下:
PagerSnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);
PagerSnapHelper效果如下:
3、SnapHelper源码分析
上面介绍了SnapHelper的使用,那么接下来我们来看一下SnapHelper到底是怎么实现的。
3.1、snapToTargetExistingView
这个方法用于
- 调用
attachToRecyclerView
绑定到RecyclerView时来完成对齐TargetView。 - 当
Scroll
被触发时和Fling操作的末尾阶段
时对齐TargetView。
在attachToRecyclerView
和onScrollStateChanged
中都调用了这个方法。
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();
}
}
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();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
snapToTargetExistingView
/**
*
* 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.2、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 接口没有,
//如果没有实现 ,则直接返回。此时需要手动去实现onFling方法进行处理。
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的对齐,只是有几个抽象方法是没有实现的,具体的对齐规则交给子类去实现。
4、LinearSnapHelper如何实现居中对齐的
接下来看一下LinearSnapHelper 是怎么实现居中对齐的:主要是实现了上面提到的三个抽象方法,findTargetSnapPosition、calculateDistanceToFinalSnap和findSnapView
。
4.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;
}
4.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;
}
4.3、findTargetSnapPosition
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);