内容简介:这都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硬核讲解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
火的礼物:人类与计算技术的终极博弈(第4版)
【美】Baase,Sara(莎拉芭氏) / 郭耀、李琦 / 电子工业出版社 / 89.00
《火的礼物:人类与计算技术的终极博弈 (第4版)》是一本讲解与计算技术相关的社会、法律和伦理问题的综合性读物。《火的礼物:人类与计算技术的终极博弈 (第4版)》以希腊神话中普罗米修斯送给人类的火的礼物作为类比,针对当前IT技术与互联网迅速发展带来的一些社会问题,从法律和道德的角度详细分析了计算机技术对隐私权、言论自由、知识产权与著作权、网络犯罪等方面带来的新的挑战和应对措施,讲解了计算技术对人类的......一起来看看 《火的礼物:人类与计算技术的终极博弈(第4版)》 这本书的介绍吧!