这都9012年了,SnapHelper
不是新鲜玩意,为啥我要拿出来解析?首先,Google已经放出 Viewpager2 测试版本,该方案计划用RecyclerView
替换掉ViewPager
;其次,我发现身边不少Android同窗
对SnapHelper
了解并不深;因此,弄懂并熟练使用SnapHelper
是必要的;我借着阅读androidx
和Viewpager2
源码的机会,跟你们仔细梳理一下SnapHelper
的原理;android
我突然以为有必要科普一下SnapHelper
的基本状况,首先SnapHelper
是附加于RecyclerView
上面的一个辅助功能,它能让RecyclerView
实现相似ViewPager
等功能;若是没有SnapHelper
,RecyclerView
也能很好的使用;但一个普通的RecyclerView
在滚动方面和ListView
没有特殊的区别,都是给人一种直来直往的感受,好比我想实现横向滚动左边的子View始终左对齐,或者我用力一滑,惯性滚动最大距离不能超过一屏,这些看似不属于RecyclerView
的功能,有了SnapHelper
就很好的解决;因此SnapHelper
有它存在的价值,它不是RecyclerView
核心功能的参与者,但有它就能锦上添花; git
在正式介绍SnapHelper
以前,先了解一下滚动相关的基础知识点,我把RecyclerView的滚动分为滚动状态
和Fling
这两类,主要应对的是OnScrollListener
和OnFlingListener
这两个回调接口;github
下RecyclerVier
一共有三种描述滚动的状态:SCROLL_STATE_IDLE
、SCROLL_STATE_DRAGGING
、SCROLL_STATE_SETTLING
,稍微注释一下:数组
SCROLL_STATE_IDLE
SCROLL_STATE_DRAGGING
SCROLL_STATE_SETTLING
咱们想监听状态的改变,调用addOnScrollListener
方法,重写OnScrollListener
的回调方法便可,注意OnScrollListener
提供的回调数据并不如ViewPager
那样详细,甚至是一种缺陷,这在ViewPager2
中ScrollEventAdapter
类有详细的适配方法,有兴趣的能够看看。bash
addOnScrollListener
方法是接下来分析SnapHelper
的重点之一;ide
承接上文,天然滚动行为底层的要点是处理fling
行为,fling
是Android 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; } 复制代码
在RecyclerView
中fling
行为流程图以下:测试
其中mOnFlingListener
是经过setOnFlingListener
方法设置,这个方法也是接下来分析SnapHelper
的重点之一;动画
SnapHelper
顾名思义是Snap
+Helper
的组合,Snap
有移到某位置的含义,Helper
译为辅助者,综合场景解释是将RecyclerView
移动到某位置的辅助类,这句话看似简单明了,却蕴藏疑问,有两个疑问点须要咱们弄明白:
什么时候何地触发RecyclerView移动?又要把RecyclerView移到哪一个位置?
带着这两个疑问,咱们从SnapHelper
的使用和入口方法看起:
以PagerSnapHelper
为例,SnapHelper的基本使用:
new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
复制代码
PagerSnapHelper
是SnapHelper
的子类,,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
接口,入口方法attachToRecyclerView
在SnapHelper
中定义,该方法主要起到清理、绑定回调关系和初始化位置的做用,在setupCallbacks
中设置了addOnScrollListener
和setOnFlingListener
两种回调;
上文说过RecyclerView
的滚动状态和fling行为的监听,在这里看到SnapHelper
对于这两种行为都须要监听,attachToRecyclerView
的主要逻辑就是干这个事的,至于如何处理回调以后的事情,且继续往下看;
SnapHelper
在attachToRecyclerView
方法中注册了滚动状态和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
是查到目标的SnapView
,calculateDistanceToFinalSnap
是计算SnapView
到最终位置的距离;因为findSnapView
和calculateDistanceToFinalSnap
是抽象方法,因此须要子类的具体实现; 整理一下滚动状态
回调下,SnapHelper
的实现流程图以下;
上文分析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逻辑首先要求layoutManager
是ScrollVectorProvider
的实现,为何要求实现ScrollVectorProvider
?,由于SnapHelper
须要知道布局的方向,而ScrollVectorProvider
正是该功能的提供者;
其次是建立SmoothScroller
,主要逻辑是createSnapScroller
方法,该方法有默认的实现,主要逻辑是建立一个LinearSmoothScroller
,在onTargetFound
中调用calculateDistanceToFinalSnap
计算距离,而后经过calculateTimeForDeceleration
计算动画时间;
而后经过findTargetSnapPosition
方法获取目标targetPosition
,最后把targetPosition
赋值给smoothScroller
,经过layoutManager
执行该scroller
;
最重要的是snapFromFling
要返回true
,前文分析过RecyclerView
的fling流程,返回true
的话,默认的ViewFlinger
就不会执行。
fling逻辑流程图以下
SnapHelper
对于滚动状态和Fling行为的处理上面已经梳理完毕,我特地画了两个草图,但愿让你们有更清晰的认识,若是还不清晰至少得知道怎么用吧,例如咱们要自定义SnapHelper
,必需要重写的三个方法是:
findSnapView(RecyclerView.LayoutManager layoutManager)
calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView)
findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY)
记住这三个方法,若是想玩转SnapHelper
,掌握这个三分方法是迈出的第一步;
每每知道方法怎么用,殊不知道代码怎么写,这是最困惑的,咱们以LinearSnapHelper
为例,从细节出发,分析自定义SnapHelper
的经常使用思路和关键方法;
动代码前,先弄清这俩哥们到底解决了啥问题,首先LinearSnapHelper
可以让线性排列的列表元素,最中间那颗元素居中显示;下图是LinearSnapHelper
的效果展现之一;
前面交待过,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常见方法
总的来讲findCenterView
并不复杂,最迷惑人的是OrientationHelper
的一堆API,在使用时稍加注意,也不是很复杂的;
首先,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
是处理Fling流程中,计算SnapPosition的关键方法,首先,findTargetSnapPosition
接受速度参数velocityX
和velocityY
,须要返回int类型的position
,这个位置对应的是Adapter
中的position
,并非LayoutManager
和RecyclerView
中子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);
}
复制代码
LinearSnapHelper
的findTargetSnapPosition
方法着实不简单,可是条理清晰逻辑严谨,考虑的比较周全,上面代码我作了比较详细的注释,相信确定有同窗不爱看代码,我也是,因此我用文字从新梳理一下上述代码逻辑和关键点;
findTargetSnapPosition
方法逻辑流程总结:
findSnapView()
活动当前的centerView
;ScrollVectorProvider
是不是reverseLayout,布局方向;estimateNextPositionDiffForFling
方法获取该惯性能产生多少个子child的平移,或者理解成该惯性能让RecyclerView滚动多远个子child的距离;centerView
下标,加上惯性产生的平移,计算出最终要落地的下标;estimateNextPositionDiffForFling
方法逻辑流程总结:
calculateScrollDistance
计算惯性能滚动多远距离;computeDistancePerChild
计算平均一个child占多大尺寸;computeDistancePerChild
方法逻辑流程总结:
终因而把LinearSnapHelper
的核心逻辑讲完了,纵观整个类,主要逻辑仍是在findTargetSnapPosition
这里,趁热打铁,我必须跟你们分享一下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的,以致于上面代码量少到不敢相信;
至于findSnapView
和distanceToCenter
方法,一样是获取屏幕(RecyclerView)中间的View,计算distanceToCenter
,跟LinearSnapHelper
一模一样;
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
的魔鬼循环局面,直到滚动到头/尾才会结束;