美文网首页Android收藏集
SnapHelper硬核讲解

SnapHelper硬核讲解

作者: HitenDev | 来源:发表于2019-04-23 17:54 被阅读61次

    前言

    这都9012年了,SnapHelper不是新鲜玩意,为啥我要拿出来解析?首先,Google已经放出 Viewpager2 测试版本,该方案计划用RecyclerView替换掉ViewPager;其次,我发现身边很多Android同学SnapHelper了解并不深;所以,弄懂并熟练使用SnapHelper是必要的;我借着阅读androidxViewpager2源码的机会,跟大家仔细梳理一下SnapHelper的原理;

    SnapHelper认识

    我忽然觉得有必要科普一下SnapHelper的基本情况,首先SnapHelper是附加于RecyclerView上面的一个辅助功能,它能让RecyclerView实现类似ViewPager等功能;如果没有SnapHelperRecyclerView也能很好的使用;但一个普通的RecyclerView在滚动方面和ListView没有特殊的区别,都是给人一种直来直往的感觉,比如我想实现横向滚动左边的子View始终左对齐,或者我用力一滑,惯性滚动最大距离不能超过一屏,这些看似不属于RecyclerView的功能,有了SnapHelper就很好的解决;所以SnapHelper有它存在的价值,它不是RecyclerView核心功能的参与者,但有它就能锦上添花;

    image

    RecyclerView滚动基础

    在正式介绍SnapHelper之前,先了解一下滚动相关的基础知识点,我把RecyclerView的滚动分为滚动状态Fling这两类,主要应对的是OnScrollListenerOnFlingListener这两个回调接口;

    滚动状态监听

    RecyclerVier一共有三种描述滚动的状态:SCROLL_STATE_IDLESCROLL_STATE_DRAGGINGSCROLL_STATE_SETTLING,稍微注释一下:

    • SCROLL_STATE_IDLE
      • 滚动闲置状态,此时并没有手指滑动或者动画执行
    • SCROLL_STATE_DRAGGING
      • 滚动拖拽状态,由于用户触摸屏幕产生
    • SCROLL_STATE_SETTLING
      • 自动滚动状态,此时没有手指触摸,一般是由动画执行滚动到最终位置,包括smoothScrollTo等方法的调用

    我们想监听状态的改变,调用addOnScrollListener方法,重写OnScrollListener的回调方法即可,注意OnScrollListener提供的回调数据并不如ViewPager那样详细,甚至是一种缺陷,这在ViewPager2ScrollEventAdapter类有详细的适配方法,有兴趣的可以看看。

    addOnScrollListener方法是接下来分析SnapHelper的重点之一;

    fling行为监听

    承接上文,自然滚动行为底层的要点是处理fling行为,flingAndroid View中惯性滚动的代言词,分析代码如下:

    RecyclerView

    public boolean fling(int velocityX, int velocityY) {
        if (mLayout == null) {
            Log.e(TAG, "Cannot fling without a LayoutManager set. " +
                    "Call setLayoutManager with a non-null argument.");
            return false;
        }
        if (mLayoutFrozen) {
            return false;
        }
        final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
        final boolean canScrollVertical = mLayout.canScrollVertically();
        if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
            velocityX = 0;
        }
        if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
            velocityY = 0;
        }
        if (velocityX == 0 && velocityY == 0) {
            // If we don't have any velocity, return false
            return false;
        }
        //处理嵌套滚动PreFling
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            //处理嵌套滚动Fling
            dispatchNestedFling(velocityX, velocityY, canScroll);
            //优先判断mOnFlingListener的逻辑
            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));
                //默认的Fling操作
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }
    

    RecyclerViewfling行为流程图如下:

    image

    其中mOnFlingListener是通过setOnFlingListener方法设置,这个方法也是接下来分析SnapHelper的重点之一;

    SnapHelper小觑

    SnapHelper顾名思义是Snap+Helper的组合,Snap有移到某位置的含义,Helper译为辅助者,综合场景解释是将RecyclerView移动到某位置的辅助类,这句话看似简单明了,却蕴藏疑问,有两个疑问点需要我们弄明白:

    何时何地触发RecyclerView移动?又要把RecyclerView移到哪个位置?

    带着这两个疑问,我们从SnapHelper的使用和入口方法看起:

    attachToRecyclerView入口

    PagerSnapHelper为例,SnapHelper的基本使用:

     new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
    

    PagerSnapHelperSnapHelper的子类,,SnapHelper的使用很简单,只需要调用attachToRecyclerView绑定到置顶RecyclerView即可;

    SnapHelper

    public abstract class SnapHelper extends RecyclerView.OnFlingListener 
        //绑定RecyclerView
        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();//移动到制定View
            }
        }
        //设置回调关系
        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);
        }
        
    }
    

    SnapHelper是一个抽象类,实现了RecyclerView.OnFlingListener接口,入口方法attachToRecyclerViewSnapHelper中定义,该方法主要起到清理、绑定回调关系和初始化位置的作用,在setupCallbacks中设置了addOnScrollListenersetOnFlingListener两种回调;

    上文说过RecyclerView的滚动状态和fling行为的监听,在这里看到SnapHelper对于这两种行为都需要监听,attachToRecyclerView的主要逻辑就是干这个事的,至于如何处理回调之后的事情,且继续往下看;

    SnapHelper处理回调流程

    SnapHelperattachToRecyclerView方法中注册了滚动状态和fling的监听,当监听触发时,如何处理后续的流程,我们先分析滚动状态的回调:

    滚动状态回调处理

    滚动状态的回调接口实例是mScrollListener

    SnapHelper

    private final RecyclerView.OnScrollListener mScrollListener =
             new RecyclerView.OnScrollListener() {
                 boolean mScrolled = false;
    
                 @Override
                 public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                     super.onScrollStateChanged(recyclerView, newState);
                     //静止状态且滚动过一段距离,触发snapToTargetExistingView();
                     if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                         mScrolled = false;
                         //移动到指定的已存在的View
                         snapToTargetExistingView();
                     }
                 }
    
                 @Override
                 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                     if (dx != 0 || dy != 0) {
                         mScrolled = true;
                     }
                 }
             };
    

    逻辑处理的入口在onScrollStateChanged方法中,当newState == RecyclerView.SCROLL_STATE_IDLE且滚动距离不等于0,触发snapToTargetExistingView方法;

    SnapHelper

    //移动到指定的已存在的View
    void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
        //查找SnapView
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
        //计算SnapView的距离
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            //调用smoothScrollBy移动到制定位置
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }
    

    snapToTargetExistingView方法顾名思义是移动到指定已存在的View的位置,findSnapView是查到目标的SnapViewcalculateDistanceToFinalSnap是计算SnapView到最终位置的距离;由于findSnapViewcalculateDistanceToFinalSnap是抽象方法,所以需要子类的具体实现;
    整理一下滚动状态回调下,SnapHelper的实现流程图如下;

    image

    Fling结果回调处理

    上文分析SnapHelper实现了RecyclerView.OnFlingListener接口,因此Fling的结果在onFling()方法中实现:

    @Override
    public boolean onFling(int velocityX, int velocityY) {
        RecyclerView.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);
    }
    //处理snap的fling逻辑
    private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
            //判断layoutManager要实现ScrollVectorProvider
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return false;
        }
        //创建SmoothScroller
        RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
        //获得snap position
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
        //设置position
        smoothScroller.setTargetPosition(targetPosition);
        //启动SmoothScroll
        layoutManager.startSmoothScroll(smoothScroller);
        //返回true拦截掉后续的fling操作
        return true;
    }
    
    //创建Scroller
    protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return null;
        }
        return new LinearSmoothScroller(mRecyclerView.getContext()) {
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
                if (mRecyclerView == null) {
                    // The associated RecyclerView has been removed so there is no action to take.
                    return;
                }
                //计算Snap到目标位置的距离
                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);
                }
            }
            //计算速度
            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };
    }
    

    fling流程分析

    • fling的逻辑主要在snapFromFling方法中,完成fling逻辑首先要求layoutManagerScrollVectorProvider的实现,为什么要求实现ScrollVectorProvider?,因为SnapHelper需要知道布局的方向,而ScrollVectorProvider正是该功能的提供者;

    • 其次是创建SmoothScroller,主要逻辑是createSnapScroller方法,该方法有默认的实现,主要逻辑是创建一个LinearSmoothScroller,在onTargetFound中调用calculateDistanceToFinalSnap计算距离,然后通过calculateTimeForDeceleration计算动画时间;

    • 然后通过findTargetSnapPosition方法获取目标targetPosition,最后把targetPosition赋值给smoothScroller,通过layoutManager执行该scroller;

    • 最重要的是snapFromFling要返回true,前文分析过RecyclerView的fling流程,返回true的话,默认的ViewFlinger就不会执行。

    fling逻辑流程图如下

    image

    段落小结

    SnapHelper对于滚动状态和Fling行为的处理上面已经梳理完毕,我特意画了两个草图,希望让大家有更清晰的认识,如果还不清晰至少得知道怎么用吧,例如我们要自定义SnapHelper,必须要重写的三个方法是:

    • findSnapView(RecyclerView.LayoutManager layoutManager)
      • 在滚动状态回调时调用,目的是查找SnapView,注意返回的SnapView必须是LayoutManager已经加载出来的View;
    • calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView)
      • 计算sanpView到指定位置的距离,这是在滚动状态回调和Fling的计算时间工程中使用;
    • findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY)
      • 查找指定的SnapPosition,这个方法只有在Fling的时候调用;

    记住这三个方法,如果想玩转SnapHelper,掌握这个三分方法是迈出的第一步;

    SnapHelper到底怎么玩

    往往知道方法怎么用,却不知道代码怎么写,这是最困惑的,我们以LinearSnapHelper为例,从细节出发,分析自定义SnapHelper的常用思路和关键方法;

    动代码前,先弄清这俩哥们到底解决了啥问题,首先LinearSnapHelper能够让线性排列的列表元素,最中间那颗元素居中显示;下图是LinearSnapHelper的效果展示之一;

    image

    findSnapView怎么玩

    前面交待过,findSnapView方法是查找SnapView的,何为SnapView,在LinearSnapHelper的应用场景中,屏幕(RecyclerView)中间的View就是SnapView,且看findSnapView方法的实现:

    LinearSnapHelper

    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;
    }
    
    @NonNull
    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
        if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
        }
        return mVerticalHelper;
    }
    
    @NonNull
    private OrientationHelper getHorizontalHelper(
            @NonNull RecyclerView.LayoutManager layoutManager) {
        if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
        }
        return mHorizontalHelper;
    }
    

    首先,findSnapView中需要判断RecyclerView滚动的方向,然后拿到对应的OrientationHelper,最后通过findCenterView查找到SnapView并返回;

    LinearSnapHelper

    private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }
        View closestChild = null;
        final int center;//中间位置
        //判断ClipToPadding逻辑
        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++) {
            final View child = layoutManager.getChildAt(i);
            //child的中间位置
            int childCenter = helper.getDecoratedStart(child) +
                    (helper.getDecoratedMeasurement(child) / 2);
            //每个child距离中心位置的差值
            int absDistance = Math.abs(childCenter - center);
            //取距离最小的那个
            if (absDistance < absClosest) {
                absClosest = absDistance;
                closestChild = child;
            }
        }
        return closestChild;
    }
    

    findCenterView()方法是获取屏幕(RecyclerView控件)中间位置最近的那个View当做SnapView,计算的过程稍显复杂其实比较了然,具体注释在代码中标注,容易产生疑惑的是OrientationHelper下面一堆获取位置的方法,这里稍微总结一下:

    OrientationHelper常见方法

    • getStartAfterPadding() 获取RecyclerView起始位置,如果padding不为0,则算上padding;
    • getTotalSpace() 获取RecyclerView可使用控件,本质上是RecyclerView的尺寸减轻两边的padding;
    • getDecoratedStart(View) 获取View的起始位置,如果RecyclerView有padding,则算上padding;
    • getDecoratedMeasurement(View) 获取View宽度,如果该view有maring,也会算上;

    总的来说findCenterView并不复杂,最迷惑人的是OrientationHelper的一堆API,在使用时稍加注意,也不是很复杂的;

    calculateDistanceToFinalSnap怎么玩

    首先,calculateDistanceToFinalSnap接受上一步获取的SnapView,需要返回一个int[],该数组约定长度为2,第0位表示水平方向的距离,第1位表示竖直方向的距离,且看LinearSnapHelper怎么玩;

    LinearSnapHelper

    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;
    }
    //距离中间位置的距离
    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
        //targetView的中心位置(距离RecyclerView start为准)
        final int childCenter = helper.getDecoratedStart(targetView) +
                (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;  //RecyclerView的中心位置
        if (layoutManager.getClipToPadding()) {
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
        return childCenter - containerCenter;//差距
    }
    

    很幸运,calculateDistanceToFinalSnap并没有很复杂的代码,主要是计算方向,然后通过OrientationHelper计算第一步findSnapView得到的SnapView距离中间位置的距离;代码和第一步很相似,注释在代码中;

    findTargetSnapPosition怎么玩

    前面说过,findTargetSnapPosition是处理Fling流程中,计算SnapPosition的关键方法,首先,findTargetSnapPosition接受速度参数velocityXvelocityY,需要返回int类型的position,这个位置对应的是Adapter中的position,并不是LayoutManagerRecyclerView中子View的index

    LinearSnapHelper

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
            //判断是否实现ScrollVectorProvider
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return RecyclerView.NO_POSITION;
        }
        //获取Adapter中item个数
        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;
        }
        //计算当前View在adapter中的position
        final int currentPosition = layoutManager.getPosition(currentView);
        if (currentPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }
        //获取布局方向提供者
        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
        //从当前位置往最后一个元素计算
        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
        if (vectorForEnd == null) {
            return RecyclerView.NO_POSITION;
        }
    
        int vDeltaJump, hDeltaJump;//计算惯性能滚动多少个子View
        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;
        }
        //计算目标position
        int targetPos = currentPosition + deltaJump;
        if (targetPos < 0) {//边界判断
            targetPos = 0;
        }
        if (targetPos >= itemCount) {//边界判断
            targetPos = itemCount - 1;
        }
        return targetPos;
    }
    

    计算通过惯性能滚动多少个子View的代码:

    LinearSnapHelper

    private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper, int velocityX, int velocityY) {
        //惯性能滚动多少距离
        int[] distances = calculateScrollDistance(velocityX, velocityY);
        //单个child平均占用多少宽/高像素
        float distancePerChild = computeDistancePerChild(layoutManager, helper);
        if (distancePerChild <= 0) {
            return 0;
        }
        //得到最终的水平/竖直的距离
        int distance =
                Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
        if (distance > 0) {四舍五入得到平均个数
            return (int) Math.floor(distance / distancePerChild);
        } else {//负数的除法特殊处理得到平均个数
            return (int) Math.ceil(distance / distancePerChild);
        }
    }
    

    计算每个child的平均占用多少宽/高的代码如下:

    LinearSnapHelper

    private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        View minPosView = null;
        View maxPosView = null;
        int minPos = Integer.MAX_VALUE;
        int maxPos = Integer.MIN_VALUE;
        int childCount = layoutManager.getChildCount();//获取已经加载的View个数,不是所有adapter中的count
        if (childCount == 0) {
            return INVALID_DISTANCE;
        }
        //计算已加载View中,最start和最end的View和Position
        for (int i = 0; i < childCount; i++) {
            View child = layoutManager.getChildAt(i);
            final int pos = layoutManager.getPosition(child);
            if (pos == RecyclerView.NO_POSITION) {
                continue;
            }
            if (pos < minPos) {
                minPos = pos;
                minPosView = child;
            }
            if (pos > maxPos) {
                maxPos = pos;
                maxPosView = child;
            }
        }
        if (minPosView == null || maxPosView == null) {
            return INVALID_DISTANCE;
        }
        //分别获取最start和最end位置,距RecyclerView起点的距离;
        int start = Math.min(helper.getDecoratedStart(minPosView),
                helper.getDecoratedStart(maxPosView));
        int end = Math.max(helper.getDecoratedEnd(minPosView),
                helper.getDecoratedEnd(maxPosView));
        //得到距离的绝对差值
        int distance = end - start;
        if (distance == 0) {
            return INVALID_DISTANCE;
        }
        //计算平均宽/高
        return 1f * distance / ((maxPos - minPos) + 1);
    }
    

    LinearSnapHelperfindTargetSnapPosition方法着实不简单,但是条理清晰逻辑严谨,考虑的比较周全,上面代码我做了比较详细的注释,相信肯定有同学不爱看代码,我也是,所以我用文字重新梳理一下上述代码逻辑和关键点;

    • findTargetSnapPosition方法逻辑流程总结:

      • 首先通过findSnapView()活动当前的centerView;
      • 通过ScrollVectorProvider是否是reverseLayout,布局方向;
      • 通过estimateNextPositionDiffForFling方法获取该惯性能产生多少个子child的平移,或者理解成该惯性能让RecyclerView滚动多远个子child的距离;
      • 通过当前的centerView下标,加上惯性产生的平移,计算出最终要落地的下标;
      • 边界判断
    • estimateNextPositionDiffForFling方法逻辑流程总结:

      • 通过calculateScrollDistance计算惯性能滚动多远距离;
      • 通过computeDistancePerChild计算平均一个child占多大尺寸;
      • 距离除以尺寸,四舍五入得到个数并返回;
    • computeDistancePerChild方法逻辑流程总结:

      • 获取layoutManager已经加载的所有子View;
      • 获取最start和最end的view和下标;
      • 分别计算最start和最end的View的start和end值;
      • 计算平均值并返回;

    终于是把LinearSnapHelper的核心逻辑讲完了,纵观整个类,主要逻辑还是在findTargetSnapPosition这里,趁热打铁,我必须跟大家分享一下PagerSnapHelper是如何玩转这个方法的;

    PagerSnapHelper似乎更简单

    pagerSnapHelper同样也实现了SnapHelper的三个方法,下面先看findTargetSnapPosition:

    PagerSnapHelper

    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        final int itemCount = layoutManager.getItemCount();//获取adapter中所有的itemcount
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }
    
        View mStartMostChildView = null;//获取最start的View
        if (layoutManager.canScrollVertically()) {
            mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
        }
    
        if (mStartMostChildView == null) {
            return RecyclerView.NO_POSITION;
        }
        //最start的View当前centerposition
        final int centerPosition = layoutManager.getPosition(mStartMostChildView);
        if (centerPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }
    
        final boolean forwardDirection;//速度判定
        if (layoutManager.canScrollHorizontally()) {
            forwardDirection = velocityX > 0;
        } else {
            forwardDirection = velocityY > 0;
        }
        boolean reverseLayout = false;//是否是reverseLayout,布局方向
        if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                    (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
            PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
            if (vectorForEnd != null) {
                reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
            }
        }
        return reverseLayout
                ? (forwardDirection ? centerPosition - 1 : centerPosition)下标要买+1 or -1,要么保持不变
                : (forwardDirection ? centerPosition + 1 : centerPosition);
    }
    

    众所周知,ViewPager的翻页要么是保持不变,要么是下一页/上一页,上面findTargetSnapPosition方法就是主要的实现逻辑,其中判定是否翻页的条件由forwardDirection来控制,直接对比速度>0,用户想轻松滑到下一页是比较easy的,以至于上面代码量少到不敢相信;

    至于findSnapViewdistanceToCenter方法,同样是获取屏幕(RecyclerView)中间的View,计算distanceToCenter,跟LinearSnapHelper如出一辙;

    PagerSnapHelper注意事项

    PagerSnapHelper设计之初是就是适用于一屏(RecyclerView范围内)显示单个child的,如果有一屏显示多个child的需求,PagerSnapHelper并不适用;其实在实际开发中这种需求还是挺多的,当然github上早已经有大神写过一个库,实现了几个常用的SnapHelper场景,github传送门;当然这个库并不能满足所有的需求,有机会再跟大家分享更有意义的SnapHelper实战;

    结尾:明明是玩了一场接力赛

    什么玩意,接力赛?没有错。SnapHelper在运行过程中,RecyclerView的状态可能会经历这样DRAGGING->SETTLING->IDLE->SETTLING->IDLE甚至更多状态,我称之为接力赛,为什么会这个样子?拿LinearSnapHelper来说,前期手势拖拽,肯定是玩DRAGGING状态,一旦撒手加之惯性,会进入SETTLING状态,然后fling()方法会计算snapPosition并指示SmoothScrooler滚动到snapPosition位置,滚动完毕会进入IDLE状态,注意SmoothScrooler滚动结束的位置相对于RecyclerView的start位置的,而LinearSnapHelper要求中间对齐,此时必然会触发snapToTargetExistingView()方法,做最后的调整,所谓最后的调整是通过snapToTargetExistingView调用smoothScrollBy,而结束条件通常是calculateDistanceToFinalSnap()返回[0,0],这就是我所说的接力赛;

    陷阱: 一旦calculateDistanceToFinalSnap()返回值计算错误,有可能造成RecyclerView进入smoothScroolBy的魔鬼循环局面,直到滚动到头/尾才会结束;

    相关文章

      网友评论

        本文标题:SnapHelper硬核讲解

        本文链接:https://www.haomeiwen.com/subject/gdjbgqtx.html