Hiten's Blog.

SnapHelper硬核讲解

字数统计: 4.8k阅读时长: 20 min
2019/04/24 Share

前言

这都9012年了,SnapHelper不是新鲜玩意,为啥我要拿出来解析?首先,Google已经放出 Viewpager2 测试版本,该方案计划用RecyclerView替换掉ViewPager;其次,我发现身边很多Android同学SnapHelper了解并不深;所以,弄懂并熟练使用SnapHelper是必要的;我借着阅读androidxViewpager2源码的机会,跟大家仔细梳理一下SnapHelper的原理;

SnapHelper认识

我忽然觉得有必要科普一下SnapHelper的基本情况,首先SnapHelper是附加于RecyclerView上面的一个辅助功能,它能让RecyclerView实现类似ViewPager等功能;如果没有SnapHelperRecyclerView也能很好的使用;但一个普通的RecyclerView在滚动方面和ListView没有特殊的区别,都是给人一种直来直往的感觉,比如我想实现横向滚动左边的子View始终左对齐,或者我用力一滑,惯性滚动最大距离不能超过一屏,这些看似不属于RecyclerView的功能,有了SnapHelper就很好的解决;所以SnapHelper有它存在的价值,它不是RecyclerView核心功能的参与者,但有它就能锦上添花;

RecyclerView滚动基础

在正式介绍SnapHelper之前,先了解一下滚动相关的基础知识点,我把RecyclerView的滚动分为滚动状态Fling这两类,主要应对的是OnScrollListenerOnFlingListener这两个回调接口;

滚动状态监听

RecyclerVier一共有三种描述滚动的状态:SCROLL_STATE_IDLESCROLL_STATE_DRAGGINGSCROLL_STATE_SETTLING,稍微注释一下:

  • SCROLL_STATE_IDLE
    • 滚动闲置状态,此时并没有手指滑动或者动画执行
  • SCROLL_STATE_DRAGGING
    • 滚动拖拽状态,由于用户触摸屏幕产生
  • SCROLL_STATE_SETTLING
    • 自动滚动状态,此时没有手指触摸,一般是由动画执行滚动到最终位置,包括smoothScrollTo等方法的调用

我们想监听状态的改变,调用addOnScrollListener方法,重写OnScrollListener的回调方法即可,注意OnScrollListener提供的回调数据并不如ViewPager那样详细,甚至是一种缺陷,这在ViewPager2ScrollEventAdapter类有详细的适配方法,有兴趣的可以看看。

addOnScrollListener方法是接下来分析SnapHelper的重点之一;

fling行为监听

承接上文,自然滚动行为底层的要点是处理fling行为,flingAndroid View中惯性滚动的代言词,分析代码如下:

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
public boolean fling(int velocityX, int velocityY) {
if (mLayout == null) {
Log.e(TAG, "Cannot fling without a LayoutManager set. " +
"Call setLayoutManager with a non-null argument.");
return false;
}
if (mLayoutFrozen) {
return false;
}
final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
final boolean canScrollVertical = mLayout.canScrollVertically();
if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
velocityX = 0;
}
if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
velocityY = 0;
}
if (velocityX == 0 && velocityY == 0) {
// If we don't have any velocity, return false
return false;
}
//处理嵌套滚动PreFling
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
//处理嵌套滚动Fling
dispatchNestedFling(velocityX, velocityY, canScroll);
//优先判断mOnFlingListener的逻辑
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));
//默认的Fling操作
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}

RecyclerViewfling行为流程图如下:

其中mOnFlingListener是通过setOnFlingListener方法设置,这个方法也是接下来分析SnapHelper的重点之一;

SnapHelper小觑

SnapHelper顾名思义是Snap+Helper的组合,Snap有移到某位置的含义,Helper译为辅助者,综合场景解释是将RecyclerView移动到某位置的辅助类,这句话看似简单明了,却蕴藏疑问,有两个疑问点需要我们弄明白:

何时何地触发RecyclerView移动?又要把RecyclerView移到哪个位置?

带着这两个疑问,我们从SnapHelper的使用和入口方法看起:

attachToRecyclerView入口

PagerSnapHelper为例,SnapHelper的基本使用:

