最近用到了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;
}
首先,通过ScrollVectorProvider
的computeScrollVectorForPosition(int targetPosition)
获得滚动的正反方向。 可以简单看下LinearLayoutManager
的computeScrollVectorForPosition(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。