SnapHelper硬核讲解

栏目: Android · 发布时间: 5年前

内容简介:这都9012年了,我忽然觉得有必要科普一下在正式介绍

这都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 核心功能的参与者,但有它就能锦上添花;

SnapHelper硬核讲解

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 行为流程图如下:

SnapHelper硬核讲解

其中 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 的实现流程图如下;

SnapHelper硬核讲解

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逻辑流程图如下

SnapHelper硬核讲解

段落小结

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 的效果展示之一;

SnapHelper硬核讲解

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
    
  • 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() 方法,做最后的调整,这就是我所说的接力赛;


以上所述就是小编给大家介绍的《SnapHelper硬核讲解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

火的礼物:人类与计算技术的终极博弈(第4版)

火的礼物:人类与计算技术的终极博弈(第4版)

【美】Baase,Sara(莎拉芭氏) / 郭耀、李琦 / 电子工业出版社 / 89.00

《火的礼物:人类与计算技术的终极博弈 (第4版)》是一本讲解与计算技术相关的社会、法律和伦理问题的综合性读物。《火的礼物:人类与计算技术的终极博弈 (第4版)》以希腊神话中普罗米修斯送给人类的火的礼物作为类比,针对当前IT技术与互联网迅速发展带来的一些社会问题,从法律和道德的角度详细分析了计算机技术对隐私权、言论自由、知识产权与著作权、网络犯罪等方面带来的新的挑战和应对措施,讲解了计算技术对人类的......一起来看看 《火的礼物:人类与计算技术的终极博弈(第4版)》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

html转js在线工具
html转js在线工具

html转js在线工具