很久之前写过了几篇android滚动原理的文章,在读本篇博文之前,可以先阅读下博主以下的几篇博文:
《View的滚动原理简单解析》
《View的滚动原理简单解析(二)》
《ViewDragHelper的简单分析(一)》
《ViewDragHelper的简单分析及应用(二)》
因为本篇博文会用到上面几篇博客的一些知识点。
本篇是对RecycleView源码解读的开篇,如题所述,准备对其滚动原理来简单分析下,当然也会涉及到RecycleView的其他知识点。
最近看周围的一些朋友在玩抖音,其首页加载的模式不像RecycleView那样随手指的滑动在抬起手指的一刹那,还会有fling动作连续滚动好几页;而抖音的滑动是一页一页的滑动,当滑动到最新页面的时候视频自动播放。
其实这个功能用RecycleView也能实现,但是要对滑动原理要有一定的了解,废话说了这么多,开始发车。
通过文章开头的四篇博客我们知道实现View滚动的方式有四种:
1)调用View的layout方法,设置View的布局位置
2)修改View的layoutParam参数(结合属性动画,效果刚刚滴)
3)ParentView调用scrollTo/scrollBy方法改动childView的位置
4)调用View的offsetLeftAndRight和offsetTopAndBottom方法来实现竖屏或者竖直方向的滚动(详细说明见《ViewDragHelper的简单分析(一)》)。
那么在RecycleView里面实现item滚动的方式什么呢?通过阅读源码发现用的就是offsetLeftAndRight/offsetTopAndBottom!
滚动的过程说起来也很简单:就是手指的三种动作而作出相应的相应而已:在RecyclerView的onTouchEvent里面
ACTION_DOWN: 初始化手指按下的位置。
ACTION_MOVE:随着手指的移动调用scrollByInternal方法实现滚动,并且调用相关的监听通知RecyclerView的ScorllListener
ACTION_UP: 手指抬起时,随着滚动的惯性调用fling方法处理惯性运动。在这里需要注意的是RevyclerView提供了mOnFlingListener,如果客户端配置了这个监听,并且该监听的fling方法返回true,那么处理惯性运动就交给客户端自己处理,否则就交给RecyclerView自己处理:
//客户端(程序员)自己处理惯性运动
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));
//RecyclerView自己处理惯性运动
mViewFlinger.fling(velocityX, velocityY);
return true;
}
所以为了使得RecyclerView在抬起手指的时候不连续滚动,就调用RecyclerView的setOnFlingListener方法设置mOnFlingListener并将mOnFlingListener的onFling返回true即可。到这里距离抖音首页的效果在思路上迈出了一小步。
RecyclerView的onTouchEvent通过上面的说明,关于滚动的大致逻辑如下:
public boolean onTouchEvent(MotionEvent e) {
//省略部与本文无关的分代码
//根据布局的方向来决定滚动的方向
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
//省略部与本文无关的分代码
switch (action) {
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
//省略部分代码
} break;
//省略部与本文无关的分代码
case MotionEvent.ACTION_MOVE: {
//省略部与本文无关的分代码
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
//计算滚动的距离
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
//省略部与本文无关的分代码
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//真正处理滚动的逻辑
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
// 省略部与本文无关的分代码
case MotionEvent.ACTION_UP: {
//省略部与本文无关的分代码
//处理惯性滚动
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
//省略部与本文无关的分代码
} break;
//省略部与本文无关的分代码
}
//省略部与本文无关的分代码
//默认返回true说明默认由RecycleView来处理事件
return true;
}
处理滚动的时候当然要知道滚动的方向,我们知道RecyclerView是可以水平和竖直布局的,且设置水平或者竖直布局的时候要调用如下方法(以竖直方向为例):
setLayoutManager(LinearLayoutManager(contentView.context, LinearLayoutManager.VERTICAL, false))
以线性布局LinearLayoutManager为例:
public boolean canScrollHorizontally() {
return mOrientation == HORIZONTAL;
}
@Override
public boolean canScrollVertically() {
return mOrientation == VERTICAL;
}
如果配置了mOrientation为VERTICAL的则只能竖直滚动。
然后就是在ACTION_MOVE里计算手指移动的距离,然后调用scrollByInternal方法让item随着手指的移动而滚动。所以核心方法是scrollByInternal,让我们看看这个方法都做了些什么:
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;
//省略与本文无关的代码
if (mAdapter != null) {
//省略与本文无关的代码
//实现水平滚动
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
//实现竖直滚动
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
//省略与本文无关的代码
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
//省略与本文无关的代码
// 滚动回调
if (consumedX != 0 || consumedY != 0) {
//调用dispatchOnScrolled
dispatchOnScrolled(consumedX, consumedY);
}
//省略与本文无关的代码
return consumedX != 0 || consumedY != 0;
}
上面的代码从本文的角度来说主要作了两件事:
1、调用mLayout对象的scrollHorizontallyBy/scrollVerticallyBy实现了RecyclerView的滚动(注:mLayout为LayoutManager对象,此处以竖直滚动为例)
2、调用dispatchOnScrolled方法来通知客户端的滚动监听
简单分析下scrollVerticallyBy方法:
本篇博文以竖直滚动为例,来分析分析scrollVerticallyBy方法,而地球人都知道竖直滚动的mLayout引用对象就是LinearLayoutManager。所以直接进入LinearLayoutManager的LinearLayoutManager方法观察下:
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
//如果RecyclerView是水平布局,则竖直不滚动
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
如果是水平布局的话,则不滚动;否则调用scrollBy方法实现(竖直)滚动:
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
//省略部分无关代码
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
//调用mOrientationHelper的offsetChildren实现滚动
mOrientationHelper.offsetChildren(-scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
可以看出scrollBy方法对滚动距离稍作处理后,就调用了mOrientationHelper的offsetChildren来实现滚动的。在进入这个方法之前,现总结下上面的所讲的东西:
1、RecyclerView通过事件处理机制,在处理ACTION_MOVE的时候开始item的滚动
2、通过设置LayoutManager来决定滚动的方向
3、在处理ACTION_UP的时候通过fling方法处理惯性滚动
下面再来分析mOrientationHelper的offsetChildren方法,mOrientationHelper是通过OrientationHelper的createOrientationHelper方法来完成初始化的:
public static OrientationHelper createOrientationHelper(....) {
switch (orientation) {
case HORIZONTAL:
return createHorizontalHelper(layoutManager);
case VERTICAL:
return createVerticalHelper(layoutManager);
}
}
因为分析的是竖直滚动,所以看看createVerticalHelper这个方法返回的OrientationHelper对象,其offsetChild都做了啥:
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
public void offsetChildren(int amount) {
mLayoutManager.offsetChildrenVertical(amount);
}
//省略与本文无关的代码
};
}
所以仅需进入LayoutManager 的offsetChildrenVertical方法:
public void offsetChildrenVertical(int dy) {
if (mRecyclerView != null) {
mRecyclerView.offsetChildrenVertical(dy);
}
}
绕来绕去,最终LayoutManger还是要调用RecyclerView的offsetChildrenVertical方法:
public void offsetChildrenVertical(int dy) {
//1、获取RecyclerView的itemview的个数
final int childCount = mChildHelper.getChildCount();
//2,循环itemview,然后调用view的offsetTopAndBottom方法进行滚动
for (int i = 0; i < childCount; i++) {
mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
}
}
最终发现还是RecyclerView是通过循环遍历itemView的offsetTopAndBottom来实现竖直滚动的。(关于offsetTopAndBottom的详细说明,详细说明见《ViewDragHelper的简单分析(一)》,本篇不在赘述)
另外,在随着手指滚动的时候可以设置对滚动的监听,在scrollByInternal()方法中,处理好滚动之后有这么一段:
// 滚动回调
if (consumedX != 0 || consumedY != 0) {
//调用dispatchOnScrolled
dispatchOnScrolled(consumedX, consumedY);
}
dispatchOnScrolled()方法就是调用滚动监听的:
void dispatchOnScrolled(int hresult, int vresult) {
final int scrollX = getScrollX();
final int scrollY = getScrollY();
/*调用超类View的onScrollChanged方法*
onScrollChanged(scrollX, scrollY, scrollX, scrollY);
//RecyclerView的空方法,可以有其子类实现
onScrolled(hresult, vresult);
//通知相关监听对象,告知滚动的水平/竖直距离
if (mScrollListener != null) {
mScrollListener.onScrolled(this, hresult, vresult);
}
if (mScrollListeners != null) {
for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
mScrollListeners.get(i).onScrolled(this, hresult, vresult);
}
}
}
需要注意的是mScrollListener接口不仅仅可以通知客户端RecyclerView的滚动距离,该接口还提供了另外一个方法onScrollStateChanged,来让客户端监听滚动的状态:
SCROLL_STATE_IDLE:RecyclerView不再滚动或者停止滚动
SCROLL_STATE_DRAGGING:RecyclerView随着手指拖动而滚动的状态
SCROLL_STATE_SETTLING:RecyclerView随着手指的离开而发生惯性滚动状态。
当然,不再滚动的状态是在fling方法之后,也就是惯性运动结束之后才设置的:
//fling方法处理手指抬起后的惯性运动
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
//惯性运动结束后,设置滚动状态为停止滚动
setScrollState(SCROLL_STATE_IDLE);
}
在setScrollState方法里,当滚动状态发生改变的时候,会调用dispatchOnScrollStateChange方法:
void setScrollState(int state) {
/*状态没有发生改变*/
if (state == mScrollState) {
return;
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
stopScrollersInternal();
}
/*调用相关监听方法,通知滚动状态发生改变*/
dispatchOnScrollStateChanged(state);
}
/*调用相关监听方法,通知滚动状态发生改变*/
void dispatchOnScrollStateChanged(int state) {
//1、可以用LayoutManager的onScrollStateChanged方法监听
if (mLayout != null) {
mLayout.onScrollStateChanged(state);
}
//2、可以重写RecyclerView的onScrollStateChanged方法监听滚动状态
onScrollStateChanged(state);
//3、设置监听接口OnScrollListener来监听
if (mScrollListener != null) {
mScrollListener.onScrollStateChanged(this, state);
}
if (mScrollListeners != null) {
for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
mScrollListeners.get(i).onScrollStateChanged(this, state);
}
}
}
观察dispatchOnScrollStateChanged发现,如果我们想知道滚动状态的改变,可以有三个方法:
1、自定义LayoutManager,重写其onScrollStateChanged方法。
2、自定义RecyclerView,重写其onScrollStateChanged方法。
3、设置RecyclerView的滚动监听OnScrollListener
所以如果在使用RecyclerView的时候打算在RecyclerView滚动停止的时候做写什么,方法之一可以在ScrollListener的onScrollStateChanged这么判断:
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
//newState==SCROLL_STATE_IDLE表示滚动停止
if (newState == SCROLL_STATE_IDLE) {
}
}
到此位置只讲了滚动的两个状态:停止状态和拖动状态,还有一个惯性运动状态没有说明,本篇博文的后半部分就是对此进行简单的解析:
上面说到在onTouchEvent处理ACTION_UP的时候,核心代码如下:
case MotionEvent.ACTION_UP: {
//省略部分代码
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
//滚动结束
setScrollState(SCROLL_STATE_IDLE);
}
} break;
ACTION_UP事件主要作了两件事:
1、调用fling方法处理惯性运动
2、惯性运动结束后设置状态为SCROLL_STATE_IDLE,通知相关监听
按照惯例进入fling方法:
public boolean fling(int velocityX, int velocityY) {
//省略部分代码
if (!dispatchNestedPreFling(velocityX, velocityY)) {
//省略部分代码
//返回true的情况下,由客户端按照需求自己处理惯性运动
//比如实现类似抖音的一页页滚动
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
//RecyclerView自己处理惯性运动
if (canScroll) {
//计算水平和竖直方向惯性运动的初始速度
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
//开始惯性滚动
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
fling方法主要是先判断有没有惯性速度,有的话则处理的方式有两种:
1、客户端自己处理惯性运动,方式是通过客户端设置OnFlingListener并且其fling的方法返回true,就是说RecyclerView优先处理客户端自己的惯性运动,然后在决定是否有本身继续处理惯性运动
2、由RecyclerView自己处理惯性运动。调用mViewFlinger的fling方法。
mViewFlinger是RecyclerView的一个内部类ViewFlinger,是一个Runnable,其在RecyclerView初始化的时候就得到初始化,该对象内部持有一个ScrollerCompat的引用mScroller:
private int mLastFlingX;
private int mLastFlingY;
private ScrollerCompat mScroller;
public ViewFlinger() {
mScroller = ScrollerCompat.create(getContext(), sQuinticInterpolator);
}
继续进入ViewFlinger的fling方法,分析研究源码就这样,总是在各种方法里面来回杀进杀出:
private ScrollerCompat mScroller;
public void fling(int velocityX, int velocityY) {
//改变滚动状态为惯性运动状态
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
//开始惯性滚动
mScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
//该方法会回调ViewFlinger的run方法
postOnAnimation();
}
fling方法主要调用了mScroller的fling方法,但是只调用一次肯定不会有平滑惯性运动的效果,所以继续调用了postOnAnimation方法,该方法会继续几经辗转会继续调用Flinger的run方法:
public void run() {
//省略部分代码
final ScrollerCompat scroller = mScroller;
final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
//如果滚动没结束
if (scroller.computeScrollOffset()) {
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
//计算滚动的距离
final int dx = x - mLastFlingX;
final int dy = y - mLastFlingY;
int hresult = 0;
int vresult = 0;
mLastFlingX = x;
mLastFlingY = y;
int overscrollX = 0, overscrollY = 0;
if (mAdapter != null) {
//省略部分代码
if (dx != 0) {
//水平滚动
hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
overscrollX = dx - hresult;
}
if (dy != 0) {
//竖直滚动
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
//省略部分代码
}
//省略部分代码
//回调客户端滚动监听
if (hresult != 0 || vresult != 0) {
dispatchOnScrolled(hresult, vresult);
}
//省略部分代码
if (scroller.isFinished() || !fullyConsumedAny) {
//滚动结束
setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
//省略部分代码
} else {
//继续回调run方法
postOnAnimation();
}
}
//省略部分代码
}
其实上面的代码总体思路也很简单,就是通过判断computeScrollOffset的方法是否返回true来判断滚动是否结束,如果没结束继续调用postOnAnimation方法继续回调run方法。在此不再赘述,具体的思路可参考博主的《View的滚动原理简单解析(二)》,思路是一摸一样的。
到此为止,RecyclerView的滚动原理已经写完。总的思路无非就是博主博文开头列的几篇讲解滚动博文的应用。了解后简单的分析下RecyclerView的滚动原理相关代码其实也没啥难度。
如果错误之处,欢迎批评指正,共同学习