关于SnapHelper
SnapHelper的功能: 在RecyclerView滑动结束时,定位到某个指定的位置。比如说,RecyclerView水平滑动,滑动结束后,item的中心与RecyclerView的水平中心对齐。
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。
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中的抽象方法,需要子类实现。 首先我们需要明确一下几点:
该方法什么时候触发:手指离开RecyclerView的瞬间,即Fling刚开始的瞬间,执行该方法。
该方法有什么用:根据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这个方法什么时候调用呢?
SnapHelper#attachToRecyclerView的时候,也就是初始化的时候,将初始状态按要求对齐
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。
总结
初始化的时候(也就是SnapHelper#attachToRecyclerView),设置RecyclerView#addOnScrollListener 和RecyclerView.setOnFlingListener
在RecyclerView#addOnScrollListener中,如果RecyclerView有过滑动,则重新对齐
在RecyclerView.setOnFlingListener中处理RecyclerView的fling逻辑(替代RecyclerView原有的fling逻辑)
对齐的方法是SnapHelper#snapToTargetExistingView, 该方法就是找到当前(初始化的时候,或者是滑动结束的时候)距离对齐位置最近的item view,然后计算该item view距离对齐位置的距离,最后调用RecyclerView#smoothScrollBy方法实现对齐
最后更新时间:2018-03-02 11:30:32