1
new PagerSnapHelper().attachToRecyclerView(mRecyclerView);

PagerSnapHelperSnapHelper的子类,,SnapHelper的使用很简单,只需要调用attachToRecyclerView绑定到置顶RecyclerView即可;

SnapHelper

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
public abstract class SnapHelper extends RecyclerView.OnFlingListener 
//绑定RecyclerView
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();//移动到制定View
}
}
//设置回调关系
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}

//注销回调关系
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}

}

SnapHelper是一个抽象类,实现了RecyclerView.OnFlingListener接口,入口方法attachToRecyclerViewSnapHelper中定义,该方法主要起到清理、绑定回调关系和初始化位置的作用,在setupCallbacks中设置了addOnScrollListenersetOnFlingListener两种回调;

上文说过RecyclerView的滚动状态和fling行为的监听,在这里看到SnapHelper对于这两种行为都需要监听,attachToRecyclerView的主要逻辑就是干这个事的,至于如何处理回调之后的事情,且继续往下看;

SnapHelper处理回调流程

SnapHelperattachToRecyclerView方法中注册了滚动状态和fling的监听,当监听触发时,如何处理后续的流程,我们先分析滚动状态的回调:

滚动状态回调处理

滚动状态的回调接口实例是mScrollListener

SnapHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;

@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//静止状态且滚动过一段距离,触发snapToTargetExistingView();
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
//移动到指定的已存在的View
snapToTargetExistingView();
}
}

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};

逻辑处理的入口在onScrollStateChanged方法中,当newState == RecyclerView.SCROLL_STATE_IDLE且滚动距离不等于0,触发snapToTargetExistingView方法;

SnapHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//移动到指定的已存在的View
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
//查找SnapView
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
//计算SnapView的距离
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
//调用smoothScrollBy移动到制定位置
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}

snapToTargetExistingView方法顾名思义是移动到指定已存在的View的位置,findSnapView是查到目标的SnapViewcalculateDistanceToFinalSnap是计算SnapView到最终位置的距离;由于findSnapViewcalculateDistanceToFinalSnap是抽象方法,所以需要子类的具体实现;
整理一下滚动状态回调下,SnapHelper的实现流程图如下;

Fling结果回调处理

上文分析SnapHelper实现了RecyclerView.OnFlingListener接口,因此Fling的结果在onFling()方法中实现:

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
@Override
public boolean onFling(int velocityX, int velocityY) {
RecyclerView.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);
}
//处理snap的fling逻辑
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
//判断layoutManager要实现ScrollVectorProvider
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
//创建SmoothScroller
RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
//获得snap position
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
//设置position
smoothScroller.setTargetPosition(targetPosition);
//启动SmoothScroll
layoutManager.startSmoothScroll(smoothScroller);
//返回true拦截掉后续的fling操作
return true;
}

//创建Scroller
protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
if (mRecyclerView == null) {
// The associated RecyclerView has been removed so there is no action to take.
return;
}
//计算Snap到目标位置的距离
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
//计算时间
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, mDecelerateInterpolator);
}
}
//计算速度
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}

fling流程分析

  • fling的逻辑主要在snapFromFling方法中,完成fling逻辑首先要求layoutManagerScrollVectorProvider的实现,为什么要求实现ScrollVectorProvider?,因为SnapHelper需要知道布局的方向,而ScrollVectorProvider正是该功能的提供者;

  • 其次是创建SmoothScroller,主要逻辑是createSnapScroller方法,该方法有默认的实现,主要逻辑是创建一个LinearSmoothScroller,在onTargetFound中调用calculateDistanceToFinalSnap计算距离,然后通过calculateTimeForDeceleration计算动画时间;

  • 然后通过findTargetSnapPosition方法获取目标targetPosition,最后把targetPosition赋值给smoothScroller,通过layoutManager执行该scroller;
  • 最重要的是snapFromFling要返回true,前文分析过RecyclerView的fling流程,返回true的话,默认的ViewFlinger就不会执行。

fling逻辑流程图如下

段落小结

