RecyclerView 源碼分析(二) - RecyclerView的滑動機制

  RecyclerView做爲一個列表View,天生就能夠滑動。做爲一個使用者,咱們能夠不去了解它是怎麼進行滑動,可是咱們做爲一個學習源碼的人,必須得知道RecyclerView的滑動機制,因此,咱們今天來看看RecyclerView滑動部分的代碼。緩存

  本文參考資料:bash

  1. Android 源碼分析 - 嵌套滑動機制的實現原理
  2. 深刻 RecyclerView 源碼探究三:繪製和滑動

  同時,從RecyclerView的類結構上來看,咱們知道RecyclerView實現了NestedScrollingChild接口,因此RecyclerView也是一個能夠產生滑動事件的View。我相信你們都有用過CoordinatorLayoutRecyclerView這個組合,這其中原理的也是嵌套滑動。本文在介紹普通滑動中,可能會涉及到嵌套滑動的知識,因此在閱讀本文時,須要你們掌握嵌套滑動的機制,具體能夠參考我上面的文章:Android 源碼分析 - 嵌套滑動機制的實現原理,此文專門從RecyclerView的角度上來理解嵌套滑動的機制。ide

  本文打算從以下幾個方面來分析RecyclerView源碼分析

  1. 正常的TouchEvent
  2. 嵌套滑動(穿插着文章各個地方,不會專門的講解)
  3. 多指滑動
  4. fling滑動

1. 傳統事件

  如今,咱們正式分析源碼,首先咱們來看看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;
    }
複製代碼

  如上就是RecyclerViewonTouchEvent方法,我大量的簡化了這個方法,先讓你們對它的結構有一個瞭解。學習

  其中ACTION_DOWNACTION_MOVEACTION_UPACTION_CANCEL這幾個事件,我相信各位同窗都比較熟悉,這是View最基本的事件。fetch

  可能有人對ACTION_POINTER_DOWNACTION_POINTER_UP事件比較陌生,這兩個事件就跟多指滑動有關,也是本文重點分析之一。ui


  好了,咱們如今開始正式分析源碼。在分析源碼以前,我先將上面的代碼作一個簡單的概述。this

  1. 若是當前的mActiveOnItemTouchListener須要消耗當前事件,那麼優先交給它處理。
  2. 若是mActiveOnItemTouchListener不消耗當前事件,那麼就走正常的事件分發機制。這裏面有不少的細節,稍後我會詳細的介紹。

  關於第一步,這裏不用我來解釋,它就是一個Listener的回調,很是的簡單,咱們重點的在於分析第二步。spa

(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;
複製代碼

  這裏主要是作了兩件事。

  1. 記錄下Down事件的x、y座標。
  2. 調用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;
複製代碼

  這部分代碼很是的簡單,我將它分爲以下幾步:

  1. 根據Move事件產生的x、y座標來計算dx、dy。
  2. 調用dispatchNestedPreScroll詢問父View是否優先處理滑動事件,若是要消耗,dx和dy分別會減去父View消耗的那部分距離。
  3. 而後根據狀況來判斷RecyclerView是垂直滑動仍是水平滑動,最終是調用scrollByInternal方法來實現滑動的效果的。
  4. 調用GapWorkerpostFromTraversal來預取ViewHolder。這個過程會走緩存機制部分的邏輯,同時也有可能會調用AdapteronBindViewHolder方法來提早加載數據。

  其中第一步和第二步都是比較簡單的,這裏就直接省略。

  而scrollByInternal方法也是很是的簡單,在scrollByInternal方法內部,其實是調用了LayoutManagerscrollHorizontallyBy方法或者scrollVerticallyBy方法來實現的。LayoutManager這兩個方法實際上也沒有作什麼比較騷的操做,歸根結底,最終調用了就是調用了每一個ChildoffsetTopAndBottom或者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。看來重點的分析仍是GapWorkerrun方法:

@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,這其中會調用RecyclerViewtryGetViewHolderForPositionByDeadline方法來獲取一個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事件,最後調用ViewFlingerfling方法來實現fling效果,因此真正的核心在於ViewFlingerfling方法裏面,咱們繼續來看:

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();
        }
複製代碼

  在ViewFlingerfling方法裏面,先是調用了OverScrollerfling來計算fling相關的參數,包括fling的距離和fling的時間。這裏就不深刻的分析計算相關的代碼,由於這裏面都是一些數學和物理的計算。最後就是調用了postOnAnimation方法。

void postOnAnimation() {
            if (mEatRunOnAnimationRequest) {
                mReSchedulePostAnimationCallback = true;
            } else {
                removeCallbacks(this);
                ViewCompat.postOnAnimation(RecyclerView.this, this);
            }
        }
複製代碼

  可能你們有可能看不懂上面的代碼,其實跟Viewpost差很少,因此最終仍是得看ViewFlingerrun方法。

  ViewFlingerrun方法比較長,這裏我將它簡化了一下:

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的效果,上面的注意已經很是的清晰了,這裏就不繼續分析了。

  咱們分析了RecyclerViewfling事件,有什麼幫助呢?在平常的開發中,若是須要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的源碼,咱們能夠學習兩點:

  1. 多指滑動。咱們能夠根據RecyclerView的源碼,來實現本身的多指滑動,這是一種參考,也是學以至用
  2. fling滑動。RecyclerView實現了fling效果,在平常開發過程當中,若是咱們也須要實現這種效果,咱們能夠根據RecyclerView的源碼來實現。
相關文章
相關標籤/搜索