关于SnapHelper

  1. SnapHelper的功能: 在RecyclerView滑动结束时,定位到某个指定的位置。比如说,RecyclerView水平滑动,滑动结束后,item的中心与RecyclerView的水平中心对齐。
  2. android.support.v7.widget.SnapHelper有子类android.support.v7.widget.LinearSnapHelper以及android.support.v7.widget.PagerSnapHelper。LinearSnapHelper使用场景:滑动结束后,item与RecyclerView的中心对齐,每次可以滑动多个item;PagerSnapHelper:通过RecyclerView实现类似ViewPager的效果,每次只能滑动一个item。

简单使用

1
2
3
4
5
LinearSnapHelper linearSnapHelper = new LinearSnapHelper();
linearSnapHelper.attachToRecyclerView(mRecyclerView);

PagerSnapHelper pagerSnapHelper = new PagerSnapHelper();
pagerSnapHelper.attachToRecyclerView(mRecyclerView);

源码分析

从SnapHelper#attachToRecyclerView开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
//判断是否已经关联过相同的recyclerView,关联过,则不再处理
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
//如果之前已经设置过别的recyclerView,那么,清空之前的设置
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
//添加监听设置
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
//初始化对齐, 比如中间对齐,那么,attachToRecyclerView后,RecyclerView中靠近中心的item并不一定就在RecyclerView的中心,那么,就需要调用该方法让该item对齐
snapToTargetExistingView();
}
}

方法中有用到SnapHelper#destroyCallbacks,SnapHelper#setupCallbacks,以及SnapHelper#snapToTargetExistingView.

SnapHelper#destroyCallbacks

1
2
3
4
5

private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}

destroyCallbacks很简单,就是清空之前设置的OnScrollListener和OnFlingListener。

SnapHelper#setupCallbacks

1
2
3
4
5
6
7
8
9

private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}

setupCallbacks用来设置OnScrollListener和OnFlingListener。OnScrollListener的话用来监听RecyclerView的滑动状态变化。OnFlingListener的话,用来监听RecyclerView的Fling状态(指的是手指以一定的速度离开控件,控件会继续滑动,直到停止的状态)。

我们先来看一下mScrollListener。

SnapHelper#mScrollListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

mScrolled初始为false,一旦RecyclerView有滑动,触发了onScrolled,也就是dx和dy有一个不等于0,那么mScrolled就会被置为true。再看看onScrollStateChanged,当RecyclerView的滑动状态有发生变化的时候,会触发该方法。如果滑动状态发生了变化,并且新的状态的停止(SCROLL_STATE_IDLE),RecyclerView曾经发生过滑动(mScrolled为true),那么就会触发snapToTargetExistingView方法。此时,我们整理一下逻辑,如果RecyclerView有x或者y方向的滑动,那么滑动停止的时候,会触发snapToTargetExistingView方法,来对齐RecyclerView。

SnapHelper#onFling

SnapHelper实现了android.support.v7.widget.RecyclerView.OnFlingListener。首先我们知道,RecyclerView是有Fling的,但我们以一定的速度松开手指时,RecyclerView会继续滑动,那么,SnapHelper#setupCallbacks中的mRecyclerView.setOnFlingListener(this)又有什么用呢?

在RecyclerView中,我们可以看到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

public void setOnFlingListener(@Nullable OnFlingListener onFlingListener) {
mOnFlingListener = onFlingListener;
}

@Override
public boolean onTouchEvent(MotionEvent e) {
...省略代码

switch (action) {
...省略代码

case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;

...省略代码
}

...省略代码

return true;
}

public boolean fling(int velocityX, int velocityY) {
...省略代码

if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);

if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}

if (canScroll) {
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}

RecyclerView中的mOnFlingListener默认为空,如果我们没有给mOnFlingListener赋值,那么RecyclerView将会按照默认的逻辑,继续滑动,直到停止,但是如果设置了mOnFlingListener,并且OnFlingListener.onFling返回true的话,就不会再执行RecyclerView的默认fling逻辑。

现在我们来看看SnapHelper#onFling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

@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;
}
//获取RecyclerView最小能识别的速度
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
//如果x或者y方向的速度有一个大于这个最小速度,则执行snapFromFling方法,否则,返回false,执行RecyclerView的默认fling逻辑。
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}

OnFlingListener#onFling方法在手指离开RecyclerView的时刻触发,velocityX和velocityY分别是手指离开瞬间x方向和y方向的速度。

SnapHelper#snapFromFling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return false;
}

RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
//根据当前的velocityX和velocityY,找到RecyclerView应该滑动到哪个位置
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}

smoothScroller.setTargetPosition(targetPosition);
//开始滑动
layoutManager.startSmoothScroll(smoothScroller);
return true;
}

根据手指松开的瞬间的velocityX和velocityY,判断RecyclerView最终应该停在哪个位置,然后,调用layoutManager.startSmoothScroll来滑动到这个位置,返回true,表示RecyclerView最终会执行这个Fling逻辑,而不是RecycerView默认的Fling逻辑。

SnapHelper#findTargetSnapPosition

1
2
3

public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
int velocityY);

该方法是SnapHelper中的抽象方法,需要子类实现。
首先我们需要明确一下几点:

  1. 该方法什么时候触发:手指离开RecyclerView的瞬间,即Fling刚开始的瞬间,执行该方法。
  2. 该方法有什么用:根据velocityX和velocityY判断RecyclerView的fling状态结束的时候,RecyclerView应该停在哪个位置

