这都9012年了,
SnapHelper
不是新鲜玩意,为啥我要拿出来解析?首先,Google已经放出
Viewpager2
测试版本,该方案计划用
RecyclerView
替换掉
ViewPager
;其次,我发现身边很多
Android同学
对
SnapHelper
了解并不深;所以,弄懂并熟练使用
SnapHelper
是必要的;我借着阅读
androidx
和
Viewpager2
源码的机会,跟大家仔细梳理一下
SnapHelper
的原理;
SCROLL_STATE_IDLE
SCROLL_STATE_DRAGGING
SCROLL_STATE_SETTLING
我们想监听状态的改变,调用
addOnScrollListener
方法,重写
OnScrollListener
的回调方法即可,注意
OnScrollListener
提供的回调数据并不如
ViewPager
那样详细,甚至是一种缺陷,这在
ViewPager2
中
ScrollEventAdapter
类有详细的适配方法,有兴趣的可以看看。
addOnScrollListener
方法是接下来分析
SnapHelper
的重点之一;
承接上文,自然滚动行为底层的要点是处理
fling
行为,
fling
是
Android View中
惯性滚动的代言词,分析代码如下:
RecyclerView
1 |
public boolean fling(int velocityX, int velocityY) { |
在
RecyclerView
中
fling
行为流程图如下:
其中
mOnFlingListener
是通过
setOnFlingListener
方法设置,这个方法也是接下来分析
SnapHelper
的重点之一;
何时何地触发RecyclerView移动?又要把RecyclerView移到哪个位置?
带着这两个疑问,我们从
SnapHelper
的使用和入口方法看起:
以
PagerSnapHelper
为例,SnapHelper的基本使用:
1 |
new PagerSnapHelper().attachToRecyclerView(mRecyclerView); |
PagerSnapHelper
是
SnapHelper
的子类,,
SnapHelper
的使用很简单,只需要调用
attachToRecyclerView
绑定到置顶
RecyclerView
即可;
SnapHelper
1 |
public abstract class SnapHelper extends RecyclerView.OnFlingListener |
SnapHelper
是一个抽象类,实现了
RecyclerView.OnFlingListener
接口,入口方法
attachToRecyclerView
在
SnapHelper
中定义,该方法主要起到清理、绑定回调关系和初始化位置的作用,在
setupCallbacks
中设置了
addOnScrollListener
和
setOnFlingListener
两种回调;
上文说过
RecyclerView
的滚动状态和fling行为的监听,在这里看到
SnapHelper
对于这两种行为都需要监听,
attachToRecyclerView
的主要逻辑就是干这个事的,至于如何处理回调之后的事情,且继续往下看;
SnapHelper
在
attachToRecyclerView
方法中注册了滚动状态和fling的监听,当监听触发时,如何处理后续的流程,我们先分析
滚动状态
的回调:
滚动状态的回调接口实例是
mScrollListener
:
SnapHelper
1 |
private final RecyclerView.OnScrollListener mScrollListener = |
逻辑处理的入口在
onScrollStateChanged
方法中,当
newState == RecyclerView.SCROLL_STATE_IDLE
且滚动距离不等于0,触发
snapToTargetExistingView
方法;
SnapHelper
1 |
//移动到指定的已存在的View |
snapToTargetExistingView
方法顾名思义是移动到指定已存在的View的位置,
findSnapView
是查到目标的
SnapView
,
calculateDistanceToFinalSnap
是计算
SnapView
到最终位置的距离;由于
findSnapView
和
calculateDistanceToFinalSnap
是抽象方法,所以需要子类的具体实现;
整理一下
滚动状态
回调下,
SnapHelper
的实现流程图如下;
上文分析
SnapHelper
实现了
RecyclerView.OnFlingListener
接口,因此
Fling
的结果在
onFling()
方法中实现:
1 |
@Override |
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逻辑流程图如下
findSnapView(RecyclerView.LayoutManager layoutManager)
calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView)
findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY)
记住这三个方法,如果想玩转
SnapHelper
,掌握这个三分方法是迈出的第一步;
往往知道方法怎么用,却不知道代码怎么写,这是最困惑的,我们以
LinearSnapHelper
为例,从细节出发,分析自定义
SnapHelper
的常用思路和关键方法;
动代码前,先弄清这俩哥们到底解决了啥问题,首先
LinearSnapHelper
能够让线性排列的列表元素,最中间那颗元素居中显示;下图是
LinearSnapHelper
的效果展示之一;
LinearSnapHelper
1 |
public View findSnapView(RecyclerView.LayoutManager layoutManager) { |
首先,
findSnapView
中需要判断
RecyclerView
滚动的方向,然后拿到对应的
OrientationHelper
,最后通过
findCenterView
查找到
SnapView
并返回;
LinearSnapHelper
1 |
private View findCenterView(RecyclerView.LayoutManager layoutManager, |
findCenterView()方法是获取屏幕(RecyclerView控件)中间位置最近的那个View当做SnapView,计算的过程稍显复杂其实比较了然,具体注释在代码中标注,容易产生疑惑的是
OrientationHelper
下面一堆获取位置的方法,这里稍微总结一下:
OrientationHelper常见方法
总的来说
findCenterView
并不复杂,最迷惑人的是
OrientationHelper
的一堆API,在使用时稍加注意,也不是很复杂的;
LinearSnapHelper
1 |
public int[] calculateDistanceToFinalSnap( |
很幸运,
calculateDistanceToFinalSnap
并没有很复杂的代码,主要是计算方向,然后通过
OrientationHelper
计算第一步
findSnapView
得到的
SnapView
距离中间位置的距离;代码和第一步很相似,注释在代码中;
LinearSnapHelper
1 |
@Override |
计算通过惯性能滚动多少个子View的代码:
LinearSnapHelper
1 |
private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager, |
计算每个child的平均占用多少宽/高的代码如下:
LinearSnapHelper
1 |
private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager, |
LinearSnapHelper
的
findTargetSnapPosition
方法着实不简单,但是条理清晰逻辑严谨,考虑的比较周全,上面代码我做了比较详细的注释,相信肯定有同学不爱看代码,我也是,所以我用文字重新梳理一下上述代码逻辑和关键点;
findTargetSnapPosition
方法逻辑流程总结:
findSnapView()
活动当前的
centerView
;
ScrollVectorProvider
是否是reverseLayout,布局方向;
estimateNextPositionDiffForFling
方法获取该惯性能产生多少个子child的平移,或者理解成该惯性能让RecyclerView滚动多远个子child的距离;
centerView
下标,加上惯性产生的平移,计算出最终要落地的下标;
estimateNextPositionDiffForFling
方法逻辑流程总结:
calculateScrollDistance
计算惯性能滚动多远距离;
computeDistancePerChild
计算平均一个child占多大尺寸;
computeDistancePerChild
方法逻辑流程总结:
终于是把
LinearSnapHelper
的核心逻辑讲完了,纵观整个类,主要逻辑还是在
findTargetSnapPosition
这里,趁热打铁,我必须跟大家分享一下
PagerSnapHelper
是如何玩转这个方法的;
pagerSnapHelper
同样也实现了
SnapHelper
的三个方法,下面先看
findTargetSnapPosition
:
PagerSnapHelper
1 |
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, |
众所周知,
ViewPager
的翻页要么是保持不变,要么是下一页/上一页,上面
findTargetSnapPosition
方法就是主要的实现逻辑,其中判定是否翻页的条件由
forwardDirection
来控制,直接对比速度>0,用户想轻松滑到下一页是比较easy的,以至于上面代码量少到不敢相信;
至于
findSnapView
和
distanceToCenter
方法,同样是获取屏幕(RecyclerView)中间的View,计算
distanceToCenter
,跟
LinearSnapHelper
如出一辙;
PagerSnapHelper
设计之初是就是适用于一屏(RecyclerView范围内)显示单个
child
的,如果有一屏显示多个
child
的需求,
PagerSnapHelper
并不适用;其实在实际开发中这种需求还是挺多的,当然github上早已经有大神写过一个库,实现了几个常用的
SnapHelper
场景,
github传送门
;当然这个库并不能满足所有的需求,有机会再跟大家分享更有意义的
SnapHelper
实战;
陷阱:
一旦
calculateDistanceToFinalSnap()
返回值计算错误,有可能造成
RecyclerView
进入
smoothScroolBy
的魔鬼循环局面,直到滚动到头/尾才会结束;