前言
最近开发中遇到了一个需求,需要RecyclerView滚动到指定位置后置顶显示,当时遇到这个问题的时候,心里第一反应是直接使用RecyclerView的smoothScrollToPosition()方法,实现对应位置的平滑滚动。但是在实际使用中发现并没有到底自己想要的效果。本想着偷懒直接从网上Copy下,但是发现效果并不是很好。于是就自己去研究源码。
该系列文章分为两篇文章。
- 如果你想了解其内部实现,请观看本篇文章,
- 如果你想解决通过smoothScrollToPosition滚动到顶部,或者修改滚动加速,请观看RecyclerView滚动位置,滚动速度设置
什么是可见范围
在了解RecyclerView的smoothScrollToPosition方法之前,有个知识点,我觉得有必要给大家说一下,因为使用smoothScrollToPosition中遇到的问题都与可见范围有关。
这里所说的可见范围是,RecyclerView第一个可见item的位置与最后一个可见item的位置之间的范围。
一、实际使用中遇见的问题
如果当前滚动位置在可见范围内,是不会发生滚动的
当前RecyclerView的可见范围为0到9,当我们想要滚动到1位置时,发现当前RecyclerView并没有发生滚动。
二、如果当前滚动位置在可见范围之后,会滚动到底部
当前RecyclerView的可见范围为0到9,当我们想要滚动到10位置时,发现RecyclerView滚动了,且当前位置对应的视图在RecyclreView的底部。
三、如果当前滚动位置在可见范围之前,会滚动到顶部
这里我们滚动RecyclerView,使其可见范围为10到19,当我们分别滚动到1、3位置时,RecyclerView滚动了。且当前位置对应的视图在RecyclerView的顶部。
二、RecyclerView smoothScrollToPosition源码解析
到了这里我们发现对于不同情况,RecyclerView内部处理是不一样的,所以为了解决实际问题,看源码是必不可少的,接下来我们就一起跟着源码走一遍。来看看RecyclerView具体的滚动实现。(这里需要提醒大家的是这里我采用的是LinearLayoutManager,本文章都是基于LinearLayoutManager进行分析的)
1 | public void smoothScrollToPosition(int position) { |
mRecycler.smoothScrollToPosition()方法时,内部调用了LayoutManager的smoothScrollToPosition方法,LayoutManager中smoothScrollToPosition没有实现,具体实现在其子类中,这里我们使用的是LinearLayoutManager,所以我们来看看内部是怎么实现的。
1 |
|
这里我们可以看到,这里导致RecyclerView滑动的是LinearSmoothScroller,而LinearSmoothScroller的父类是RecyclerView.SmoothScroller,看到这里我相信大家都会感到一丝熟悉,因为我们在对控件内内容进行移动的时候,我们都会使用到一个类,那就是Scroller。这里RecyclerView也自定了一个滑动Scroller。肯定是与滑动其内部视图相关的。
1 | public void startSmoothScroll(SmoothScroller smoothScroller) { |
继续走startSmoothScroll,方法内部判断了如果正在计算坐标值就停止,然后调用start()方法重新开始计算坐标值。接着开始看start()方法。
1 | void start(RecyclerView recyclerView, LayoutManager layoutManager) { |
在start方法中,会标识当前scroller的执行状态,同时会根据滚动的位置去寻找对应的目标视图。这里需要着重提示一下,findViewByPosition()这个方法,该方法会在Recycler的可见范围内去查询是否有目标位置对应的视图,例如,现在RecyclerView的可见范围为1-9,目标位置为10,那么mTargetView =null,如果可见范围为9-20,目标位置为1,那么mTargetView =null。
最终调用RecyclerView的内部类 ViewFlinger的postOnAnimation()方法。
1 | class ViewFlinger implements Runnable { |
这里我们发现,ViewFlinger其实一个Runnable,在postOnAnimation()内部又将该Runnable发送出去了。那下面我们只用关心ViewFlinger的run()方法就行了。
1 |
|
ViewFlinger的run()方法内部实现比较复杂, 在该方法第一次执行的时候,会执行,if (scroller.computeScrollOffset()) ,其中scroller是ViewFlinger中的属性mScroller的引用,其中mScroller会在ViewFlinger创建对象的时候,就默认初始化了。那么第一次判断时候,因为还没有开始计算,所以不会进这个if语句块,那么接下来就会直接走下面的语句:
1 | if (smoothScroller != null) { |
最后发现,只是走了一个onAnimation(0,0),继续走该方法。
1 | private void onAnimation(int dx, int dy) { |
在onAnimation方法中,判断了目标视图是否为空,大家应该还记得上文中,我们对目标视图的查找。如果当前位置不在可见范围之内,那么mTargetView =null,就不回走对应的判断语句。继续查看onSeekTargetStep()。
1 | protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) { |
直接通过代码,发现并不理解改函数要做什么样的工作,这里我们只知道第一次发生滚动时,mInterimTargetDx=0与mInterimTargetDy =0,那么会走updateActionForInterimTarget()方法。
1 | protected void updateActionForInterimTarget(Action action) { |
根据官方文档进行翻译:当目标滚动位置对应视图不在RecyclerView的可见范围内,该方法计算朝向该视图的方向向量并触发平滑滚动。默认滚动的距离为12000(单位:px),(也就是说了为了滚动到目标位置,会让Recycler至多滚动12000个像素)。
既然该方法计算了时间,那么我们就看看calculateTimeForScrolling()方法,通过方法名我们就应该了解了该方法是计算给定距离在默认速度下需要滚动的时间。
1 | protected int calculateTimeForScrolling(int dx) { |
其中MILLISECONDS_PER_PX 会在LinearSmoothScroller初始化的时候创建。
1 | public LinearSmoothScroller(Context context) { |
查看calculateSpeedPerPixel()方法
1 | private static final float MILLISECONDS_PER_INCH = 25f;// 默认为移动一英寸需要花费25ms |
也就是说,当前滚动的速度是与屏幕的像素密度相关, 通过获取当前手机屏幕每英寸的像素密度,与每英寸移动所需要花费的时间,用每英寸移动所需要花费的时间除以像素密度就能计算出移动一个像素密度需要花费的时间。OK,既然我们已经算出了移动一个像素密度需要花费的时间,那么直接乘以像素,就能算出移动该像素所需要花费的时间了。
既然现在我们算出了时间,我们现在只用关心Action的update()方法到底是干什么的就好了,
1 | //保存关于SmoothScroller滑动距离信息 |
这里我们发现Action,只是存储关于SmoothScroller滑动信息的一个类,那么初始时保存了横向与竖直滑动的距离(12000px)、滑动时间,插值器。同时记录当前数据改变的状态。
现在我们已经把Action的onSeekTargetStep方法走完了,那接下来,我们继续看Action的runIfNecessary()方法。
1 | void runIfNecessary(RecyclerView recyclerView) { |
TNND,调来调去最后又把Action存储的信息传给了ViewFlinger的smoothScrollBy()方法。这里需要注意:一旦调用该方法会将mChanged置为false,下次再次进入该方法时,那么就不会调用ViewFlinger的滑动方法了。
1 | public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) { |
这里mScroller接受到Acttion传入的滑动信息开始滑动后。最后会调用postOnAnimation(),又将ViewFiinger的run()法发送出去。那么最终我们又回到了ViewFiinger的run()方法。
1 | public void run() { |
这里scroller(拿到之前Action传入的滑动距离信息)已经开始滑动了,故 if (scroller.computeScrollOffset()) 条件为true, 那么scroller拿到当前竖直方向的值就开始让RecyclerView滚动了,也就是代码 mLayout.scrollVerticallyBy(dy, mRecycler, mState);接着又让smoothScroller执行onAnimation()方法。其中传入的参数是RecyclerView已经滚动的距离。那我们现在继续看onAnimation方法。
1 | private void onAnimation(int dx, int dy) { |
那么现在代码就明了了,RecylerView会判断在滚动的时候,目标视图是否已经出现,如果没有出现,会调用onSeekTargetStep保存当前RecylerView滚动距离,然后判断RecyclerView是否需要滑动,然后又通过postOnAnimation()将ViewFlinger 发送出去了。那么直到找到目标视图才会停止。
那什么情况下,目标视图不为空呢,其实在RecylerView内部滚动的时候。会判断目标视图是否存在,如果存在会对mTargetView进行赋值操作。由于篇幅限制,这里就不对目标视图的查找进行介绍了,有兴趣的小伙伴可以自己看一下源码。
那接下来,我们就假如当前已经找到了目标视图,那么接下来程序会走onTargetFound()方法。
1 | protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { |
当目标视图被找到以后,会计算让目标视图出现在可见范围内,需要移动的横向与纵向距离。并计算所需要花费的时间。然后重新让RecyclerView滚动一段距离。
这里我们着重看calculateDyToMakeVisible。
1 | public int calculateDyToMakeVisible(View view, int snapPreference) { |
这里我们会根据当前view的top、bottom及当前布局的start、end等坐标信息,然后调用了calculateDtToFit()方法。现在最重要的出现了,也是我们那三个问题出现的原因!!
1 | public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int |
我们会根据snapPreference对应的值来计算相应的距离,同时snapPreference的具体值与getVerticalSnapPreference(这里我们是竖直方向)所以我们看该方法。
1 | protected int getVerticalSnapPreference() { |
其中mTargetVector与layoutManager.computeScrollVectorForPosition有关。
1 |
|
也就是说在LinerlayoutManager为竖直的情况下,snapPreference默认为SNAP_ANY,那么我们就可以得到,下面三种情况。
- 当滚动位置在可见范围之内时
boxStart - viewStart<=0
boxEnd - viewEnd>0
滚动距离为0,故不会滚动 - 当滚动位置在可见范围之前时
boxStart - viewStart> 0
那么实际滚动距离为正值,内容向上滚动,故只能滚动到顶部 - 当滚动位置在可见范围距离之外时
boxEnd - viewEnd<0
那么实际滚动距离为其差值,内容向下滚动,故只能滚动到底部
有可能大家现在看代码已经看晕了,下面我就用一张图来总结整个流程,结合流程图再去看代码,我相信大家能有更好的理解。