RecyclerView 源碼分析(二) - RecyclerView的滑動(dòng)機(jī)制

??RecyclerView作為一個(gè)列表View,天生就可以滑動(dòng)萌丈。作為一個(gè)使用者克懊,我們可以不去了解它是怎么進(jìn)行滑動(dòng),但是我們作為一個(gè)學(xué)習(xí)源碼的人蒿讥,必須得知道RecyclerView的滑動(dòng)機(jī)制挥等,所以友绝,我們今天來看看RecyclerView滑動(dòng)部分的代碼。
??本文參考資料:

  1. Android 源碼分析 - 嵌套滑動(dòng)機(jī)制的實(shí)現(xiàn)原理
  2. 深入 RecyclerView 源碼探究三:繪制和滑動(dòng)

??同時(shí)肝劲,從RecyclerView的類結(jié)構(gòu)上來看迁客,我們知道RecyclerView實(shí)現(xiàn)了NestedScrollingChild接口,所以RecyclerView也是一個(gè)可以產(chǎn)生滑動(dòng)事件的View辞槐。我相信大家都有用過CoordinatorLayoutRecyclerView這個(gè)組合掷漱,這其中原理的也是嵌套滑動(dòng)。本文在介紹普通滑動(dòng)中催蝗,可能會(huì)涉及到嵌套滑動(dòng)的知識(shí)切威,所以在閱讀本文時(shí)育特,需要大家掌握嵌套滑動(dòng)的機(jī)制丙号,具體可以參考我上面的文章:Android 源碼分析 - 嵌套滑動(dòng)機(jī)制的實(shí)現(xiàn)原理,此文專門從RecyclerView的角度上來理解嵌套滑動(dòng)的機(jī)制缰冤。
??本文打算從如下幾個(gè)方面來分析RecyclerView

  1. 正常的TouchEvent
  2. 嵌套滑動(dòng)(穿插著文章各個(gè)地方犬缨,不會(huì)專門的講解)
  3. 多指滑動(dòng)
  4. fling滑動(dòng)

1. 傳統(tǒng)事件

??現(xiàn)在,我們正式分析源碼棉浸,首先我們來看看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;
    }

??如上就是RecyclerViewonTouchEvent方法,我大量的簡(jiǎn)化了這個(gè)方法迷郑,先讓大家對(duì)它的結(jié)構(gòu)有一個(gè)了解枝恋。
??其中ACTION_DOWN创倔、ACTION_MOVEACTION_UPACTION_CANCEL這幾個(gè)事件焚碌,我相信各位同學(xué)都比較熟悉畦攘,這是View最基本的事件。
??可能有人對(duì)ACTION_POINTER_DOWNACTION_POINTER_UP事件比較陌生十电,這兩個(gè)事件就跟多指滑動(dòng)有關(guān)知押,也是本文重點(diǎn)分析之一。


??好了鹃骂,我們現(xiàn)在開始正式分析源碼台盯。在分析源碼之前,我先將上面的代碼做一個(gè)簡(jiǎn)單的概述畏线。

  1. 如果當(dāng)前的mActiveOnItemTouchListener需要消耗當(dāng)前事件静盅,那么優(yōu)先交給它處理。
  2. 如果mActiveOnItemTouchListener不消耗當(dāng)前事件寝殴,那么就走正常的事件分發(fā)機(jī)制温亲。這里面有很多的細(xì)節(jié),稍后我會(huì)詳細(xì)的介紹杯矩。

??關(guān)于第一步栈虚,這里不用我來解釋,它就是一個(gè)Listener的回調(diào)史隆,非常的簡(jiǎn)單魂务,我們重點(diǎn)的在于分析第二步。

