RecyclerView
做爲一個列表View
,天生就能夠滑動。做爲一個使用者,咱們能夠不去了解它是怎麼進行滑動,可是咱們做爲一個學習源碼的人,必須得知道RecyclerView
的滑動機制,因此,咱們今天來看看RecyclerView
滑動部分的代碼。緩存
本文參考資料:bash
同時,從RecyclerView
的類結構上來看,咱們知道RecyclerView
實現了NestedScrollingChild
接口,因此RecyclerView
也是一個能夠產生滑動事件的View
。我相信你們都有用過CoordinatorLayout
和RecyclerView
這個組合,這其中原理的也是嵌套滑動。本文在介紹普通滑動中,可能會涉及到嵌套滑動的知識,因此在閱讀本文時,須要你們掌握嵌套滑動的機制,具體能夠參考我上面的文章:Android 源碼分析 - 嵌套滑動機制的實現原理,此文專門從RecyclerView
的角度上來理解嵌套滑動的機制。ide
本文打算從以下幾個方面來分析RecyclerView
:源碼分析
- 正常的
TouchEvent
- 嵌套滑動(穿插着文章各個地方,不會專門的講解)
- 多指滑動
- fling滑動
如今,咱們正式分析源碼,首先咱們來看看onTouchEvent
方法,來看看它爲咱們作了那些事情:post
@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最基本的事件。fetch
可能有人對ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件比較陌生,這兩個事件就跟多指滑動有關,也是本文重點分析之一。ui
好了,咱們如今開始正式分析源碼。在分析源碼以前,我先將上面的代碼作一個簡單的概述。this
- 若是當前的
mActiveOnItemTouchListener
須要消耗當前事件,那麼優先交給它處理。- 若是
mActiveOnItemTouchListener
不消耗當前事件,那麼就走正常的事件分發機制。這裏面有不少的細節,稍後我會詳細的介紹。
關於第一步,這裏不用我來解釋,它就是一個Listener
的回調,很是的簡單,咱們重點的在於分析第二步。spa
咱們先來看看這部分的代碼吧。
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事件
咱們先來看看這部分的代碼:
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事件,待會咱們會詳細的分析。
你們千萬不會誤會這裏多指滑動的意思,這裏的多指滑動不是指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
事件是最容易被忽視的。
咱們先來看看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
實現方式來實現,是否是就以爲很是簡單呢?對的,這就是咱們學習源碼的目的,不只要理解其中的原理,還須要學以至用😂。
這裏的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裏面去看看😂。
RecyclerView
的滑動機制相比較來講,仍是很是簡單,我也感受沒有什麼能夠總結。不過從RecyclerView
的源碼,咱們能夠學習兩點:
- 多指滑動。咱們能夠根據
RecyclerView
的源碼,來實現本身的多指滑動,這是一種參考,也是學以至用fling
滑動。RecyclerView
實現了fling
效果,在平常開發過程當中,若是咱們也須要實現這種效果,咱們能夠根據RecyclerView
的源碼來實現。