转载请注明出处:http://blog.csdn.net/crazy1235/article/details/53386286
SnapHelper 是 Android Support Library reversion 24.2.0 新增加的API。
SnapHelper 的应用
SnapHelper 是RecyclerView的一个辅助工具类。
它实现了RecyclerView.onFlingListener接口。而RecyclerView.onFlingListener 是一个用来响应用户手势滑动的接口。
SnapHelper是一个抽象类,官方提供了一个LinearSnapHelper子类,可以实现类似ViewPager的滚动效果,滑动结束之后让某个item停留在中间位置。
效果类似于Google Play主界面中item的滚动效果。
LinearSnapHelper的使用很简单,只需要调用 attachToRecyclerView(xxx) ,绑定上一个RecyclerView即可。
上一张自己的效果图:
LinearSnapHelper 源码分析
下面来分析一下 LinearSnapHelper
先从 attachToRecyclerView() 入手。
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(); } }
destoryCallback() 作用在于取消之前的RecyclerView的监听接口。
/** * Called when the instance of a {@link RecyclerView} is detached. */ private void destroyCallbacks() { mRecyclerView.removeOnScrollListener(mScrollListener); mRecyclerView.setOnFlingListener(null); }
setupCallbacks() – 设置监听器
/** * Called when an instance of a {@link RecyclerView} is attached. */ private void setupCallbacks() throws IllegalStateException { if (mRecyclerView.getOnFlingListener() != null) { throw new IllegalStateException("An instance of OnFlingListener already set."); } mRecyclerView.addOnScrollListener(mScrollListener); mRecyclerView.setOnFlingListener(this); }
此时可以看到,如果当前RecyclerView已经设置了OnFlingListener,会抛出一个 状态异常 。
snapToTargetExistingView()
/** * 找到居中显示的view,计算它的位置,调用smoothScrollBy使其居中 */ void snapToTargetExistingView() { if (mRecyclerView == null) { return; } LayoutManager layoutManager = mRecyclerView.getLayoutManager(); if (layoutManager == null) { return; } View snapView = findSnapView(layoutManager); if (snapView == null) { return; } // 计算目标View需要移动的距离 int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView); if (snapDistance[0] != 0 || snapDistance[1] != 0) { mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]); } }
该方法中显示调用 findSnapView() 找到目标View(需要居中显示的View),然后调用 calculateDistanceToFinalSnap() 来计算该目标View需要移动的距离。这两个方法均需要LinearSnapHelper重写。
SnapHelper.Java 中有三个抽象函数需要LinearSnapHelper 重写。
/** * 找到那个“snapView” */ public abstract View findSnapView(LayoutManager layoutManager);
/** * 计算targetView需要移动的距离 * 该方法返回一个二维数组,分别表示X轴、Y轴方向上需要修正的偏移量 */ public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView);
/** * 根据速度找到将要滑到的position */ public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY);
在setupCallbacks() 方法中可以看到对RecyclerView 设置了 OnScrollListener 和 OnFlingListener 两个监听器。
查看SnapHelper可以发现
// Handles the snap on scroll case. 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(); } } public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (dx != 0 || dy != 0) { mScrolled = true; } } }; @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); }
当滚动结束是,会调用 snapToTargetExistingView() 方法。
而当手指滑动触发onFling() 函数时,会根据X轴、Y轴方向上的速率加上 snapFromFling() 方法的返回值综合判断。
看一下 snapFromFling()
/** * Helper method to facilitate for snapping triggered by a fling. * * @param layoutManager The {@link LayoutManager} associated with the attached * {@link RecyclerView}. * @param velocityX Fling velocity on the horizontal axis. * @param velocityY Fling velocity on the vertical axis. * * @return true if it is handled, false otherwise. */ private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) { if (!(layoutManager instanceof ScrollVectorProvider)) { return false; } // 创建SmoothScroll对象 RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager); if (smoothScroller == null) { return false; } int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY); if (targetPosition == RecyclerView.NO_POSITION) { return false; } smoothScroller.setTargetPosition(targetPosition); layoutManager.startSmoothScroll(smoothScroller); return true; }
接下来看LinearSnapHelper.java 复写的三个方法
@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; } // 重点在findSnapView() final View currentView = findSnapView(layoutManager); if (currentView == null) { return RecyclerView.NO_POSITION; } final int currentPosition = layoutManager.getPosition(currentView); if (currentPosition == RecyclerView.NO_POSITION) { return RecyclerView.NO_POSITION; } // ...省略若干代码 return targetPos; }
省略的若干代码主要是根据手势滑动的速率计算目标item的位置。具体算法不用多研究。
可以看到方法内部又调用了 findSnapView() ;
@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; }
这里根据LayoutManager的方向做个判断,进而调用 findCenterView() 方法。
/** * 返回距离父容器中间位置最近的子View */ @Nullable private View findCenterView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) { int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } View closestChild = null; final int center; // 中间位值 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++) { // 循环判断子View中间位值距离父容器中间位值的差值 final View child = layoutManager.getChildAt(i); int childCenter = helper.getDecoratedStart(child) + (helper.getDecoratedMeasurement(child) / 2); int absDistance = Math.abs(childCenter - center); /** if child center is closer than previous closest, set it as closest **/ if (absDistance < absClosest) { absClosest = absDistance; closestChild = child; } } return closestChild; // 返回距离父容器中间位置最近的子View }
然后来看 calculateDistanceToFinalSnap()
@Override 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; }
定义一个二维数组,根据LayoutManager的方向来判断进行赋值。
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; }
该方法的目的即是 计算目标View距离父容器中间位值的差值。
至此,流程已经分析完毕。
总结如下:
-
有速率的滑动,会触发onScrollStateChanged() 和 onFling() 两个方法。
-
onScrollStateChanged() 方法内部调用 findSnapView() 找到对应的View,然后据此View在调用calculateDistanceToFinalSnap() 来计算该目标View需要移动的距离,最后通过RecyclerView.smoothScrollBy() 来移动View。
-
onFling() 方法内部调用 snapFromFling(), 然后在此方法内部首先创建了一个SmoothScroller 对象。接着调用 findTargetSnapPosition() 找到目标View的position,然后对smoothScroller设置该position,最后通过LayoutManager.startSmoothScroll() 开始移动View。
-
-
没有速率的滚动只会触发 onScrollStateChanged() 函数。
扩展
LinearSnapHelper 类的目的是将某个View停留在正中间,我们也可以通过这种方式来实现每次滑动结束之后将某个View停留在最左边或者最右边。
其实通过上面的分析,就会发现最主要的就是 calculateDistanceToFinalSnap 和 findSnapView 这两个函数。
在寻找目标View的时候,不像findCenterView那么简单。
以为需要考虑到最后item的边界情况。判断的不好就会出现,无论怎么滑动都会出现最后一个item无法完整显示的bug。
且看我的代码:
/** * 注意判断最后一个item时,应通过判断距离右侧的位置 * * @param layoutManager * @param helper * @return */ private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) { if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager return null; } int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } View closestChild = null; final int start = helper.getStartAfterPadding(); int absClosest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) { final View child = layoutManager.getChildAt(i); int childStart = helper.getDecoratedStart(child); int absDistance = Math.abs(childStart - start); if (absDistance < absClosest) { absClosest = absDistance; closestChild = child; } } // 边界情况判断 View firstVisibleChild = layoutManager.getChildAt(0); if (firstVisibleChild != closestChild) { return closestChild; } int firstChildStart = helper.getDecoratedStart(firstVisibleChild); int lastChildPos = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); View lastChild = layoutManager.getChildAt(childCount - 1); int lastChildCenter = helper.getDecoratedStart(lastChild) + (helper.getDecoratedMeasurement(lastChild) / 2); boolean isEndItem = lastChildPos == layoutManager.getItemCount() - 1; if (isEndItem && firstChildStart < 0 && lastChildCenter < helper.getEnd()) { return lastChild; } return closestChild; }
对于“反向的”同样要考虑边界情况。
private View findEndView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) { if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager return null; } int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == 0) { return null; } View closestChild = null; final int end = helper.getEndAfterPadding(); int absClosest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) { final View child = layoutManager.getChildAt(i); int childStart = helper.getDecoratedEnd(child); int absDistance = Math.abs(childStart - end); if (absDistance < absClosest) { absClosest = absDistance; closestChild = child; } } // 边界情况判断 View lastVisibleChild = layoutManager.getChildAt(childCount - 1); if (lastVisibleChild != closestChild) { return closestChild; } if (layoutManager.getPosition(closestChild) == ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()) { return closestChild; } View firstChild = layoutManager.getChildAt(0); int firstChildStart = helper.getDecoratedStart(firstChild); int firstChildPos = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); boolean isFirstItem = firstChildPos == 0; int firstChildCenter = helper.getDecoratedStart(firstChild) + (helper.getDecoratedMeasurement(firstChild) / 2); if (isFirstItem && firstChildStart < 0 && firstChildCenter > helper.getStartAfterPadding()) { return firstChild; } return closestChild; }
果图如下:
完整代码,请移步:JackSnapHelper.java