概述
SnapHelper可以看做是RecyclerView惯性滑动的一个辅助类,可以帮我们做一些惯性滑动时和滑动后的一些处理,所以对于一些惯性滑动的操作处理就可以优先考虑使用这个类,可以处理的点可以归纳为以下三点:
- 可以监听到滑动时手指抬起的那一刻;
- 指定手指抬起后RecyclerView惯性滑动的item个数;
- 滑动结束后指定item在界面所显示的位置;
SnapHelper用到的关键类说明
Google为我们提供了两个内部实现了SnapHelper的类,分别是LinearSnapHelper和PagerSnapHelper,作用分别是:
- LinearSnapHelper会将当前最靠近中间位置的item居中显示;
- PagerSnapHelper可以让RecyclerView实现像ViewPager一样的功能,只不过有一个前提条件,RecyclerView的item布局必须在滑动方向上使用MATCH_PARENT布局的;
为了更好的理解SnapHelper,这里有必要先了解下RecyclerView的两个内部类,分别是RecyclerView.OnFlingListener和RecyclerView.SmoothScroller:
- RecyclerView.OnFlingListener可以通过RecyclerView的setOnFlingListener()进行设置,设置完后,RecyclerView.OnFlingListener的onFling()方法会在滑动时(有惯性滑动)手指抬起的那一刻调用到,所以这时就可以对之后的惯性动作做一些设置;
- RecyclerView.SmoothScroller是滑动的工具类,比如对惯性滑动的速度,滑动到哪个位置等,将指定位置滑动到顶部还是底部,都是由它来处理,滑动的距离以及速度也是(onTargetFound()方法中去处理);
如果还不是很明白,可以自己写个小demo测试下这两个类,RecyclerView.OnFlingListener就不说了,设置下RecyclerView的回调就可以了,下面就来说下怎么单独测试下RecyclerView.SmoothScroller这个类,Google也为我们提供了它的一个子类LinearSmoothScroller,这里就说下对它的简单使用:
public void scrollToOffsetPostion(int position){
LinearSmoothScroller scroller = new LinearSmoothScroller(this);
scroller.setTargetPosition(position);
recyclerView.getLayoutManager().startSmoothScroll(scroller);
}
调用这个方法后会滑动到指定的position位置,如果position已经显示了,那么就不会再滑动了,如果position不再当前的显示界面,那么会将指定位置的item滑动到边缘对齐位置。
如果想想将指定的position滑动到置顶或置低,那么需要去重写LinearSmoothScroller,如下:
public class PagerGridSmoothScroller extends LinearSmoothScroller {
public PagerGridSmoothScroller(@NonNull RecyclerView recyclerView) {
super(recyclerView.getContext());
}
// SNAP_TO_START = -1;
// SNAP_TO_END = 1;
// SNAP_TO_ANY = 0;
@Override
protected int getHorizontalSnapPreference() {
return SNAP_TO_START;
}
@Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START; // 将子view与父view顶部对齐
}
}
根据想滑动到的位置去返回对应的值就可以了。
SnapHelper各方法的作用说明
1. attachToRecyclerView(@Nullable RecyclerView recyclerView): 将SnapHelper与绑定起来,从而实现辅助滚动的作用,如果想是解绑,传入null即可;
2.calculateScrollDistance(int velocityX, int velocityY): 根据传入的速度计算各自方向上滑动的距离并返回;
3.findSnapView(LayoutManager layoutManager): 这也是一个抽象方法,主要作用是找到目标位置的那个view,可用于后面计算这个view到目标位置的距离;
4.calculateDistanceToFinalSnap(LayoutManager layoutManager, @NonNull View targetView): 这是一个抽象方法,这个方法的主要作用是计算targetView到目标位置的距离,这个距离用于后面的滑动,也就是将targetView滑动到指定位置;
5.findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY): 这是一个抽象方法,主要作用是根据传入的速度计算最终哪个位置的item需要滑动到目标位置,也可以根据自己的逻辑计算最后惯性滑动停留的位置;
说完这些方法的作用后,接下来就来看看SnapHelper整体逻辑的处理,出发点就是attachToRecyclerView()这个方法了:
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
// 如果已经设置了那就不再设置了,直接返回
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
// 如果传进来的RecyclerView为null,那么就会与之前的RecyclerView进行解绑
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
// 下面这个方法就是对RecyclerView进行绑定
setupCallbacks();
// 根据速度计算滑动距离的时候会用到
mGravityScroller = new Scroller(mRecyclerView.getContext(),new DecelerateInterpolator());
// 将最靠近目标位置的item滚动到目标位置,这里有一点需要主要,如果在界面绘制之前就已经调用了这个方法,那么是不会将靠近目标位置
// 的item滚动到目标位置的
snapToTargetExistingView();
}
}
private void destroyCallbacks() {
// 与RecyclerView解绑就是移除之前设置过的回调
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
private void setupCallbacks() throws IllegalStateException {
// 注意这里,有时候再使用的时候可能会遇到这个异常
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
// 这里就是对RecyclerView回调的绑定了
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
// findSnapView()方法的作用上面已经说了,找到距离目标位置最近的View
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
// 计算到目标位置的距离
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
// 将对应的view滑动到目标位置
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
绑定的整体逻辑上面代码中都有注释,代码不多,也比较好理解,滑动时的逻辑也和这个类似,只是多了一些其他的细节,那就接着往下看,绑定回调的时候有调用mRecyclerView.setOnFlingListener(this),那就是说滑动时手指抬起时会调用到它的onFling()方法:
@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);
}
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return false;
}
SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
// 根据滑动速度计算最终需要滑动到哪个位置
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
// 滑动的工具类,设置最终滑动到哪个位置,注意这里的滑动是会与RecyclerView的边界对齐,所以在停止滑动时还需要对其进行处理,后面会说到
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
在惯性滑动开始的那一刻调用了onFling(),这里会对需要滑动到的位置进行处理,如果你对滑动时滑动的item个数有要求的话,那么就可以在findTargetSnapPosition()方法中的返回值进行限制,比如返回的最大值不超过四,那么这里滑动的item个数就不会超过四个,惯性滑动的处理就到这里了,接下来要说的就是滑动快结束时的处理,前面有说到,调用SmoothScroller的setTargetPosition()方法只会将指定位置的item滑动到RecyclerView边界对齐,要想将指定位置的item滑动到目标位置,这里还是需要借助SmoothScroller这个类, 这里先来看下createScroller(layoutManager)这个方法:
@Nullable
protected SmoothScroller createScroller(LayoutManager layoutManager) {
return createSnapScroller(layoutManager);
}
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return null;
}
// 这里需要注意onTargetFound()这个方法的调用时机,它是在RecyclerView滑动到指定位置的item时,并且指定位置的item(即下面
//onTargetFound()方法中targetView)在被绘制出来前会被调用到
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
// 计算targetView到目标位置的距离
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);
}
}
// 这个方法会影响到上面calculateTimeForDeceleration()方法的返回值,而这个返回值就会影响到最后滑动到目标位置的速度
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
可以看到,当指定item滑动到RecyclerView边界时,这时就会调用到onTargetFound()方法,这时在计算这个view到目标位置的距离并滚动到目标位置。
还记得上面绑定RecyclerView时候还调用了mRecyclerView.addOnScrollListener(mScrollListener),现在就来看看mScrollListener这个对象:
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()方法,这个方法在绑定RecyclerView的时候就有调用到,这里就不在分析了,到此这个流程就梳理了一遍。如果有对SnapHelper几个抽象方法实现感兴趣的,可以去看看LinearSnapHelper,如果看着比较吃力,可以参考SnapHelper详解,这里面有对这些方法的详细解释。