SnapHelper#snapToTargetExistingView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
//找到当前最靠近对齐位置(比如说中间)的item view
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
//计算最靠近对齐位置的item view距离最终需要对齐的位置的距离(还有多少距离才能最终对齐)
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
//如果还没对齐,则调用方法对齐
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}

我们需要回顾一下,SnapHelper#snapToTargetExistingView这个方法什么时候调用呢?

  1. SnapHelper#attachToRecyclerView的时候,也就是初始化的时候,将初始状态按要求对齐
  2. RecyclerView出现过滑动后,滑动停止的时候调用(ScrollListener#onScrollStateChanged中)

SnapHelper#calculateDistanceToFinalSnap

1
2
3
4

@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
@NonNull View targetView);

抽象方法,需要子类去实现。该方法用来计算离对齐位置最近的item view距离最终的对齐的距离。

SnapHelper#findSnapView

1
2
3

@Nullable
public abstract View findSnapView(LayoutManager layoutManager);

抽象方法,需要子类实现。该方法用来获取当前距离对齐位置最近的item view。

下面,我们通过android.support.v7.widget.LinearSnapHelper来说明一下Snaphelper三个抽象方法的实现逻辑。
首先,我们需要知道,LinearSnapHelper的对齐位置是RecyclerView的中心。

LinearSnapHelper#calculateDistanceToFinalSnap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

@Override
public int[] calculateDistanceToFinalSnap(
@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[2];
//如果RecyclerVIew支持横向滑动,则计算需要移动的距离,不支持横向滑动,那么距离就是0
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(layoutManager, targetView,
getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}

//如果RecyclerVIew支持纵向滑动,则计算需要移动的距离,不支持纵向滑动,那么距离就是0
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(layoutManager, targetView,
getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}

该方法返回的结果是一个长度为2的int数组,数组的第一个元素是snapView(当前距离对齐位置最近的item view) 为对齐在x方向需要移动的距离,同理,数组的第二个元素是snapView(当前距离对齐位置最近的item view) 为对齐在y方向需要移动的距离。

LinearSnapHelper#distanceToCenter

1
2
3
4
5
6
7
8
9
10
11
12
13

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

通过辅助类来计算targetView(也就是上面的snapView,就是当前距离对齐位置最近的item view)距离中心位置的距离。

LinearSnapHelper#findSnapView

1
2
3
4
5
6
7
8
9
10

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

可以看到,findSnapView实际上是调用的LinearSnapHelper#findCenterView

LinearSnapHelper#findCenterView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

@Nullable
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}

View closestChild = null;
final int center;
//获取RecyclerView中心的位置
if (layoutManager.getClipToPadding()) {
center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
center = helper.getEnd() / 2;
}
int absClosest = Integer.MAX_VALUE;

//遍历RecyclerView所有的子View,找出距离RecyclerView中心最近的item view
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
//获取当前item view的中心点位置
int childCenter = helper.getDecoratedStart(child)
+ (helper.getDecoratedMeasurement(child) / 2);

//计算item view的中心与RecyclerView的中心的距离
int absDistance = Math.abs(childCenter - center);

/** if child center is closer than previous closest, set it as closest **/
//这就很简单了,如果当前的item view距RecyclerView的中心近,就记录该item view,那么,循环结束的时候,closestChild就是要找的那个item view了
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}

遍历RecyclerView所有的item view,计算每个item view的中心距RecyclerView中心的绝对距离,这个距离最小的一个,也就是我们要找的,距离对齐位置(LinearSnaphelper的对齐位置是RecyclerView的中心)最近的item view,也就是前面的snapView。

LinearSnapHelper#findTargetSnapPosition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

@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;
}
//找到当前(也就是fling刚开始的瞬间)距离对齐位置最近的item view,也就是距离RecyclerVIew中心最近的item view
final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}
//获取该currentView的在RecyclerVIew的位置
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;
}

//计算以当前的速度,Fling状态从开始到结束能滑动几个位置,也就算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;
}
//currentPosition是fling开始的时候距离RecyclerView对齐位置最近的item view的位置,deltaJump是此次fling从开始到结束,currentView滑动了几个item view宽度的距离,那么,targetPos必然就是fling结束的,距离RecyclerVIew对齐位置最近的item view的位置了
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {
targetPos = 0;
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1;
}
return targetPos;
}

首先,找到fling开始的时候,距离对齐位置最近的item view(代码里是currentView,它的位置是currentPosition),然后,根据速度计算此次fling过程中,滑过几个item(代码中是deltaJump),最后计算出fling结束的时候,距离它的对齐位置最近的item view的position。

总结

  1. 初始化的时候(也就是SnapHelper#attachToRecyclerView),设置RecyclerView#addOnScrollListener 和RecyclerView.setOnFlingListener
  2. 在RecyclerView#addOnScrollListener中,如果RecyclerView有过滑动,则重新对齐
  3. 在RecyclerView.setOnFlingListener中处理RecyclerView的fling逻辑(替代RecyclerView原有的fling逻辑)
  4. 对齐的方法是SnapHelper#snapToTargetExistingView, 该方法就是找到当前(初始化的时候,或者是滑动结束的时候)距离对齐位置最近的item view,然后计算该item view距离对齐位置的距离,最后调用RecyclerView#smoothScrollBy方法实现对齐