SnapHelper对于滚动状态和Fling行为的处理上面已经梳理完毕,我特意画了两个草图,希望让大家有更清晰的认识,如果还不清晰至少得知道怎么用吧,例如我们要自定义SnapHelper,必须要重写的三个方法是:

  • findSnapView(RecyclerView.LayoutManager layoutManager)
    • 在滚动状态回调时调用,目的是查找SnapView,注意返回的SnapView必须是LayoutManager已经加载出来的View;
  • calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView)
    • 计算sanpView到指定位置的距离,这是在滚动状态回调和Fling的计算时间工程中使用;
  • findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY)
    • 查找指定的SnapPosition,这个方法只有在Fling的时候调用;

记住这三个方法,如果想玩转SnapHelper,掌握这个三分方法是迈出的第一步;

SnapHelper到底怎么玩

往往知道方法怎么用,却不知道代码怎么写,这是最困惑的,我们以LinearSnapHelper为例,从细节出发,分析自定义SnapHelper的常用思路和关键方法;

动代码前,先弄清这俩哥们到底解决了啥问题,首先LinearSnapHelper能够让线性排列的列表元素,最中间那颗元素居中显示;下图是LinearSnapHelper的效果展示之一;

findSnapView怎么玩

前面交待过,findSnapView方法是查找SnapView的,何为SnapView,在LinearSnapHelper的应用场景中,屏幕(RecyclerView)中间的View就是SnapView,且看findSnapView方法的实现:

LinearSnapHelper

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

@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}

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

首先,findSnapView中需要判断RecyclerView滚动的方向,然后拿到对应的OrientationHelper,最后通过findCenterView查找到SnapView并返回;

LinearSnapHelper

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
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int center;//中间位置
//判断ClipToPadding逻辑
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++) {
final View child = layoutManager.getChildAt(i);
//child的中间位置
int childCenter = helper.getDecoratedStart(child) +
(helper.getDecoratedMeasurement(child) / 2);
//每个child距离中心位置的差值
int absDistance = Math.abs(childCenter - center);
//取距离最小的那个
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}

findCenterView()方法是获取屏幕(RecyclerView控件)中间位置最近的那个View当做SnapView,计算的过程稍显复杂其实比较了然,具体注释在代码中标注,容易产生疑惑的是OrientationHelper下面一堆获取位置的方法,这里稍微总结一下:

OrientationHelper常见方法

  • getStartAfterPadding() 获取RecyclerView起始位置,如果padding不为0,则算上padding;
  • getTotalSpace() 获取RecyclerView可使用控件,本质上是RecyclerView的尺寸减轻两边的padding;
  • getDecoratedStart(View) 获取View的起始位置,如果RecyclerView有padding,则算上padding;
  • getDecoratedMeasurement(View) 获取View宽度,如果该view有maring,也会算上;

总的来说findCenterView并不复杂,最迷惑人的是OrientationHelper的一堆API,在使用时稍加注意,也不是很复杂的;

calculateDistanceToFinalSnap怎么玩

首先,calculateDistanceToFinalSnap接受上一步获取的SnapView,需要返回一个int[],该数组约定长度为2,第0位表示水平方向的距离,第1位表示竖直方向的距离,且看LinearSnapHelper怎么玩;

LinearSnapHelper

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
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;
}
//距离中间位置的距离
private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//targetView的中心位置(距离RecyclerView start为准)
final int childCenter = helper.getDecoratedStart(targetView) +
(helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter; //RecyclerView的中心位置
if (layoutManager.getClipToPadding()) {
containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
containerCenter = helper.getEnd() / 2;
}
return childCenter - containerCenter;//差距
}

很幸运,calculateDistanceToFinalSnap并没有很复杂的代码,主要是计算方向,然后通过OrientationHelper计算第一步findSnapView得到的SnapView距离中间位置的距离;代码和第一步很相似,注释在代码中;

findTargetSnapPosition怎么玩

前面说过,findTargetSnapPosition是处理Fling流程中,计算SnapPosition的关键方法,首先,findTargetSnapPosition接受速度参数velocityXvelocityY,需要返回int类型的position,这个位置对应的是Adapter中的position,并不是LayoutManagerRecyclerView中子View的index

