RecyclerView(五):SnapHelper对惯性滑动的作用说明

概述

SnapHelper可以看做是RecyclerView惯性滑动的一个辅助类,可以帮我们做一些惯性滑动时和滑动后的一些处理,所以对于一些惯性滑动的操作处理就可以优先考虑使用这个类,可以处理的点可以归纳为以下三点:

  1. 可以监听到滑动时手指抬起的那一刻;
  2. 指定手指抬起后RecyclerView惯性滑动的item个数;
  3. 滑动结束后指定item在界面所显示的位置;
SnapHelper用到的关键类说明

Google为我们提供了两个内部实现了SnapHelper的类,分别是LinearSnapHelper和PagerSnapHelper,作用分别是:

  1. LinearSnapHelper会将当前最靠近中间位置的item居中显示;
  2. PagerSnapHelper可以让RecyclerView实现像ViewPager一样的功能,只不过有一个前提条件,RecyclerView的item布局必须在滑动方向上使用MATCH_PARENT布局的;

为了更好的理解SnapHelper,这里有必要先了解下RecyclerView的两个内部类,分别是RecyclerView.OnFlingListener和RecyclerView.SmoothScroller:

  1. RecyclerView.OnFlingListener可以通过RecyclerView的setOnFlingListener()进行设置,设置完后,RecyclerView.OnFlingListener的onFling()方法会在滑动时(有惯性滑动)手指抬起的那一刻调用到,所以这时就可以对之后的惯性动作做一些设置;
  2. 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详解,这里面有对这些方法的详细解释。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值