(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坐標(biāo)熔酷。
  2. 調(diào)用startNestedScroll方法孤紧,詢問父View是否處理事件。

??Down事件還是比較簡(jiǎn)單拒秘,通常來說就一些初始化的事情号显。
??接下來,我們來看看重頭戲--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;

??這部分代碼非常的簡(jiǎn)單躺酒,我將它分為如下幾步:

  1. 根據(jù)Move事件產(chǎn)生的x押蚤、y坐標(biāo)來計(jì)算dx、dy羹应。
  2. 調(diào)用dispatchNestedPreScroll詢問父View是否優(yōu)先處理滑動(dòng)事件揽碘,如果要消耗,dx和dy分別會(huì)減去父View消耗的那部分距離。
  3. 然后根據(jù)情況來判斷RecyclerView是垂直滑動(dòng)還是水平滑動(dòng)雳刺,最終是調(diào)用scrollByInternal方法來實(shí)現(xiàn)滑動(dòng)的效果的劫灶。
  4. 調(diào)用GapWorkerpostFromTraversal來預(yù)取ViewHolder。這個(gè)過程會(huì)走緩存機(jī)制部分的邏輯掖桦,同時(shí)也有可能會(huì)調(diào)用AdapteronBindViewHolder方法來提前加載數(shù)據(jù)浑此。

??其中第一步和第二步都是比較簡(jiǎn)單的,這里就直接省略滞详。
??而scrollByInternal方法也是非常的簡(jiǎn)單凛俱,在scrollByInternal方法內(nèi)部,實(shí)際上是調(diào)用了LayoutManagerscrollHorizontallyBy方法或者scrollVerticallyBy方法來實(shí)現(xiàn)的料饥。LayoutManager這兩個(gè)方法實(shí)際上也沒有做什么比較騷的操作蒲犬,歸根結(jié)底,最終調(diào)用了就是調(diào)用了每個(gè)ChildoffsetTopAndBottom或者offsetLeftAndRight方法來實(shí)現(xiàn)的岸啡,這里就不一一的跟蹤代碼了原叮,大家了解就行了。在本文的后面巡蘸,我會(huì)照著RecyclerView滑動(dòng)相關(guān)的代碼寫一個(gè)簡(jiǎn)單的Demo奋隶。
??在這里,我們就簡(jiǎn)單的分析一下GapWorker是怎么進(jìn)行預(yù)取的悦荒。我們來看看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方法內(nèi)部也沒有做多少事情唯欣,最核心在于調(diào)用了post方法,向任務(wù)隊(duì)列里面添加了一個(gè)Runnable搬味【城猓看來重點(diǎn)的分析還是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方法的邏輯也是非常簡(jiǎn)單,首先計(jì)算獲得下一幀的時(shí)間碰纬,然后調(diào)用prefetch方法進(jìn)行預(yù)取ViewHolder萍聊。

    void prefetch(long deadlineNs) {
        buildTaskList();
        flushTasksWithDeadline(deadlineNs);
    }

??prefetch方法也簡(jiǎn)單,顯示調(diào)用buildTaskList方法生成任務(wù)隊(duì)列悦析,然后調(diào)用flushTasksWithDeadline來執(zhí)行task,這其中會(huì)調(diào)用RecyclerViewtryGetViewHolderForPositionByDeadline方法來獲取一個(gè)ViewHolder寿桨,這里就不一一分析了。
??不過需要提一句的是强戴,tryGetViewHolderForPositionByDeadline方法是整個(gè)RecyclerView緩存機(jī)制的核心亭螟,RecyclerView緩存機(jī)制在這個(gè)方法被淋漓盡致的體現(xiàn)出來。關(guān)于這個(gè)方法酌泰,如果不出意外的話媒佣,在下一篇文章里面我們就可以接觸到匕累,在這里陵刹,先給大家賣一個(gè)關(guān)子??。
??最后就是Up事件和Cancel事件,這兩個(gè)事件更加的簡(jiǎn)單衰琐,都進(jìn)行一些清理的操作也糊,這里就不分析了。不過在Up事件里面羡宙,有一個(gè)特殊事件可能會(huì)產(chǎn)生--fling事件狸剃,待會(huì)我們會(huì)詳細(xì)的分析。

2. 多指滑動(dòng)

??大家千萬不會(huì)誤會(huì)這里多指滑動(dòng)的意思狗热,這里的多指滑動(dòng)不是指RecyclerView能夠相應(yīng)多根手指的滑動(dòng)钞馁,而是指當(dāng)一個(gè)手指還沒釋放時(shí),此時(shí)另一個(gè)手指按下匿刮,此時(shí)RecyclerView就不相應(yīng)上一個(gè)手指的手勢(shì)僧凰,而是相應(yīng)最近按下手指的手勢(shì)。
??我們來看看這部分的代碼:

            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;

??當(dāng)另一個(gè)手指按下時(shí)熟丸,此時(shí)就會(huì)立即更新按下的坐標(biāo)训措,同時(shí)會(huì)更新mScrollPointerId,表示后面只會(huì)響應(yīng)最近按下手指的手勢(shì)。
??其次光羞,我們來看看多指松開的情況:

            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);
        }
    }

??在這里也沒有比較騷的操作绩鸣,就是普通的更新。這里就不詳細(xì)的解釋了纱兑。本文后面會(huì)有一個(gè)小Demo呀闻,讓大家看看根據(jù)RecyclerView依葫蘆畫瓢做出來的效果。
??接下來潜慎,我們來最后一個(gè)滑動(dòng)总珠,也是本文最重點(diǎn)分析的滑動(dòng)--fling滑動(dòng)。為什么需要重點(diǎn)分析fling事件勘纯,因?yàn)樵谖覀兤匠W远xView,fling事件是最容易被忽視的局服。

3. fling滑動(dòng)

??我們先來看看fling滑動(dòng)產(chǎn)生的地方,也是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;

??從上面的代碼中驳遵,我們可以看出來淫奔,最終是調(diào)用fling方法來是實(shí)現(xiàn)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方法里面,顯示調(diào)用dispatchNestedPreFling方法詢問父View是否處理fling事件,最后調(diào)用ViewFlingerfling方法來實(shí)現(xiàn)fling效果,所以真正的核心在于ViewFlingerfling方法里面堤结,我們繼續(xù)來看:

        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方法里面,先是調(diào)用了OverScrollerfling來計(jì)算fling相關(guān)的參數(shù)唆迁,包括fling的距離和fling的時(shí)間。這里就不深入的分析計(jì)算相關(guān)的代碼竞穷,因?yàn)檫@里面都是一些數(shù)學(xué)和物理的計(jì)算唐责。最后就是調(diào)用了postOnAnimation方法。

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

