SnapHelper分析

最近用到了SnapHelper,惭愧,现在才发现这个好东西...,在这里记录一下使用过程。
SnapHelper是一个抽象类,系统实现了两个子类,LinearSnapHelper和PagerSnapHelper。

使用方法

1、LinearSnapHelper

// 省略其他部分代码...
val linearHelper = LinearSnapHelper()
linearHelper.attachToRecyclerView(recyclerView)

2、PagerSnapHelper

 val snapHelper = PagerSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)

源码分析

这里分析一下LinearSnapHelper(PagerSnapHelper和LinearSnapHelper差不多),涉及到的类:


附上渣图一张

首先,从入口方法attachToRecyclerView(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();
  }
}

可以看到,
第一步,先判断我们要设置的recyclerView是不是snapHelper已经绑定的mRecyclerView,如果是,则什么都不做;
第二步,如果snapHelper已经绑定的mRecyclerView不为null,则先移除mRecyclerView上的监听(RecyclerView.OnScrollListener 和 RecyclerView.OnFlingListener)
第三步,将我们要设置的recyclerView绑定到snapHelper,并为它设置监听(RecyclerView.OnScrollListener 和 RecyclerView.OnFlingListener)
第四部,创建一个用于平滑滚动到scroller
第五步,调用snapToTargetExistingView(),滑动到符合条件的view

我们先看一下给recyclerView设置的RecyclerView.OnScrollListener:

// 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();
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                if (dx != 0 || dy != 0) {
                    mScrolled = true;
                }
            }
    };

诶,我们发现,在onScrollStateChanged()方法里面,也调用了snapToTargetExistingView(),我们看一下这个方法到底做了什么:

/**
 * Snaps to a target view which currently exists in the attached {@link RecyclerView}. This
 * method is used to snap the view when the {@link RecyclerView} is first attached; when
 * snapping was triggered by a scroll and when the fling is at its final stages.
 */
void snapToTargetExistingView() {
    if (mRecyclerView == null) {
        return;
    }
    RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    if (layoutManager == null) {
        return;
    }
    View snapView = findSnapView(layoutManager);
    if (snapView == null) {
        return;
    }
    int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
    if (snapDistance[0] != 0 || snapDistance[1] != 0) {
        mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
    }
}

从注释可以知道,该方法是滚动到一个已经添加到recyclerView中的Item的位置。那这里有两个疑问:
1、这个item是怎么确定下来的呢?或者说,我们怎么得到这个Item?
2、得到这个item之后,我们怎么滚动到这个item那里去?或者说滚多远??

关键方法出现了:
1、 findSnapView(layoutManager) 获取目标view,即待滚动到的item的view
2、calculateDistanceToFinalSnap(layoutManager, snapView) 获取距离目标view的距离

先看findSnapView(layoutManager),这是一个抽象方法,每个子类获取目标view的方法都不一定一样,我们看下LinearSnapHelper的实现:

// 包含item的margin的宽
@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;
}

可以看到,LinearSnapHelper是先判断是水平滚动还是垂直滚动,具体获取view的操作是由findCenterView(LayoutManager, OrientationHelper)来完成的。

这里又出现了一个陌生的类:OrientationHelper。OrientationHelper主要职责是用于获取Item的宽高(包含或不包含Padding、Margin等等),以及RecyclerView的宽高等。
简单看一下源码就行:

// recycerView的右边界(不包含右padding)
@Override
public int getEndAfterPadding() {
    return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
}

// recyclerView的右边界(包含右Padding)
@Override
public int getEnd() {
    return mLayoutManager.getWidth();
}

// 包含item的margin的宽
@Override
public int getDecoratedMeasurement(View view) {
    final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin;
}

... 后边都差不多不分析了

看回findCenterView():

    /**
     * Return the child view that is currently closest to the center of this parent.
     *
     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
     *                      {@link RecyclerView}.
     * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
     *
     * @return the child view that is currently closest to the center of this parent.
     */
    @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++) {
            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的中点 到 RecyclerView中点的距离,返回一个离RecyclerView中点最近的item的view。

好,拿到view后,我们再看calculateDistanceToFinalSnap(layoutManager, snapView),同样是抽象方法,看在LinearSnapHelper中是怎么拿到滑动距离的:

    @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;
    }

同样是先判断滚动方向,再由distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager))去获取水平或竖直方向要滚动到距离。这里同样是要传入一个 OrientationHelper 用于获取view到大小。

直接看distanceToCenter()的源码吧:

    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的中点坐标减去recyclerView的中点坐标,得到滑动的距离。

好,至此,滚动距离也有了,我们就可以滚动到指定位置啦:

  void snapToTargetExistingView() {
        ...
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }

完了吗?不,还没有,嘿嘿,还有一丢丢。我们还有 RecyclerView.OnFlingListener 没分析,它是用于处理快速滑动的,让我们来扒光它看看:

    @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);
    }

可以看到,我们先获取到RecyclerView的一个minFlingVelocity,只有当velocityX或velocityY大于minFlingVelocity,且snapFromFling(layoutManager, velocityX, velocityY)返回true时,我们才处理onFling。

