RecyclerView 原始碼分析(二) - RecyclerView的滑動機制
RecyclerView
作為一個列表View
,天生就可以滑動。作為一個使用者,我們可以不去了解它是怎麼進行滑動,但是我們作為一個學習原始碼的人,必須得知道RecyclerView
的滑動機制,所以,我們今天來看看RecyclerView
滑動部分的程式碼。
本文參考資料:
同時,從RecyclerView
的類結構上來看,我們知道RecyclerView
實現了NestedScrollingChild
介面,所以RecyclerView
也是一個可以產生滑動事件的View
。我相信大家都有用過CoordinatorLayout
和RecyclerView
這個組合,這其中原理的也是巢狀滑動。本文在介紹普通滑動中,可能會涉及到巢狀滑動的知識,所以在閱讀本文時,需要大家掌握巢狀滑動的機制,具體可以參考我上面的文章:Android 原始碼分析 - 巢狀滑動機制的實現原理
,此文專門從RecyclerView
的角度上來理解巢狀滑動的機制。
本文打算從如下幾個方面來分析RecyclerView
:
TouchEvent
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
快取機制在這個方法被淋漓盡致的體現出來。關於這個方法,如果不出意外的話,在下一篇文章裡面我們就可以接觸到,在這裡,先給大家賣一個關子:joy:。
最後就是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
實現方式來實現,是不是就覺得非常簡單呢?對的,這就是我們學習原始碼的目的,不僅要理解其中的原理,還需要學以致用:joy:。
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裡面去看看:joy:。
4. 總結
RecyclerView
的滑動機制相比較來說,還是非常簡單,我也感覺沒有什麼可以總結。不過從RecyclerView
的原始碼,我們可以學習兩點:
-
多指滑動。我們可以根據
RecyclerView
的原始碼,來實現自己的多指滑動,這是一種參考,也是學以致用 -
fling
滑動。RecyclerView
實現了fling
效果,在日常開發過程中,如果我們也需要實現這種效果,我們可以根據RecyclerView
的原始碼來實現。