RecyclerView
作为一个列表View
,天生就可以滑动。作为一个使用者,我们可以不去了解它是怎么进行滑动,但是我们作为一个学习源码的人,必须得知道RecyclerView
的滑动机制,所以,我们今天来看看RecyclerView
滑动部分的代码。
本文参考资料:
同时,从RecyclerView
的类结构上来看,我们知道RecyclerView
实现了NestedScrollingChild
接口,所以RecyclerView
也是一个可以产生滑动事件的View
。我相信大家都有用过CoordinatorLayout
和RecyclerView
这个组合,这其中原理的也是嵌套滑动。本文在介绍普通滑动中,可能会涉及到嵌套滑动的知识,所以在阅读本文时,需要大家掌握嵌套滑动的机制,具体可以参考我上面的文章:Android 源码分析 - 嵌套滑动机制的实现原理,此文专门从RecyclerView
的角度上来理解嵌套滑动的机制。
本文打算从如下几个方面来分析RecyclerView
:
- 正常的
TouchEvent
- 嵌套滑动(穿插着文章各个地方,不会专门的讲解)
- 多指滑动
- fling滑动
1. 传统事件
现在,我们正式分析源码,首先我们来看看onTouchEvent
方法,来看看它为我们做了那些事情:
@Override
public boolean onTouchEvent(MotionEvent e) {
// ······
if (dispatchOnItemTouch(e)) {
cancelTouch();
return true;
}
// ······
switch (action) {
case MotionEvent.ACTION_DOWN: {
// ······
} break;
case MotionEvent.ACTION_POINTER_DOWN: {
// ······
} break;
case MotionEvent.ACTION_MOVE: {
// ······
} break;
case MotionEvent.ACTION_POINTER_UP: {
// ······
} break;
case MotionEvent.ACTION_UP: {
// ······
} break;
case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}
// ······
return true;
}
如上就是RecyclerView
的onTouchEvent
方法,我大量的简化了这个方法,先让大家对它的结构有一个了解。
其中ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
和ACTION_CANCEL
这几个事件,我相信各位同学都比较熟悉,这是View最基本的事件。
可能有人对ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件比较陌生,这两个事件就跟多指滑动有关,也是本文重点分析之一。
好了,我们现在开始正式分析源码。在分析源码之前,我先将上面的代码做一个简单的概述。
- 如果当前的
mActiveOnItemTouchListener
需要消耗当前事件,那么优先交给它处理。- 如果
mActiveOnItemTouchListener
不消耗当前事件,那么就走正常的事件分发机制。这里面有很多的细节,稍后我会详细的介绍。
关于第一步,这里不用我来解释,它就是一个Listener
的回调,非常的简单,我们重点的在于分析第二步。
(1). Down 事件
我们先来看看这部分的代码吧。
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
这里主要是做了两件事。
- 记录下Down事件的x、y坐标。
- 调用
startNestedScroll
方法,询问父View
是否处理事件。
Down
事件还是比较简单,通常来说就一些初始化的事情。
接下来,我们来看看重头戏--move事件
(2). Move事件
我们先来看看这部分的代码:
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
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 (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
if (dx > 0) {
dx -= mTouchSlop;
} else {
dx += mTouchSlop;
}
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
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;
这部分代码非常的简单,我将它分为如下几步:
- 根据Move事件产生的x、y坐标来计算dx、dy。
- 调用
dispatchNestedPreScroll
询问父View
是否优先处理滑动事件,如果要消耗,dx和dy分别会减去父View
消耗的那部分距离。- 然后根据情况来判断
RecyclerView
是垂直滑动还是水平滑动,最终是调用scrollByInternal
方法来实现滑动的效果的。- 调用
GapWorker
的postFromTraversal
来预取ViewHolder
。这个过程会走缓存机制部分的逻辑,同时也有可能会调用Adapter
的onBindViewHolder
方法来提前加载数据。
其中第一步和第二步都是比较简单的,这里就直接省略。
而scrollByInternal
方法也是非常的简单,在scrollByInternal
方法内部,实际上是调用了LayoutManager
的scrollHorizontallyBy
方法或者scrollVerticallyBy
方法来实现的。LayoutManager
这两个方法实际上也没有做什么比较骚的操作,归根结底,最终调用了就是调用了每个Child
的offsetTopAndBottom
或者offsetLeftAndRight
方法来实现的,这里就不一一的跟踪代码了,大家了解就行了。在本文的后面,我会照着RecyclerView
滑动相关的代码写一个简单的Demo。
在这里,我们就简单的分析一下GapWorker
是怎么进行预取的。我们来看看postFromTraversal
方法:
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
if (recyclerView.isAttachedToWindow()) {
if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("attempting to post unregistered view!");
}
if (mPostTimeNs == 0) {
mPostTimeNs = recyclerView.getNanoTime();
recyclerView.post(this);
}
}
recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
在postFromTraversal
方法内部也没有做多少事情,最核心在于调用了post
方法,向任务队列里面添加了一个Runnable
。看来重点的分析还是GapWorker
的run
方法:
@Override
public void run() {
try {
TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
if (mRecyclerViews.isEmpty()) {
// abort - no work to do
return;
}
// Query most recent vsync so we can predict next one. Note that drawing time not yet
// valid in animation/input callbacks, so query it here to be safe.
final int size = mRecyclerViews.size();
long latestFrameVsyncMs = 0;
for (int i = 0; i < size; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
}
}
if (latestFrameVsyncMs == 0) {
// abort - either no views visible, or couldn't get last vsync for estimating next
return;
}
long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
prefetch(nextFrameNs);
// TODO: consider rescheduling self, if there's more work to do
} finally {
mPostTimeNs = 0;
TraceCompat.endSection();
}
}
run
方法的逻辑也是非常简单,首先计算获得下一帧的时间,然后调用prefetch
方法进行预取ViewHolder
。
void prefetch(long deadlineNs) {
buildTaskList();
flushTasksWithDeadline(deadlineNs);
}
prefetch
方法也简单,显示调用buildTaskList
方法生成任务队列,然后调用flushTasksWithDeadline
来执行task
,这其中会调用RecyclerView
的tryGetViewHolderForPositionByDeadline
方法来获取一个ViewHolder
,这里就不一一分析了。
不过需要提一句的是,tryGetViewHolderForPositionByDeadline
方法是整个RecyclerView
缓存机制的核心,RecyclerView
缓存机制在这个方法被淋漓尽致的体现出来。关于这个方法,如果不出意外的话,在下一篇文章里面我们就可以接触到,在这里,先给大家卖一个关子😂。
最后就是Up事件和Cancel事件,这两个事件更加的简单,都进行一些清理的操作,这里就不分析了。不过在Up事件里面,有一个特殊事件可能会产生--fling事件,待会我们会详细的分析。
2. 多指滑动
大家千万不会误会这里多指滑动的意思,这里的多指滑动不是指RecyclerView
能够相应多根手指的滑动,而是指当一个手指还没释放时,此时另一个手指按下,此时RecyclerView
就不相应上一个手指的手势,而是相应最近按下手指的手势。
我们来看看这部分的代码:
case MotionEvent.ACTION_POINTER_DOWN: {
mScrollPointerId = e.getPointerId(actionIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
当另一个手指按下时,此时就会立即更新按下的坐标,同时会更新mScrollPointerId
,表示后面只会响应最近按下手指的手势。
其次,我们来看看多指松开的情况:
case MotionEvent.ACTION_POINTER_UP: {
onPointerUp(e);
} break;
private void onPointerUp(MotionEvent e) {
final int actionIndex = e.getActionIndex();
if (e.getPointerId(actionIndex) == mScrollPointerId) {
// Pick a new pointer to pick up the slack.
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = e.getPointerId(newIndex);
mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
}
}
在这里也没有比较骚的操作,就是普通的更新。这里就不详细的解释了。本文后面会有一个小Demo,让大家看看根据RecyclerView
依葫芦画瓢做出来的效果。
接下来,我们来最后一个滑动,也是本文最重点分析的滑动--fling滑动。为什么需要重点分析fling事件,因为在我们平常自定义View
,fling
事件是最容易被忽视的。
3. fling滑动
我们先来看看fling
滑动产生的地方,也是Up事件的地方:
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
从上面的代码中,我们可以看出来,最终是调用fling
方法来是实现fling
效果的,我们来看看fling
方法:
public boolean fling(int velocityX, int velocityY) {
// ······
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
return true;
}
if (canScroll) {
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontal) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertical) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
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
方法里面,显示调用dispatchNestedPreFling
方法询问父View
是否处理fling
事件,最后调用ViewFlinger
的fling
方法来实现fling
效果,所以真正的核心在于ViewFlinger
的fling
方法里面,我们继续来看:
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);
postOnAnimation();
}
在ViewFlinger
的fling
方法里面,先是调用了OverScroller
的fling
来计算fling
相关的参数,包括fling
的距离和fling
的时间。这里就不深入的分析计算相关的代码,因为这里面都是一些数学和物理的计算。最后就是调用了postOnAnimation
方法。
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
removeCallbacks(this);
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}
可能大家有可能看不懂上面的代码,其实跟View
的post
差不多,所以最终还是得看ViewFlinger
的run
方法。
ViewFlinger
的run
方法比较长,这里我将它简化了一下:
public void run() {
// ······
// 第一步,更新滚动信息,并且判断当前是否已经滚动完毕
// 为true表示未滚动完毕
if (scroller.computeScrollOffset()) {
//······
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;
}
// ······
}
// ······
// 如果滚动完毕,就是调用finish方法;
// 如果没有滚动完毕,就调用postOnAnimation方法继续递归
if (scroller.isFinished() || (!fullyConsumedAny
&& !hasNestedScrollingParent(TYPE_NON_TOUCH))) {
// setting state to idle will stop this.
setScrollState(SCROLL_STATE_IDLE);
if (ALLOW_THREAD_GAP_WORK) {
mPrefetchRegistry.clearPrefetchPositions();
}
stopNestedScroll(TYPE_NON_TOUCH);
} else {
postOnAnimation();
if (mGapWorker != null) {
mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
}
}
}
// ······
}
整个fling
核心就在这里,通过上面的三步,最终就是实现了fling的效果,上面的注意已经非常的清晰了,这里就不继续分析了。
我们分析了RecyclerView
的fling
事件,有什么帮助呢?在日常的开发中,如果需要fling
的效果,我们可以根据RecyclerView
实现方式来实现,是不是就觉得非常简单呢?对的,这就是我们学习源码的目的,不仅要理解其中的原理,还需要学以致用😂。
4. Demo展示
这里的demo不是很高大上的东西,就是照着RecyclerView
的代码实现了一个多指滑动View而已。我们来看看源码:
public class MoveView extends View {
private int mLastTouchX;
private int mLastTouchY;
private int mTouchSlop;
private boolean mCanMove;
private int mScrollPointerId;
public MoveView(Context context) {
this(context, null);
}
public MoveView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int actionIndex = event.getActionIndex();
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
mScrollPointerId = event.getPointerId(0);
mLastTouchX = (int) (event.getX() + 0.5f);
mLastTouchY = (int) (event.getY() + 0.5f);
mCanMove = false;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mScrollPointerId = event.getPointerId(actionIndex);
mLastTouchX = (int) (event.getX(actionIndex) + 0.5f);
mLastTouchY = (int) (event.getY(actionIndex) + 0.5f);
break;
case MotionEvent.ACTION_MOVE:
final int index = event.findPointerIndex(mScrollPointerId);
int x = (int) (event.getX(index) + 0.5f);
int y = (int) (event.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if(!mCanMove) {
if (Math.abs(dy) >= mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
mCanMove = true;
}
if (Math.abs(dy) >= mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
mCanMove = true;
}
}
if (mCanMove) {
offsetTopAndBottom(-dy);
offsetLeftAndRight(-dx);
}
break;
case MotionEvent.ACTION_POINTER_UP:
onPointerUp(event);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
private void onPointerUp(MotionEvent e) {
final int actionIndex = e.getActionIndex();
if (e.getPointerId(actionIndex) == mScrollPointerId) {
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = e.getPointerId(newIndex);
mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
}
}
}
相信经过RecyclerView
源码的学习,对上面代码的理解也不是难事,所以这里我就不需要再解释了。具体的效果,大家可以拷贝Android studio里面去看看😂。
4. 总结
RecyclerView
的滑动机制相比较来说,还是非常简单,我也感觉没有什么可以总结。不过从RecyclerView
的源码,我们可以学习两点:
- 多指滑动。我们可以根据
RecyclerView
的源码,来实现自己的多指滑动,这是一种参考,也是学以致用fling
滑动。RecyclerView
实现了fling
效果,在日常开发过程中,如果我们也需要实现这种效果,我们可以根据RecyclerView
的源码来实现。