看一下snapFromFling(layoutManager, velocityX, velocityY),该方法是由SnapHelper提供的用于在fling时完成snap操作的:

/**
     * Helper method to facilitate for snapping triggered by a fling.
     *
     * @param layoutManager The {@link RecyclerView.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 RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            return false;
        }

        RecyclerView.SmoothScroller smoothScroller = createScroller(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;
    }

主要做了3件事:
1、创建一个用于平滑滚动的scroller, createScroller(layoutManager)
2、找到目标view的position, findTargetSnapPosition(layoutManager, velocityX, velocityY)
3、调用 LayoutManager的startSmoothScroll(smoothScroller) 完成滚动

下面依次分析:

1、先看createScroller(layoutManager),该方法把创建的任务又交给createSnapScroller(layoutManager)了
/**
     * Creates a scroller to be used in the snapping implementation.
     *
     * @param layoutManager     The {@link RecyclerView.LayoutManager} associated with the attached
     *                          {@link RecyclerView}.
     *
     * @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling.
     */
    @Nullable
    protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
        return createSnapScroller(layoutManager);
    }
/**
     * Creates a scroller to be used in the snapping implementation.
     *
     * @param layoutManager     The {@link RecyclerView.LayoutManager} associated with the attached
     *                          {@link RecyclerView}.
     *
     * @return a {@link LinearSmoothScroller} which will handle the scrolling.
     * @deprecated use {@link #createScroller(RecyclerView.LayoutManager)} instead.
     */
    @Nullable
    @Deprecated
    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;
                }
                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;
            }
        };
    }

可以发现,
a、RecyclerView的LayoutManager必须实现RecyclerView.SmoothScroller.ScrollVectorProvider接口

b、返回一个LinearSmoothScroller的实例,并重写void onTargetFound() 和 float calculateSpeedPerPixel()。

onTargetFound()方法中,先通过calculateDistanceToFinalSnap()(前面已经分析过啦)获取到要滑动到距离,然后通过calculateTimeForDeceleration(int dx)获取滑动剩余时间。如果剩余时间>0,则调用update(@Px int dx, @Px int dy, int duration, @Nullable Interpolator interpolator)更新一下信息。

calculateSpeedPerPixel()方法则是返回滑过每个像素所花费的时间,在LinearSmoothScroller的构造方法中被调用。

2、再看findTargetSnapPosition(layoutManager, velocityX, velocityY),该方法返回targetView的position。
@Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        // ... 省略部分代码,都是判断的

        final View currentView = findSnapView(layoutManager);
        final int currentPosition = layoutManager.getPosition(currentView);

        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
        // deltaJumps sign comes from the velocity which may not match the order of children in
        // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
        // get the direction.
        // 1、获得滚动的正反方向
        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
        
        // 2、获得fling后,滚动到targetView需要经过的item数量
        int vDeltaJump, hDeltaJump;
        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;
        }

        // 3、返回targetView的position
        int targetPos = currentPosition + deltaJump;
        if (targetPos < 0) {
            targetPos = 0;
        }
        if (targetPos >= itemCount) {
            targetPos = itemCount - 1;
        }
        return targetPos;
    }

首先,通过ScrollVectorProvidercomputeScrollVectorForPosition(int targetPosition)获得滚动的正反方向。 可以简单看下LinearLayoutManagercomputeScrollVectorForPosition(int targetPosition)方法的实现:

@Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        if (getChildCount() == 0) {
            return null;
        }
        final int firstChildPos = getPosition(getChildAt(0));
        final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
        if (mOrientation == HORIZONTAL) {
            return new PointF(direction, 0);
        } else {
            return new PointF(0, direction);
        }
    }

然后,调用estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager, OrientationHelper helper, int velocityX, int velocityY)获得fling后,滚动到targetView需要经过的item数量deltaJump

    // 获取滚动到targetView需要经过的item数量
    private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper, int velocityX, int velocityY) {
        int[] distances = calculateScrollDistance(velocityX, velocityY);
        float distancePerChild = computeDistancePerChild(layoutManager, helper);
        if (distancePerChild <= 0) {
            return 0;
        }
        int distance =
                Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
        return (int) Math.round(distance / distancePerChild);
    }

    // 获取滚动到targetView需要经过的距离
    public int[] calculateScrollDistance(int velocityX, int velocityY) {
        int[] outDist = new int[2];
        mGravityScroller.fling(0, 0, velocityX, velocityY,
                Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        outDist[0] = mGravityScroller.getFinalX();
        outDist[1] = mGravityScroller.getFinalY();
        return outDist;
    }

    // 获取一个item的宽/高
    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();
        if (childCount == 0) {
            return INVALID_DISTANCE;
        }

        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;
        }
        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);
    }

最后,返回currentPosition+ deltaJump,得到targetView的position。

3、调用LayoutManager.startSmoothScroll(smoothScroller) ,通过SmoothScroller完成平滑滚动。
至此,算是分析完毕了....累死。

你可能感兴趣的:(SnapHelper分析)