同时,从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
的源码来实现。
作者:琼珶和予
链接:https://www.jianshu.com/p/c89c99736c05