简介
SnapHelper是RecyclerView的辅助类,可用来控制在滑动结束后,RecyclerView中item的对齐方式。
SnapHelper是一个抽象类,系统内置了两个默认实现类
LinearSnapHelper:使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动(滑动多页)
PagerSnapHelper:PagerSnapHelper的展示效果和LineSnapHelper是一样的,只是PagerSnapHelper 限制一次只能滑动一页,不能快速滑动。
滑动基础
RecyclerView的滚动分为滚动状态和Fling这两类,主要应对的是OnScrollListener和OnFlingListener这两个回调接⼝;
对应
- 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);
- }
滚动状态监听
SCROLL_STATE_IDLE 滚动闲置状态,此时并没有手指滑动或动画执行
SCROLL_STATE_DRAGGING 滚动拖拽状态,由于用户触摸屏幕产生
SCROLL_STATE_SETTLING 自动滚动状态,此时手没有触摸,一般是由动画执行滚动到最终位置、包括smoothScrollTo等方法的调用
Fling操作
手指在屏幕上滑动RecyclerView然后松手,RecyclerView中的内容会顺着惯性继续往手指滑动的方向继续滚动直到停止,这个过程叫做Fling。Fling操作从手指离开屏幕瞬间被触发,在滚动停止时结束。
过程
调用attachToRecyclerView绑定到RecyclerView时来完成对齐TargetView。
当Scroll被触发时和Fling操作的末尾阶段时对齐TargetView。
在attachToRecyclerView和onScrollStateChanged中都调用了snapToTargetExistingView这个方法。
attachToRecyclerView绑定
- public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
- throws IllegalStateException {
- //如果SnapHelper之前已经附着到此RecyclerView上,不用进行任何操作
- if (mRecyclerView == recyclerView) {
- return;
- }
- //如果SnapHelper之前附着的RecyclerView和现在的不一致,清理掉之前RecyclerView的回调
- if (mRecyclerView != null) {
- destroyCallbacks();
- }
- //更新RecyclerView对象引用
- mRecyclerView = recyclerView;
- if (mRecyclerView != null) {
- //设置当前RecyclerView对象的回调
- setupCallbacks();
- //创建一个Scroller对象,用于辅助计算fling的总距离,后面会涉及到
- mGravityScroller = new Scroller(mRecyclerView.getContext(),
- new DecelerateInterpolator());
- //调用snapToTargetExistingView()方法以实现对SnapView的对齐滚动处理
- snapToTargetExistingView();
- }
- }
snapToTargetExistingView对targetView进行滚动调整
- void snapToTargetExistingView() {
- if (mRecyclerView == null) {
- return;
- }
- LayoutManager layoutManager = mRecyclerView.getLayoutManager();
- if (layoutManager == null) {
- return;
- }
- //找出SnapView
- View snapView = findSnapView(layoutManager);
- if (snapView == null) {
- return;
- }
- //计算出SnapView需要滚动的距离
- int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
- //如果需要滚动的距离不是为0,就调用smoothScrollBy()使RecyclerView滚动相应的距离
- if (snapDistance[0] != 0 || snapDistance[1] != 0) {
- mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
- }
- }
Filing 操作 该方法会在RecyclerView开始做fling操作时被调用
- @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要进行fling操作需要的最小速率,
- //只有超过该速率,ItemView才会有足够的动力在手指离开屏幕时继续滚动下去
- int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
- //这里会调用snapFromFling()这个方法,就是通过该方法实现平滑滚动并使得在滚动停止时itemView对齐到目的坐标位置
- return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
- && snapFromFling(layoutManager, velocityX, velocityY);
- }
-
- private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
- int velocityY) {
- //layoutManager必须实现ScrollVectorProvider接口才能继续往下操作
- if (!(layoutManager instanceof ScrollVectorProvider)) {
- return false;
- }
-
- //创建SmoothScroller对象,这个东西是一个平滑滚动器,用于对ItemView进行平滑滚动操作
- RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
- if (smoothScroller == null) {
- return false;
- }
-
- //通过findTargetSnapPosition()方法,以layoutManager和速率作为参数,找到targetSnapPosition
- int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
- if (targetPosition == RecyclerView.NO_POSITION) {
- return false;
- }
- //通过setTargetPosition()方法设置滚动器的滚动目标位置
- smoothScroller.setTargetPosition(targetPosition);
- //利用layoutManager启动平滑滚动器,开始滚动到目标位置
- layoutManager.startSmoothScroll(smoothScroller);
- return true;
- }
LinearSnapHelper如何实现居中对齐的
主要是实现了上面提到的三个抽象方法,findTargetSnapPosition、calculateDistanceToFinalSnap和findSnapView。
calculateDistanceToFinalSnap
该方法计算最终对齐要移动的距离,返回一个长度为2的int 数组out,out[0] 为 x 方向移动的距离,out[1] 为 y 方向移动的距离。
- @Override
- public int[] calculateDistanceToFinalSnap(
- @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
- int[] out = new int[2];
- // 如果是水平方向滚动的,则计算水平方向需要移动的距离,否则水平方向的移动距离为0
- if (layoutManager.canScrollHorizontally()) {
- out[0] = distanceToCenter(layoutManager, targetView,
- getHorizontalHelper(layoutManager));
- } else {
- out[0] = 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) {
- 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;
- }
findSnapView: 找到要对齐的目标View, 最终的逻辑在findCenterView 方法里
- // 规则是:循环LayoutManager的所有子元素,计算每个 childView的
- //中点距离Parent 的中点,找到距离最近的一个,就是需要居中对齐的目标View
- @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;
- }
findTargetSnapPosition : 在触发fling时找到需要对齐的目标View的的Position 即targetSnapPosition。
- @Override
- public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
- int velocityY) {
- //判断layoutManager是否实现了RecyclerView.SmoothScroller.ScrollVectorProvider这个接口
- if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
- return RecyclerView.NO_POSITION;
- }
-
- 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;
- }
-
- final int currentPosition = layoutManager.getPosition(currentView);
- if (currentPosition == RecyclerView.NO_POSITION) {
- return RecyclerView.NO_POSITION;
- }
-
- RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
- (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
- // 通过ScrollVectorProvider接口中的computeScrollVectorForPosition()方法
- // 来确定layoutManager的布局方向
- PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
- if (vectorForEnd == null) {
- return RecyclerView.NO_POSITION;
- }
-
- int vDeltaJump, hDeltaJump;
- if (layoutManager.canScrollHorizontally()) {
- //layoutManager是横向布局,并且内容超出一屏,canScrollHorizontally()才返回true
- //估算fling结束时相对于当前snapView位置的横向位置偏移量
- hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
- getHorizontalHelper(layoutManager), velocityX, 0);
- //vectorForEnd.x < 0代表layoutManager是反向布局的,就把偏移量取反
- if (vectorForEnd.x < 0) {
- hDeltaJump = -hDeltaJump;
- }
- } else {
- //不能横向滚动,横向位置偏移量当然就为0
- hDeltaJump = 0;
- }
-
- //竖向的原理同上
- if (layoutManager.canScrollVertically()) {
- vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
- getVerticalHelper(layoutManager), 0, velocityY);
- if (vectorForEnd.y < 0) {
- vDeltaJump = -vDeltaJump;
- }
- } else {
- vDeltaJump = 0;
- }
-
- //根据layoutManager的横竖向布局方式,最终横向位置偏移量和竖向位置偏移量二选一,作为fling的位置偏移量
- int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
- if (deltaJump == 0) {
- return RecyclerView.NO_POSITION;
- }
- //当前位置加上偏移位置,就得到fling结束时的位置,这个位置就是targetPosition
- int targetPos = currentPosition + deltaJump;
- if (targetPos < 0) {
- targetPos = 0;
- }
- if (targetPos >= itemCount) {
- targetPos = itemCount - 1;
- }
- return targetPos;
- }