??可能大家有可能看不懂上面的代碼瘾带,其實(shí)跟Viewpost差不多,所以最終還是得看ViewFlingerrun方法鼠哥。
??ViewFlingerrun方法比較長(zhǎng),這里我將它簡(jiǎn)化了一下:

        public void run() {
            // ······
            // 第一步,更新滾動(dòng)信息朴恳,并且判斷當(dāng)前是否已經(jīng)滾動(dòng)完畢
            // 為true表示未滾動(dòng)完畢
            if (scroller.computeScrollOffset()) {
                //······

                if (mAdapter != null) {
                    // ······
                    // 滾動(dòng)特定距離
                    if (dx != 0) {
                        hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                        overscrollX = dx - hresult;
                    }
                    if (dy != 0) {
                        vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
                        overscrollY = dy - vresult;
                    }
                    // ······
                }
                // ······
                // 如果滾動(dòng)完畢抄罕,就是調(diào)用finish方法;
                // 如果沒有滾動(dòng)完畢于颖,就調(diào)用postOnAnimation方法繼續(xù)遞歸
                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);
                    }
                }
            }
            // ······
        }

??整個(gè)fling核心就在這里呆贿,通過上面的三步,最終就是實(shí)現(xiàn)了fling的效果森渐,上面的注意已經(jīng)非常的清晰了做入,這里就不繼續(xù)分析了。
??我們分析了RecyclerViewfling事件同衣,有什么幫助呢母蛛?在日常的開發(fā)中,如果需要fling的效果乳怎,我們可以根據(jù)RecyclerView實(shí)現(xiàn)方式來實(shí)現(xiàn)彩郊,是不是就覺得非常簡(jiǎn)單呢?對(duì)的蚪缀,這就是我們學(xué)習(xí)源碼的目的秫逝,不僅要理解其中的原理,還需要學(xué)以致用??询枚。

4. Demo展示

??這里的demo不是很高大上的東西违帆,就是照著RecyclerView的代碼實(shí)現(xiàn)了一個(gè)多指滑動(dòng)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);
    }
  }
}

??相信經(jīng)過RecyclerView源碼的學(xué)習(xí)金蜀,對(duì)上面代碼的理解也不是難事刷后,所以這里我就不需要再解釋了。具體的效果渊抄,大家可以拷貝Android studio里面去看看??尝胆。

4. 總結(jié)

??RecyclerView的滑動(dòng)機(jī)制相比較來說,還是非常簡(jiǎn)單护桦,我也感覺沒有什么可以總結(jié)含衔。不過從RecyclerView的源碼,我們可以學(xué)習(xí)兩點(diǎn):

  1. 多指滑動(dòng)二庵。我們可以根據(jù)RecyclerView的源碼贪染,來實(shí)現(xiàn)自己的多指滑動(dòng),這是一種參考催享,也是學(xué)以致用
  2. fling滑動(dòng)杭隙。RecyclerView實(shí)現(xiàn)了fling效果,在日常開發(fā)過程中因妙,如果我們也需要實(shí)現(xiàn)這種效果痰憎,我們可以根據(jù)RecyclerView的源碼來實(shí)現(xiàn)票髓。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市信殊,隨后出現(xiàn)的幾起案子炬称,更是在濱河造成了極大的恐慌汁果,老刑警劉巖涡拘,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異据德,居然都是意外死亡鳄乏,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門棘利,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橱野,“玉大人,你說我怎么就攤上這事善玫∷” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵茅郎,是天一觀的道長(zhǎng)蜗元。 經(jīng)常有香客問我,道長(zhǎng)系冗,這世上最難降的妖魔是什么奕扣? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮掌敬,結(jié)果婚禮上惯豆,老公的妹妹穿的比我還像新娘。我一直安慰自己奔害,他們只是感情好楷兽,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著华临,像睡著了一般拄养。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上银舱,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天瘪匿,我揣著相機(jī)與錄音,去河邊找鬼寻馏。 笑死棋弥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诚欠。 我是一名探鬼主播顽染,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼漾岳,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了粉寞?” 一聲冷哼從身側(cè)響起尼荆,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎唧垦,沒想到半個(gè)月后捅儒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡振亮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坊秸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片麸祷。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖褒搔,靈堂內(nèi)的尸體忽然破棺而出阶牍,到底是詐尸還是另有隱情,我是刑警寧澤星瘾,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布走孽,位于F島的核電站,受9級(jí)特大地震影響死相,放射性物質(zhì)發(fā)生泄漏融求。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一算撮、第九天 我趴在偏房一處隱蔽的房頂上張望生宛。 院中可真熱鬧,春花似錦肮柜、人聲如沸陷舅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽莱睁。三九已至,卻和暖如春芒澜,著一層夾襖步出監(jiān)牢的瞬間仰剿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工痴晦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留南吮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓誊酌,卻偏偏與公主長(zhǎng)得像部凑,于是被迫代替她去往敵國(guó)和親露乏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容