LinearSnapHelper

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
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
//判断是否实现ScrollVectorProvider
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}
//获取Adapter中item个数
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
//查找中间SnapView
final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}
//计算当前View在adapter中的position
final int currentPosition = layoutManager.getPosition(currentView);
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
//获取布局方向提供者
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
//从当前位置往最后一个元素计算
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd == null) {
return RecyclerView.NO_POSITION;
}

int vDeltaJump, hDeltaJump;//计算惯性能滚动多少个子View
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;
}
//计算目标position
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {//边界判断
targetPos = 0;
}
if (targetPos >= itemCount) {//边界判断
targetPos = itemCount - 1;
}
return targetPos;
}

计算通过惯性能滚动多少个子View的代码:

LinearSnapHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper, int velocityX, int velocityY) {
//惯性能滚动多少距离
int[] distances = calculateScrollDistance(velocityX, velocityY);
//单个child平均占用多少宽/高像素
float distancePerChild = computeDistancePerChild(layoutManager, helper);
if (distancePerChild <= 0) {
return 0;
}
//得到最终的水平/竖直的距离
int distance =
Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
if (distance > 0) {四舍五入得到平均个数
return (int) Math.floor(distance / distancePerChild);
} else {//负数的除法特殊处理得到平均个数
return (int) Math.ceil(distance / distancePerChild);
}
}

计算每个child的平均占用多少宽/高的代码如下:

LinearSnapHelper

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
private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
View minPosView = null;
View maxPosView = null;
int minPos = Integer.MAX_VALUE;
int maxPos = Integer.MIN_VALUE;
int childCount = layoutManager.getChildCount();//获取已经加载的View个数,不是所有adapter中的count
if (childCount == 0) {
return INVALID_DISTANCE;
}
//计算已加载View中,最start和最end的View和Position
for (int i = 0; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
final int pos = layoutManager.getPosition(child);
if (pos == RecyclerView.NO_POSITION) {
continue;
}
if (pos < minPos) {
minPos = pos;
minPosView = child;
}
if (pos > maxPos) {
maxPos = pos;
maxPosView = child;
}
}
if (minPosView == null || maxPosView == null) {
return INVALID_DISTANCE;
}
//分别获取最start和最end位置,距RecyclerView起点的距离;
int start = Math.min(helper.getDecoratedStart(minPosView),
helper.getDecoratedStart(maxPosView));
int end = Math.max(helper.getDecoratedEnd(minPosView),
helper.getDecoratedEnd(maxPosView));
//得到距离的绝对差值
int distance = end - start;
if (distance == 0) {
return INVALID_DISTANCE;
}
//计算平均宽/高
return 1f * distance / ((maxPos - minPos) + 1);
}

LinearSnapHelperfindTargetSnapPosition方法着实不简单,但是条理清晰逻辑严谨,考虑的比较周全,上面代码我做了比较详细的注释,相信肯定有同学不爱看代码,我也是,所以我用文字重新梳理一下上述代码逻辑和关键点;

  • findTargetSnapPosition方法逻辑流程总结:

    • 首先通过findSnapView()活动当前的centerView;
    • 通过ScrollVectorProvider是否是reverseLayout,布局方向;
    • 通过estimateNextPositionDiffForFling方法获取该惯性能产生多少个子child的平移,或者理解成该惯性能让RecyclerView滚动多远个子child的距离;
    • 通过当前的centerView下标,加上惯性产生的平移,计算出最终要落地的下标;
    • 边界判断
  • estimateNextPositionDiffForFling方法逻辑流程总结:

    • 通过calculateScrollDistance计算惯性能滚动多远距离;
    • 通过computeDistancePerChild计算平均一个child占多大尺寸;
    • 距离除以尺寸,四舍五入得到个数并返回;
  • computeDistancePerChild方法逻辑流程总结:
    • 获取layoutManager已经加载的所有子View;
    • 获取最start和最end的view和下标;
    • 分别计算最start和最end的View的start和end值;
    • 计算平均值并返回;

终于是把LinearSnapHelper的核心逻辑讲完了,纵观整个类,主要逻辑还是在findTargetSnapPosition这里,趁热打铁,我必须跟大家分享一下PagerSnapHelper是如何玩转这个方法的;

