内容简介:这都9012年了,我忽然觉得有必要科普一下在正式介绍
这都9012年了, SnapHelper 不是新鲜玩意,为啥我要拿出来解析?首先,Google已经放出Viewpager2 测试版本,该方案计划用 RecyclerView 替换掉 ViewPager ;其次,我发现身边很多 Android同学 对 SnapHelper 了解并不深;所以,弄懂并熟练使用 SnapHelper 是必要的;我借着阅读 androidx 和 Viewpager2 源码的机会,跟大家仔细梳理一下 SnapHelper 的原理;
SnapHelper认识
我忽然觉得有必要科普一下 SnapHelper 的基本情况,首先 SnapHelper 是附加于 RecyclerView 上面的一个辅助功能,它能让 RecyclerView 实现类似 ViewPager 等功能;如果没有 SnapHelper , RecyclerView 也能很好的使用;但一个普通的 RecyclerView 在滚动方面和 ListView 没有特殊的区别,都是给人一种直来直往的感觉,比如我想实现横向滚动左边的子View始终左对齐,或者我用力一滑,惯性滚动最大距离不能超过一屏,这些看似不属于 RecyclerView 的功能,有了 SnapHelper 就很好的解决;所以 SnapHelper 有它存在的价值,它不是 RecyclerView 核心功能的参与者,但有它就能锦上添花;
RecyclerView滚动基础
在正式介绍 SnapHelper 之前,先了解一下滚动相关的基础知识点,我把RecyclerView的滚动分为 滚动状态 和 Fling 这两类,主要应对的是 OnScrollListener 和 OnFlingListener 这两个回调接口;
滚动状态监听
下 RecyclerVier 一共有三种描述滚动的状态: SCROLL_STATE_IDLE 、 SCROLL_STATE_DRAGGING 、 SCROLL_STATE_SETTLING ,稍微注释一下:
-
SCROLL_STATE_IDLE- 滚动闲置状态,此时并没有手指滑动或者动画执行
-
SCROLL_STATE_DRAGGING- 滚动拖拽状态,由于用户触摸屏幕产生
-
SCROLL_STATE_SETTLING- 自动滚动状态,此时没有手指触摸,一般是由动画执行滚动到最终位置,包括smoothScrollTo等方法的调用
我们想监听状态的改变,调用 addOnScrollListener 方法,重写 OnScrollListener 的回调方法即可,注意 OnScrollListener 提供的回调数据并不如 ViewPager 那样详细,甚至是一种缺陷,这在 ViewPager2 中 ScrollEventAdapter 类有详细的适配方法,有兴趣的可以看看。
addOnScrollListener 方法是接下来分析 SnapHelper 的重点之一;
fling行为监听
承接上文,自然滚动行为底层的要点是处理 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小觑
SnapHelper 顾名思义是 Snap + Helper 的组合, Snap 有移到某位置的含义, Helper 译为辅助者,综合场景解释是将 RecyclerView 移动到某位置的辅助类,这句话看似简单明了,却蕴藏疑问,有两个疑问点需要我们弄明白:
何时何地触发RecyclerView移动?又要把RecyclerView移到哪个位置?
带着这两个疑问,我们从 SnapHelper 的使用和入口方法看起:
attachToRecyclerView入口
以 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处理回调流程
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 的实现流程图如下;
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逻辑首先要求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)- 在滚动状态回调时调用,目的是查找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 的效果展示之一;
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 接受速度参数 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
-
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的,以至于上面代码量少到不敢相信;
至于 findSnapView 和 distanceToCenter 方法,同样是获取屏幕(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硬核讲解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
500 Lines or Less
Amy Brown、Michael DiBernardo / 2016-6-28 / USD 35.00
This book provides you with the chance to study how 26 experienced programmers think when they are building something new. The programs you will read about in this book were all written from scratch t......一起来看看 《500 Lines or Less》 这本书的介绍吧!