PagerSnapHelper似乎更简单

pagerSnapHelper同样也实现了SnapHelper的三个方法,下面先看findTargetSnapPosition:

PagerSnapHelper

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
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
final int itemCount = layoutManager.getItemCount();//获取adapter中所有的itemcount
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}

View mStartMostChildView = null;//获取最start的View
if (layoutManager.canScrollVertically()) {
mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
}

if (mStartMostChildView == null) {
return RecyclerView.NO_POSITION;
}
//最start的View当前centerposition
final int centerPosition = layoutManager.getPosition(mStartMostChildView);
if (centerPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}

final boolean forwardDirection;//速度判定
if (layoutManager.canScrollHorizontally()) {
forwardDirection = velocityX > 0;
} else {
forwardDirection = velocityY > 0;
}
boolean reverseLayout = false;//是否是reverseLayout,布局方向
if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd != null) {
reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
}
}
return reverseLayout
? (forwardDirection ? centerPosition - 1 : centerPosition)下标要买+1 or -1,要么保持不变
: (forwardDirection ? centerPosition + 1 : centerPosition);
}

众所周知,ViewPager的翻页要么是保持不变,要么是下一页/上一页,上面findTargetSnapPosition方法就是主要的实现逻辑,其中判定是否翻页的条件由forwardDirection来控制,直接对比速度>0,用户想轻松滑到下一页是比较easy的,以至于上面代码量少到不敢相信;

至于findSnapViewdistanceToCenter方法,同样是获取屏幕(RecyclerView)中间的View,计算distanceToCenter,跟LinearSnapHelper如出一辙;

PagerSnapHelper注意事项

PagerSnapHelper设计之初是就是适用于一屏(RecyclerView范围内)显示单个child的,如果有一屏显示多个child的需求,PagerSnapHelper并不适用;其实在实际开发中这种需求还是挺多的,当然github上早已经有大神写过一个库,实现了几个常用的SnapHelper场景,github传送门;当然这个库并不能满足所有的需求,有机会再跟大家分享更有意义的SnapHelper实战;

结尾:明明是玩了一场接力赛

什么玩意,接力赛?没有错。SnapHelper在运行过程中,RecyclerView的状态可能会经历这样DRAGGING->SETTLING->IDLE->SETTLING->IDLE甚至更多状态,我称之为接力赛,为什么会这个样子?拿LinearSnapHelper来说,前期手势拖拽,肯定是玩DRAGGING状态,一旦撒手加之惯性,会进入SETTLING状态,然后fling()方法会计算snapPosition并指示SmoothScrooler滚动到snapPosition位置,滚动完毕会进入IDLE状态,注意SmoothScrooler滚动结束的位置相对于RecyclerView的start位置的,而LinearSnapHelper要求中间对齐,此时必然会触发snapToTargetExistingView()方法,做最后的调整,所谓最后的调整是通过snapToTargetExistingView调用smoothScrollBy,而结束条件通常是calculateDistanceToFinalSnap()返回[0,0],这就是我所说的接力赛;

陷阱: 一旦calculateDistanceToFinalSnap()返回值计算错误,有可能造成RecyclerView进入smoothScroolBy的魔鬼循环局面,直到滚动到头/尾才会结束;

CATALOG
  1. 1. 前言
    1. 1.1. SnapHelper认识
  2. 2. RecyclerView滚动基础
    1. 2.1. 滚动状态监听
    2. 2.2. fling行为监听
  3. 3. SnapHelper小觑
    1. 3.1. attachToRecyclerView入口
    2. 3.2. SnapHelper处理回调流程
      1. 3.2.1. 滚动状态回调处理
      2. 3.2.2. Fling结果回调处理
    3. 3.3. 段落小结
  4. 4. SnapHelper到底怎么玩
    1. 4.1. findSnapView怎么玩
    2. 4.2. calculateDistanceToFinalSnap怎么玩
    3. 4.3. findTargetSnapPosition怎么玩
    4. 4.4. PagerSnapHelper似乎更简单
    5. 4.5. PagerSnapHelper注意事项
  5. 5. 结尾:明明是玩了一场接力赛