天天看點

【進階android】ListView源碼分析——ListView的滾動機制1、滑動相關的常量、變量及接口2、down——滑動開始前的準備3、move——滑動開始與持續4、up——滑動結束與後序5、ListView的抛動機制6、ListView滾動機制的總結

        前面幾篇文章主要在介紹ListView的初始化(當然這些方法并不僅僅隻在ListView執行個體化時被調用),這一篇文章我們則主要分析ListView在運動時的情況,即ListView的滾動機制。滾動機制主要分為ListView是如何滾動以及滾動時會引起什麼東西變化。

        ListView的滾動機制與ListView的觸摸事件息息相關,是以了解其滾動機制就是了解ListView對觸摸事件是如何解析、控制的。衆所周知,觸摸事件分為三個比較主要的動作:down、move、up,本文将按照這個流程來對ListView的滾動機制進行分析,并且在具體分析觸摸事件之前,會對ListView的一些與滑動相關的常量、變量及接口做一些解析。是以,本文的目錄如下:

       1、與滑動相關的常量、變量及接口;

       2、down——滑動開始前的準備;

       3、move——滑動開始與持續;

       4、up——滑動結束與後序;

       5、ListView的抛動機制

1、滑動相關的常量、變量及接口

    1.1描述滑動狀态的常量

            ListView,為一個滾動機制定義了一系列的觸摸模式,每一個觸摸模式下的滑動都呈現出不同的效果;ListView中,将滾動機制定位為8種不同的觸摸模式,分别如下:

TOUCH_MODE_REST: 未處于滾動機制之中;
TOUCH_MODE_DOWN: 接收到down觸摸事件,但還沒有達到輕觸的程度;
TOUCH_MODE_TAP: 觸摸事件被辨別為輕觸事件(輕觸Runnable已經執行),且等待一個長按事件;
TOUCH_MODE_DONE_WAITING: 仍為down事件的範疇,等待手指開始移動(即等待move觸摸事件的發生);
TOUCH_MODE_SCROLL: ListView内容随着指尖的移動而移動,此時已經進入move觸摸事件之中了;
TOUCH_MODE_FLING: ListView進入抛動模式,滑動速度過快,手指離開螢幕後,ListView會繼續滑動;
TOUCH_MODE_OVERSCROLL: 滑動到ListView邊緣的狀态;
TOUCH_MODE_OVERFLING: 抛動回彈,抛動到ListView邊緣的狀态;

        這八個常量的值,從-1到6依次遞增;這種遞增順序,也大體表現出了一個ListView滑動的生命周期。而ListView的觸摸事件處理函數則根據不同的時刻,來更改目前ListView的觸摸模式,并執行目前觸摸模式狀态下應當呈現的效果。

    1.2滑動相關的變量

       ListView之中與滑動相關的變量,主要的作用是用來緩存一些重要的坐标點,如下所示:

mTouchMode: 目前的觸摸模式,1.1節中所列出的八個常量之一,初始值為TOUCH_MODE_REST;
mMotionCorrection: 開始滑動之前,手指已經移動的距離;
mMotionPosition: 接受到down手勢事件的視圖對應的item的位置;
mMotionViewOriginalTop: 接收到down手勢事件的視圖的頂部偏移量;
mMotionX: down手勢事件位置的X坐标;
mMotionY: down手勢事件位置的Y坐标;
mLastY: 上一個手勢事件位置的Y坐标(如果存在);
mVelocityTracker: 在觸摸滾動期間決定速率;
mTouchSlop
      
當手指滑動一定距離後,才開始産生滑動效果,此變量表示所謂的“一定距離”。

         當然,與滑動相關的變量遠遠不止這9個,在下文分析遇到時,在進行說明;除了這些屬性變量外,還有些屬于方法的本地變量,在分析時,在做特殊說明。

    1.3滑動變量的相關接口

        如果說常量和變量盡可能的來描述一個滑動,那麼與滑動相關的接口則是來定義一個滑動在不同的狀态,不同的時刻下應該做些什麼。此處的接口,除了interface之外還有一些ListView自己定義好了的Runnable。 OnScrollChangeListener接口:        當ListView滾動時,用來回調的接口;該接口主要側重于滾動狀态的改變。接口的内部定義了三個常量,用來描述ListView的3中滾動狀态,分别如下:

SCROLL_STATE_IDLE: 空閑狀态,及此狀态下ListView沒有滾動
SCROLL_STATE_TOUCH_SCROLL:
      
滑動狀态;即Listview的内容随着手指的移動而移動;
SCROLL_STATE_FLING: 抛動狀态;即手指離開螢幕,但由于速度過快,ListView的内容會繼續滾動一段時間;

        當ListView的滑動狀态在這三種狀态之中互相切換時,就會回調該接口的onScrollStateChanged方法;一般而言onScrollStateChanged方法将在Adapter.getView(int, View, ViewGroup)方法之前被調用。OnScrollChangeListener接口之中還定義了另一個方法:onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount),此方法會在滑動完成之後被調用。 CheckForTap内部類:

       輕觸事件一般是指,從手指接觸到螢幕的一瞬間開始,經過100毫秒之後,會觸發一個事件,這個事件就是一個輕觸事件。        輕觸事件回調類是指,當産生輕觸事件時,進行回調的一個類;它執行了Runnable接口,當進行回調時,會調用run方法。run方法的流程如下:        1、判斷目前模式,因為輕觸模式是相對于down觸摸事件而言,是以如果目前并不是TOUCH_MODE_DOWN模式,則run方法不會做任何事情;        2、觸摸模式由TOUCH_MODE_DOWN轉變為TOUCH_MODE_TAP模式;        3、輕觸事件産生之後,就說明使用者的手指已經按在了視圖一定時間了,是以需要将對應視圖的狀态設定為press(調用setPress方法);        4、判斷ListView是否能夠具有可長按性,如果具有則post一個異步的長按事件回調消息,一般而言長按事件會在使用者按住螢幕後500毫秒觸發。 PerformClick内部類:

       執行點選效果,與CheckForTap内部類一緻,PerformClick内部類也執行了一個Runnable接口,當執行點選效果時,也會回調此類之中的run方法;此run方法首先會根據mChoiceMode來更新ListView被選擇的item,然後 會找到點選效果發送在ListView之中的哪個Item上,最後調用AdapterView. performItemClick方法,performItemClick則會調用OnItemClickListener接口中的 onItemClick 方法( onItemClick 方法應該很熟悉了吧)。 CheckForLongPress内部類:

       執行長按效果,與PerformClick内部類一緻,CheckForLongPress内部類也執行了一個Runnable接口;前文曾提過,當調用CheckForTap内部類中的run方法時,會根據ListView是否具有可長按性而向UI線程發送一個異步的回調消息,當處理這個異步消息時,便會調用CheckForLongPress内部類中的run方法。       run方法首先會判斷出作用于長按事件的子視圖,然後調用ListView中的performLongPress方法,執行長按事件處理,最後根據performLongPress方法的傳回值來确定目前的觸摸模式。       确定作用于長按事件的子視圖,則需要借助上文提及的變量mMotionPosition(接受到down手勢事件的視圖對應的item的位置)。       performLongPress方法的源代碼如下:

boolean performLongPress(final View child,
            final int longPressPosition, final long longPressId) {
        ......
        boolean handled = false;//是否處理了長按事件
        if (mOnItemLongClickListener != null) {
            handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child,
                    longPressPosition, longPressId);
        }
        if (!handled) {//長按事件建立内容菜單
            mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
            handled = super.showContextMenuForChild(AbsListView.this);
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }
           

      performLongPress方法一共有三個入參:第一個入參是作用于長按事件的子視圖;第二個參數是第一個入參對應的item在擴充卡之中的位置;第三個入參是指第一個入參對應的item的ID。       根據源代碼,ListView之中主要用來處理長按事件的還是OnItemLongClickListener監聽器之中的onItemlongClick方法。       最後傳回一個布爾類型的結果。       回到CheckForLongPress内部類的run方法之中,當run方法調用了performLongPress方法後,會根據performLongPress方法的傳回值來設定具體的觸摸模式,如果傳回true,則表示執行了長按事件,也就是此次觸摸流程主要的目的是為了執行長按事件,而非引起滑動,是以将觸摸模式還原為TOUCH_MODE_REST;如果performLongPress傳回false,則表示未執行長按事件,将觸摸模式設定為TOUCH_MODE_DONE_WAITING,即直到此時,使用者的手指還處于按住螢幕的階段——開始等待使用者的手指移動。

       将一些主要的常量、變量、接口及内部類介紹了之後,我們就具體分析一個ListView的滑動流程。

2、down——滑動開始前的準備

      ListView的滑動是通過ListView的觸摸事件來引起的,那麼一個滑動流程的開始是否就是從AbsListView中的onTouchEvent(ListView之中沒有onTouchEvent方法)開始的呢?顯然不是!        通過ViewGroup對觸摸事件的配置設定流程來看,在調用onTouchEvent方法之前,會先調用ViewGroup的onInterceptTouchEvent方法來判斷是否在将觸摸事件配置設定給子視圖的onTouchEvent方法之前,進行攔截。        衆所周知,AbsListView是ViewGroup的一個子類,而在AbsListView類中則重寫了ViewGroup的onInterceptTouchEvent方法,即重新定義了攔截規範;AbsListView具體的攔截規範,在此處不詳述,我們隻需要知道AbsListView的onInterceptTouchEvent方法會在AbsListView的onTouchEvent方法之前接收到觸摸事件。        是以,ListView滑動流程的開始方法就是AbsListView中的onInterceptTouchEvent方法。        一個觸摸事件的開始手勢肯定是down,下面我就看看onInterceptTouchEvent方法中對down手勢事件處理的源代碼:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int actionMasked = ev.getActionMasked();
        View v;
        ......
        switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {
            int touchMode = mTouchMode;
            if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
                mMotionCorrection = 0;//開始滾動之前,手指移動的距離
                return true;
            }

            final int x = (int) ev.getX();
            final int y = (int) ev.getY();
            ......

            int motionPosition = findMotionRow(y);//擷取手指按住的這個子視圖對應的item在擴充卡中的位置
            if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
                // User clicked on an actual view (and was not stopping a fling).
                // Remember where the motion event started
                v = getChildAt(motionPosition - mFirstPosition);//手指按住哪一個子視圖
                mMotionViewOriginalTop = v.getTop();//更新接收到down手勢事件的視圖的頂部偏移量
                mMotionX = x;//更新down手勢事件位置的X坐标
                mMotionY = y;//更新down手勢事件位置的Y坐标
                mMotionPosition = motionPosition;/更新接受到down手勢事件的視圖的位置
                mTouchMode = TOUCH_MODE_DOWN;//更新解析模式
                clearScrollingCache();
            }
            //因為down手勢是滑動的第一個動作,而mLastY表示上一個動作的Y值,
            //是以會在此處讓mLastY的值失效
            mLastY = Integer.MIN_VALUE;
            initOrResetVelocityTracker();//初始化速率追蹤器
            mVelocityTracker.addMovement(ev);//将目前時間添加到速率追蹤器中,以便計算出相應的滑動速率
            ......
            if (touchMode == TOUCH_MODE_FLING) {
                return true;
            }
            break;
        }

        ......

        return false;
    }
           

       根據代碼,可知 onInterceptTouchEvent方法對down手勢事件的處理,主要是将上文提及的相關變量進行更新指派。

       下面就看AbsListView中onTouchEvent對down手勢事件處理的源代碼:

@Override
    public boolean onTouchEvent(MotionEvent ev) {
     ......
final int actionMasked = ev.getActionMasked();
     ......
switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                onTouchDown(ev);
                break;
            }
        ......
     }
     ......
}
           

     直接調用onTouchDown方法,onTouchDown方法的源代碼如下:

private void onTouchDown(MotionEvent ev) {
        ......
        if (mTouchMode == TOUCH_MODE_OVERFLING) {//如果已經抛動到ListView的邊緣
            // Stopped the fling. It is a scroll.
            mFlingRunnable.endFling();//停止滾動
            if (mPositionScroller != null) {
                mPositionScroller.stop();
            }
            mTouchMode = TOUCH_MODE_OVERSCROLL;
            mMotionX = (int) ev.getX();
            mMotionY = (int) ev.getY();
            mLastY = mMotionY;
            mMotionCorrection = 0;
            mDirection = 0;
        } else {
            final int x = (int) ev.getX();//按住螢幕的X坐标
            final int y = (int) ev.getY();//按住螢幕的Y坐标
            int motionPosition = pointToPosition(x, y);//确定坐标對應的item在擴充卡中的位置

            if (!mDataChanged) {
                if (mTouchMode == TOUCH_MODE_FLING) {//如果ListView的内容正在抛動中,使用者的手指按住了ListView
                    // Stopped a fling. It is a scroll.
                    ......
                    mTouchMode = TOUCH_MODE_SCROLL;//變為滑動模式
                    mMotionCorrection = 0;
                    motionPosition = findMotionRow(y);
                    mFlingRunnable.flywheelTouch();
                } else if ((motionPosition >= 0) && getAdapter().isEnabled(motionPosition)) {//初始模式
                    // User clicked on an actual view (and was not stopping a
                    // fling).                 
                    //使用者點選到一個确切的子視圖,同時并不是停止一個抛動
                    //It might be a click or a scroll. Assume it is a // click until proven otherwise.
                    //使用者的點選可能是一個click也可能是一個滑動,在驗證它之前,假定它是一個click
                    mTouchMode = TOUCH_MODE_DOWN; // FIXME Debounce 
                    if (mPendingCheckForTap == null) { 
                           mPendingCheckForTap = new CheckForTap(); 
                    } 
                    mPendingCheckForTap.x = ev.getX(); mPendingCheckForTap.y = ev.getY();
                    //發出一個輕觸檢測異步消息
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 
                } 
            } 
            if (motionPosition >= 0) { // Remember where the motion event started 
                    final View v = getChildAt(motionPosition - mFirstPosition);               
                    mMotionViewOriginalTop = v.getTop(); 
            } 
            mMotionX = x; 
            mMotionY = y; 
            mMotionPosition = motionPosition; 
            mLastY = Integer.MIN_VALUE;//整個觸摸流程之中,down手勢事件為第一個事件,是以它的上一個事件的Y坐标無效
        }
        ...... 
}
           

         onTouchDown方法分别對 TOUCH_MODE_OVERFLING、TOUCH_MODE_FLING以及TOUCH_MODE_RESET模式進行了處理;對于前兩者主要是停止抛動,變為對應的滑動模式;而對于後者,則将滑動模式轉變為TOUCH_MODE_DOWN模式,并且發送一個異步的輕觸消息。        結合CheckForTap内部類,當輕觸消息被處理時,則會調用CheckForTap内部類的run方法,将觸摸模式由TOUCH_MODE_DOWN轉換為TOUCH_MODE_TAP模式,并發送一個異步的長按消息;當長按消息被處理時,會調用CheckForLongPress内部類的run方法,此方法中根據是否處理了長按事件(是否存在OnItemLongClickListener監聽器),分别将觸摸模式改變為TOUCH_MODE_RESET模式和TOUCH_MODE_DONE_WAITING模式。        總體而言,down手勢事件,主要做了滑動的一些準備工作,判斷差別了額輕觸、長按事件是否發生。

3、move——滑動開始與持續

      經曆了down手勢事件之後,一般來說,目前的觸摸模式已經變味了TOUCH_MODE_DONE_WAITING模式,也就是說ListView已經準備就緒,處于等待(滑動)的狀态之中。        與down手勢事件相似,在AbsListView的onInterceptTouchEvent方法中,會預先處理move手勢事件。        代碼如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        ......
        switch (actionMasked) {
        ......
        case MotionEvent.ACTION_MOVE: {
            switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
                int pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex == -1) {
                    pointerIndex = 0;
                    mActivePointerId = ev.getPointerId(pointerIndex);
                }
                final int y = (int) ev.getY(pointerIndex);
                initVelocityTrackerIfNotExists();//初始化速率追蹤器
                mVelocityTracker.addMovement(ev);//将此次事件添加到速率追蹤器之中,以便計算滑動速率
                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) {
                    return true;
                }
                break;
            }
            break;
        }
        ......
        }

        return false;
    }
           

      根據代碼,onInterceptTouchEvent方法之中,對move手勢事件的處理,隻限制于TOUCH_MODE_DOWN模式;也就是說,當使用者一按下螢幕,就立即move(未經曆輕觸事件和長按事件)時,會在onInterceptTouchEvent方法之中預先處理。        onInterceptTouchEvent方法之中,主要是調用了startScrollIfNeeded方法,對于此方法,下文會有進一步的分析。         對于move手勢事件,onInterceptTouchEvent方法中的處理邏輯較為簡單;我們繼續分析onTouchEvent方法;在onTouchEvent方法之中,對于move手勢事件的處理是直接調用onToucheMove方法,相關的源碼如下:

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
        ......
        if (mDataChanged) {
            // Re-sync everything if data has been changed
            // since the scroll operation can query the adapter.
            layoutChildren();
        }
        //目前事件的Y坐标
        final int y = (int) ev.getY(pointerIndex);

        switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
            case TOUCH_MODE_TAP:
            case TOUCH_MODE_DONE_WAITING:
                //以上三個模式表示滑動的開始
                // Check if we have moved far enough that it looks more like a
                // scroll than a tap. If so, we'll enter scrolling mode.
                //檢查是否我們移動的足夠遠,以至于看起來更像是一個滑動而非一個輕觸。
                //如果是這樣,我們将進入滑動模式
                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
                    break;
                }
                // Otherwise, check containment within list bounds. If we're
                // outside bounds, cancel any active presses.
                //如果我們移動的并不遠(還是像一個輕觸),那麼檢查我們是否移動到ListView
                //的範圍之外,如果是,則取消所有活躍的按下(取消輕觸事件和長按事件的發生)
                final View motionView = getChildAt(mMotionPosition - mFirstPosition);//down事件下,按住的子視圖
                final float x = ev.getX(pointerIndex);
                if (!pointInView(x, y, mTouchSlop)) {//如果目前移動的點已經離開了motionView視圖的範圍
                    setPressed(false);//取消ListView的按下狀态
                    if (motionView != null) {
                        motionView.setPressed(false);//取消子視圖的按下狀态
                    }
                    removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                            mPendingCheckForTap : mPendingCheckForLongPress);//取消相關還未發生的時間
                    mTouchMode = TOUCH_MODE_DONE_WAITING;
                    updateSelectorState();
                } else if (motionView != null) {
                    // Still within bounds, update the hotspot.
                    ......
                }
                break;
            case TOUCH_MODE_SCROLL:
            case TOUCH_MODE_OVERSCROLL:
                //以上兩個模式表示滑動的持續
                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
                break;
        }
    }
           

       可以看出onTouchMove方法之中主要分為兩大部分:一部分是針對TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING,即ListView還未開始滑動的情況;另一部分是針對TOUCH_MODE_SCROLL、TOUCH_MODE_OVERSCROLL;即ListView已經開始滑動的情況。

 3.1TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING模式下的onTouchMove方法

       對于ListView還未開始滑動的情況,主要會進行兩個判斷,一個判斷是目前滑動的距離是否足夠大,如果是則進行滑動,如果不是,則進行第二個判斷:目前滑動的位置是否離開了down手勢事件時,按住的那個子視圖的範圍,如果離開了,則将ListView及按住的那個子視圖的press狀态設定為false。        第一個判斷主要是通過startScrollIfNeeded方法來執行的,如果startScrollIfNeeded傳回true,則表示已經開始滑動,傳回false,則表示還未滿足開始滑動的條件;startScrollIfNeeded方法的相關源碼如下:

private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) {
        final int deltaY = y - mMotionY;//與down事件發生時,按住的Y坐标相比,移動了多少距離
        final int distance = Math.abs(deltaY);
        final boolean overscroll = mScrollY != 0;//ListView本身是否存在Y方向上的偏移量
        //如果ListView發生了Y方向的偏移,或者移動的距離達到了一定程度
        if ((overscroll || distance > mTouchSlop) &&
                (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
            createScrollingCache();
            if (overscroll) {
                mTouchMode = TOUCH_MODE_OVERSCROLL;
                mMotionCorrection = 0;
            } else {
                mTouchMode = TOUCH_MODE_SCROLL;//更新觸摸模式
                //開始滑動前,手指已經移動了的距離
                mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
            }
            removeCallbacks(mPendingCheckForLongPress);//開始滑動了,自然不是長按事件了
            setPressed(false);
            final View motionView = getChildAt(mMotionPosition - mFirstPosition);
            if (motionView != null) {
                motionView.setPressed(false);
            }
            //調用OnScrollChangeListener接口,表明目前滑動狀态改變!
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
            // Time to start stealing events! Once we've stolen them, don't let anyone
            // steal from us
            final ViewParent parent = getParent();
            if (parent != null) {
               //滑動開始了,就不允許ListView的父類中斷觸摸事件
                parent.requestDisallowInterceptTouchEvent(true);
            }
            scrollIfNeeded(x, y, vtev);//滑動
            return true;
        }

        return false;
    }
           

      能夠進行滑動,需要滿足兩種條件之一:第一個條件是ListView本身進行了Y軸方向的偏移(滑動);第二個條件是以down手勢事件發生時,手指按住螢幕的y坐标為起點,到此時move手勢事件發生時,手指按下的螢幕y坐标為終點,這兩點之間的距離超過了mTouchSlop。       對于第一個條件,一般而言,ListView的滑動并不是ListView本身進行滑動(即,ListView的偏移量依舊為0),其滑動的原理為,ListView目前所有的子視圖分别朝上(下)移動相同的距離(移動子視圖的布局位置),進而實作滑動的效果;是以,一旦ListView本身的偏移量大于了0,則說明滑動到了ListView的底部或者頂部。此時為了實作ListView的回彈效果,則需要先進行一點滑動。        對于第二個條件之中mTouchSlop的值是配置好了的,預設為8(像素)。        真正進行滑動處理的是scrollIfNeeded方法,總體而言,scrollIfNeeded方法也分别處理的TOUCH_MODE_SCROLL和TOUCH_MODE_OVERSCROLL。         其中處理TOUCH_MODE_SCROLL模式的其源碼如下:

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
        int rawDeltaY = y - mMotionY;//上次觸摸事件到此次觸摸事件移動的距離
        ......
        if (mLastY == Integer.MIN_VALUE) {
            rawDeltaY -= mMotionCorrection;
        }
        ......
        //如果滑動需要滑動的距離
        final int deltaY = rawDeltaY;
        int incrementalDeltaY =
                mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
        int lastYCorrection = 0;

        if (mTouchMode == TOUCH_MODE_SCROLL) {
            ......
            if (y != mLastY) {//此次觸摸事件和上次觸摸事件的y值發生了改變(需要滑動的距離>0)
                // We may be here after stopping a fling and continuing to scroll.
                // If so, we haven't disallowed intercepting touch events yet.
                // Make sure that we do so in case we're in a parent that can intercept.
                // 當停止一個抛動且繼續滑動之後,我們可能會執行此處的代碼
                //確定ListView的父視圖不會攔截觸摸事件
                if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
                        Math.abs(rawDeltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }

                final int motionIndex;//down手勢事件,按住的子視圖在ListView之中的位置
                if (mMotionPosition >= 0) {
                    motionIndex = mMotionPosition - mFirstPosition;
                } else {
                    // If we don't have a motion position that we can reliably track,
                    // pick something in the middle to make a best guess at things below.
                    motionIndex = getChildCount() / 2;
                }

                int motionViewPrevTop = 0;//down手勢事件,按住的子視圖的頂端位置
                View motionView = this.getChildAt(motionIndex);
                if (motionView != null) {
                    motionViewPrevTop = motionView.getTop();
                }

                // No need to do all this work if we're not going to move anyway
                //不需要做所有的工作,如果我們并沒有進行移動
                boolean atEdge = false;//是否到達了ListView的邊緣
                if (incrementalDeltaY != 0) {
                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);//追蹤手勢滑動
                }

                // Check to see if we have bumped into the scroll limit
                //檢視我們是否撞到了滑動限制(邊緣)
                motionView = this.getChildAt(motionIndex);
                if (motionView != null) {
                    // Check if the top of the motion view is where it is
                    // supposed to be
                    final int motionViewRealTop = motionView.getTop();
                    if (atEdge) {//到達了ListView的邊緣
                        // Apply overscroll
                        //響應的回彈效果實作
                        ......
                    }
                    mMotionY = y + lastYCorrection + scrollOffsetCorrection;//更新
                }
                mLastY = y + lastYCorrection + scrollOffsetCorrection;//更新
            }
        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
             ......
        }
    }
           

      總體而言,這一步也算是一個外殼,真正跟蹤滑動運作的是trackMotionScroll方法。 trackMotionScroll方法的邏輯較為複雜;總體而言一個可歸納為以下7個步驟,來實作滑動效果:       1、确定相關變量的值,以及定義一些臨時變量;代碼如下:

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        final int childCount = getChildCount();//子視圖個數
        if (childCount == 0) {
            return true;
        }

        final int firstTop = getChildAt(0).getTop();//第一個子視圖的頂部
        //最後一個子視圖的底部
        final int lastBottom = getChildAt(childCount - 1).getBottom();

        final Rect listPadding = mListPadding;

        // "effective padding" In this case is the amount of padding that affects
        // how much space should not be filled by items. If we don't clip to padding
        // there is no effective padding.
        int effectivePaddingTop = 0;//paddingTop
        int effectivePaddingBottom = 0;//paddingBottom
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            effectivePaddingTop = listPadding.top;
            effectivePaddingBottom = listPadding.bottom;
        }

         // FIXME account for grid vertical spacing too?
        //第一個子視圖的頂部離ListView頂部的距離,即向下滑動此距離,需要調用getView方法重新綁定一個子視圖
        final int spaceAbove = effectivePaddingTop - firstTop;
        final int end = getHeight() - effectivePaddingBottom;
        //最後一個子視圖的底部離ListView底部的距離,即向上可滑動的距離,需要調用getView方法重新綁定一個子視圖
        final int spaceBelow = lastBottom - end;
        //整個ListView的高度(出去padding)
        final int height = getHeight() - mPaddingBottom - mPaddingTop;
        //確定最大的可滾動距離不能超過ListView的高度
        if (deltaY < 0) {
            deltaY = Math.max(-(height - 1), deltaY);
        } else {
            deltaY = Math.min(height - 1, deltaY);
        }
        //確定最大的可滾動距離不能超過ListView的高度
        if (incrementalDeltaY < 0) {
            incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
        } else {
            incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
        }
        //目前第一個視圖對應的item在擴充卡之中的位置
        final int firstPosition = mFirstPosition;
        ......
}
           

         2、判斷目前滑動是否已經滑動到ListView的頂(低)部邊緣位置;

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        //是否可以向下滑動
        //目前第一個子視圖對應的item在擴充卡中的位置為0,且
        //第一個子視圖整個視圖的位置都在ListView之中,且
        //手指滑動的距離大于0
        //以上三個條件同時成立,則不能向下滑動
        final boolean cannotScrollDown = (firstPosition == 0 &&
                firstTop >= listPadding.top && incrementalDeltaY >= 0);
        //是否可以向上滑動
        //目前最後一個子視圖對應的item在擴充卡中的位置為最後一個,且
        //最後一個子視圖整個視圖的位置都在ListView之中,且
        //手指滑動的距離小于于0
        //以上三個條件同時成立,則不能向上滑動
        final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
                lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);

        if (cannotScrollDown || cannotScrollUp) {//如果到達了邊緣,則傳回true
            return incrementalDeltaY != 0;
        }
        ......
}
           

       如果到達了ListView的邊緣位置,且滑動的距離不等于0,則傳回true。        3、如果未達到ListView的邊緣位置,則判斷目前滑動,是否将一些子視圖完全滑出了ListView的可是範圍之外;

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        //是否從下往上滑動
        final boolean down = incrementalDeltaY < 0;

        ......
        //headerViewsCount與footerViewsStart之間的就是item所在的範圍
        //頁眉視圖的個數
        final int headerViewsCount = getHeaderViewsCount();
        //頁腳視圖對應的item在擴充卡中對應的位置
        final int footerViewsStart = mItemCount - getFooterViewsCount();

        int start = 0;//第一個離開了ListView的可見範圍的子視圖的位置(index)
        int count = 0;//一共有多少個子視圖離開了ListView的可視範圍

        if (down) {//從下往上移動
            int top = -incrementalDeltaY;//移動的距離
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                top += listPadding.top;
            }
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                //是否有子視圖完全被滑動離開了ListView的可見範圍
                if (child.getBottom() >= top) {
                    break;
                } else {//目前子視圖
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);//回收子視圖
                    }
                }
            }
        } else {//從上往下移動
            int bottom = getHeight() - incrementalDeltaY;//移動的距離
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                bottom -= listPadding.bottom;
            }
            for (int i = childCount - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                //是否有子視圖完全滑動離開了ListView的可見範圍
                if (child.getTop() <= bottom) {
                    break;
                } else {
                    start = i;
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();
                        mRecycler.addScrapView(child, position);//回收子視圖
                    }
                }
            }
        }

        mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

        mBlockLayoutRequests = true;

        if (count > 0) {//如果存在完全離開了ListView可視範圍的子視圖
            detachViewsFromParent(start, count);//将這些完全離開了可是範圍的子視圖全部删掉
            mRecycler.removeSkippedScrap();//從視圖重用池中删除需要丢棄的視圖
        }
        ......
}
           

           關于判斷是否存在子視圖完全離開了ListView的可視範圍的算法,如下圖所示(以從上往下為例):

【進階android】ListView源碼分析——ListView的滾動機制1、滑動相關的常量、變量及接口2、down——滑動開始前的準備3、move——滑動開始與持續4、up——滑動結束與後序5、ListView的抛動機制6、ListView滾動機制的總結

        如圖所示,這個黑線框就是ListView在滑動之前的可視範圍;down手勢事件,手指按在了A點,目前move手勢事件滑動到了B點,代碼之中的incrementalDeltaY本地變量的值=A-B=C-D。其中D是down手勢事件時,ListView的底部,而當滑動完成之後C就是ListView的底部,而目前所有頂部在C點下方的子視圖,在滑動動作完成之後,都會被滑動到ListView的底部以下,即完全離開了ListView的可視範圍。是以,ListView會以incrementalDeltaY本地變量的值來當做判斷是否完全離開ListView的标準。        4、将未從ListView删除的子視圖(即沒有完全離開ListView可視範圍的所有視圖)全部朝上(下)移動incrementalDeltaY變量對應的值,機關為像素。代碼如下:

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        //将未删除的所有的子視圖朝上(下)移動incrementalDeltaY這麼多距離
        offsetChildrenTopAndBottom(incrementalDeltaY);
        //更新第一個子視圖對應的item在擴充卡中的位置
        if (down) {
            mFirstPosition += count;
        }
        ......
}
           

        通過ViewGroup類的offsetChildrenTopAndBottom方法來實作子視圖的移動;而該方法的原理則是同時将一個子視圖的mTop變量和mBottom變量加上incrementalDeltaY變量的值。         5、ListView有可能删除了一些完全離開ListView視圖範圍的子視圖,為了将ListView填充滿,需要重新調用擴充卡的getView方法,綁定相應的item。代碼如下:

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
        //如果還有可移動的範圍
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
          //因為有可能有一些子視圖完全離開了ListView範圍,所有需要重新加載新的item來填充ListView的空白
            fillGap(down);
        }
        ......
}
           

        spaceAbove變量和spaceBelow變量,是在第一個步驟裡被定義指派的;其具體的含義如下圖所示(以 spaceAbove為例):

【進階android】ListView源碼分析——ListView的滾動機制1、滑動相關的常量、變量及接口2、down——滑動開始前的準備3、move——滑動開始與持續4、up——滑動結束與後序5、ListView的抛動機制6、ListView滾動機制的總結

        如圖所示,如果向下滑動的距離超過了spaceAbove變量的值,那麼肯定需要重新擷取一個新的item,來作為新的第一個子視圖。         fillGap方法的實作請參照【進階android】ListView源碼分析——子視圖的七種填充方式一文的分析。         6、重新設定被選中的item;

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
        //重新定位被選中的item
        if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
            final int childIndex = mSelectedPosition - mFirstPosition;
            if (childIndex >= 0 && childIndex < getChildCount()) {
                positionSelector(mSelectedPosition, getChildAt(childIndex));
            }
        } else if (mSelectorPosition != INVALID_POSITION) {
            final int childIndex = mSelectorPosition - mFirstPosition;
            if (childIndex >= 0 && childIndex < getChildCount()) {
                positionSelector(INVALID_POSITION, getChildAt(childIndex));
            }
        } else {
            mSelectorRect.setEmpty();
        }
        ......
}
           

           7、執行OnScrollListener中的onScroll方法,代碼如下:

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
        ......
       //執行mOnScrollListener中的onScroll方法
        invokeOnItemScrollListener();
        return false;
}
           

          至此, onTouchMove方法方法對TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING三種模式(即ListView還未開始滑動的情況),做了一個大緻的分析。總體而言,onTouchMove方法首先調用startScrollIfNeed方法,startScrollIfNeed方法根據兩個條件判斷是否繼續,這兩個條件一個是ListView本身是否發生偏移,一個是滑動的距離是否超過了mTouchSlop變量的值,這兩個條件任意一個為true,則調用scrollIfNeed方法;而在scrollIfNeed方法之中,則根據trackMotionScroll方法的傳回值判斷是否已經滾動到了ListView的邊緣,如果是則實作相應的邊緣效果。         trackMotionScroll方法才是真正實作滑動效果的方法。         最後要特别說明一下mLastY和mMotionY這兩個變量,根據android官方的解釋,前者的意思是:Y value from on the previous motion event (if any),即上一個手勢事件的Y值;後者的意思是:The Y value associated with the the down motion event,即與down手勢事件相關的Y值,然而在onTouchMove方法調用的scrollIfNeed方法中,都将目前的move手勢的Y值都更新到這兩個變量之上,如此而言,總覺得這兩個變量的意義都是差不多的,不知道可不可以如此了解,也算是一個存疑點。

3.2TOUCH_MODE_SCROLL、TOUCH_MODE_OVERSCROLL模式下的onTouchMove方法

        前文曾提過,onTouchMove方法對兩種情況分别進行了處理;一種是ListView還未開始滑動;一種是ListView正在滑動。onTouchMove方法對兩者的處理的最大的不同就是,前者調用了startScrollIfNeed方法,後者直接調用了scrollIfNeed方法。        而在scrollIfNeed方法的源碼中也對兩種情況進行分别處理,如下代碼所示:

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
        ......

        if (mTouchMode == TOUCH_MODE_SCROLL) {//對為滑動或持續滑動的情況的處理
            ......
        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {//對滑動到ListView邊緣的處理
            ......
        }
        ......    
    }
           

       對于TOUCH_MODE_SCROLL的處理,在3.1節已經詳細叙述;而對TOUCH_MODE_OVER_SCROLL的處理則和TOUCH_MODE_SCROLL分支中,滑動到ListView邊緣的處理方式相似。

4、up——滑動結束與後序

        終于來到觸摸事件的最後一個階段:up手勢事件;根據down、move的分析方式,我們先看看onInterceptToucheEvent方法之中對up手勢事件的處理,代碼如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        ......

        switch (actionMasked) {
        ......
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            mTouchMode = TOUCH_MODE_REST;
            mActivePointerId = INVALID_POINTER;
            recycleVelocityTracker();
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            ......
            break;
        }

        ......
        }

        return false;
    }
}
           

        邏輯很簡單,将相關值設定為初始值,回收速率控制器,報告滾動狀态變更;當然,要執行這段邏輯的前提是,up手勢事件還能被傳遞到onInterceptTouchEvent方法之中。         對于up手勢事件,onTouchUp方法之中分别處理了三大類别的觸摸模式:          1、TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING;          2、TOUCH_MODE_SCROLL;          3、TOUCH_MODE_OVERSCROLL;

4.1、TOUCH_MODE_DOWN、TOUCH_MODE_TAP、TOUCH_MODE_DONE_WAITING模式下的onTouchUp方法

          此三類模式下的onTouchUp方法隻做了一件事情,那就是執行點選事件(OnItemClickListener)。源碼如下:

private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            //down手勢事件,按住的子視圖對應的item在adapter之中的位置
            final int motionPosition = mMotionPosition;
            //down手勢事件,按住的子視圖
            final View child = getChildAt(motionPosition - mFirstPosition);
            if (child != null) {
                if (mTouchMode != TOUCH_MODE_DOWN) {
                    child.setPressed(false);
                }

                final float x = ev.getX();
                //up手勢事件對應的x坐标是否還在ListView的視圖範圍之内
                final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
                if (inList && !child.hasFocusable()) {//x坐标還在ListView之中,且子視圖不能擷取焦點
                    if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }

                    final AbsListView.PerformClick performClick = mPerformClick;
                    performClick.mClickMotionPosition = motionPosition;//更新位置
                    performClick.rememberWindowAttachCount();

                    mResurrectToPosition = motionPosition;
                    //如果目前觸摸模式屬于TOUCH_MODE_DOWN或者TOUCH_MODE_TAP
                    //則表明還未執行輕觸事件或者還未執行長按事件
                    if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
                        //取消輕觸事件或者長按事件的觸發
                        removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                                mPendingCheckForTap : mPendingCheckForLongPress);
                        mLayoutMode = LAYOUT_NORMAL;
                        if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                            mTouchMode = TOUCH_MODE_TAP;//将觸摸模式更改為輕觸模式
                            setSelectedPositionInt(mMotionPosition);//設定被選中的item
                            layoutChildren();//重新布局
                            child.setPressed(true);//子視圖按下狀态為true
                            positionSelector(mMotionPosition, child);//定位選中效果
                            setPressed(true);ListView的按下狀态為true
                            if (mSelector != null) {
                                Drawable d = mSelector.getCurrent();
                                if (d != null && d instanceof TransitionDrawable) {
                                    ((TransitionDrawable) d).resetTransition();
                                }
                                mSelector.setHotspot(x, ev.getY());
                            }                                
                            //mTouchModeReset是一個Runnalbe
                            //主要用于将觸摸模式恢複為TOUCH_MODE_RESET
                            //取消子視圖和ListView的按下狀态
                            //在資料未改變的情況下執行item click.
                            if (mTouchModeReset != null) {
                                removeCallbacks(mTouchModeReset);
                            }
                            mTouchModeReset = new Runnable() {
                                @Override
                                public void run() {
                                    mTouchModeReset = null;
                                    mTouchMode = TOUCH_MODE_REST;
                                    child.setPressed(false);
                                    setPressed(false);
                                    if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
                                        performClick.run();
                                    }
                                }
                            };
                            //延遲執行mTouchModeReset
                            postDelayed(mTouchModeReset,
                                    ViewConfiguration.getPressedStateDuration());
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                            updateSelectorState();
                        }
                        return;
                    } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                        //如果已經調用了長按事件,則直接執行item click
                        performClick.run();
                    }
                }
            }
            mTouchMode = TOUCH_MODE_REST;
            updateSelectorState();
            break;
            ......

            ......
        }

        ......
    }
           

       執行item click遵循一個規則;如果目前觸摸模式,為輕觸模式或者輕觸模式之前的模式,那麼則需要将目前模式強制設定為輕觸模式,并且輕觸模式與執行item click直接存在一個延遲(一般延遲時間為64毫秒),一般而言輕觸模式和執行item click處理時間上差異,還有就是輕觸模式下ListView與對應的子視圖的press狀态為true,item click下ListView與對應的子視圖的press狀态為false。        結合上文所述,performClick變量是一個PerformClick内部類,主要目的是調用OnItemClickListener監聽器的onItemClick方法,實作item click的效果。

4.2、TOUCH_MODE_SCROLL模式下的onTouchUp方法

        TOUCH_MODE_SCROLL模式,則會根據目前速率,以及是否滑動到item的第一個item或者最後一個item,來判斷是否将TOUCH_MODE_SCROLL模式變為TOUCH_MODE_FILING模式;具體的代碼如下:

private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        ......
        case TOUCH_MODE_SCROLL:
            final int childCount = getChildCount();
            if (childCount > 0) {
                //所有子視圖的最高位置
                final int firstChildTop = getChildAt(0).getTop();
                //所有子視圖的最低位置
                final int lastChildBottom = getChildAt(childCount - 1).getBottom();
                //ListView的top位置
                final int contentTop = mListPadding.top;
                //ListView的bottom位置
                final int contentBottom = getHeight() - mListPadding.bottom;
                //所有的item都完全展示在ListView的可是範圍之内
                if (mFirstPosition == 0 && firstChildTop >= contentTop &&
                        mFirstPosition + childCount < mItemCount &&
                        lastChildBottom <= getHeight() - contentBottom) {
                    //不滑動,恢複初始狀态
                    mTouchMode = TOUCH_MODE_REST;
                    //滑動狀态變為不滑動
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                } else {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    //計算目前滑動的速率
                    //computeCurrentVelocity方法第一個入參表示機關
                    //1000表示每一秒滑過的像素
                    //第二個入參表示computeCurrentVelocity方法能夠計算的最大速率
                    //mMaximumVelocity的值預設為8000
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    
                    //getYVelocity方法将傳回Y方向上的最後一次計算出的速率
                    //mVelocityScale預設為1
                    final int initialVelocity = (int)
                            (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
                    // Fling if we have enough velocity and we aren't at a boundary.
                    // Since we can potentially overfling more than we can overscroll, don't
                    // allow the weird behavior where you can scroll to a boundary then
                    // fling further.
                    // 如果我們有着足夠的速率且在ListViewd的可視範圍之内,抛動。
                    // 一旦我們潛在的将抛動復原的距離多于滑動復原,則禁止滑動到一個邊界,然後
                    // 抛動更遠,這一奇怪的行為。
                    // mMinimumVelocity表示可以進行抛動的速率的零界點,預設值為50像素/秒
                    boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
                    // mOverscrollDistance的預設值為0
                    if (flingVelocity &&
                            !((mFirstPosition == 0 &&
                                    firstChildTop == contentTop - mOverscrollDistance) ||
                              (mFirstPosition + childCount == mItemCount &&
                                    lastChildBottom == contentBottom + mOverscrollDistance))) {
                        //進入此分支滿足的條件為:速率足夠大,并且可以上、下同時滑動
                        if (!dispatchNestedPreFling(0, -initialVelocity)) {
                            if (mFlingRunnable == null) {
                                mFlingRunnable = new FlingRunnable();
                            }
                            //滾動狀态變為抛動狀态
                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                            mFlingRunnable.start(-initialVelocity);//開始抛動
                            dispatchNestedFling(0, -initialVelocity, true);
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                        }
                    } else {//速率不夠大,或者不能向上滑動,或者不能向下滑動
                        mTouchMode = TOUCH_MODE_REST;
                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                        if (mFlingRunnable != null) {
                            mFlingRunnable.endFling();//結束抛動
                        }
                        if (mPositionScroller != null) {
                            mPositionScroller.stop();
                        }
                        if (flingVelocity && !dispatchNestedPreFling(0, -initialVelocity)) {
                            dispatchNestedFling(0, -initialVelocity, false);
                        }
                    }
                }
            } else {//沒有子視圖則不進行滑動
                mTouchMode = TOUCH_MODE_REST;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            }
            break;
            ......
        }
        ......
    }
           

       ListView的抛動完全是由FlingRunnable内部類控制、實作;關于FlingRunable内部類的抛動機制,下文會詳細叙述!。

4.3 TOUCH_MODE_OVERSCROLL模式下的onTouchUp方法

       源碼如下:

private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        ......

        case TOUCH_MODE_OVERSCROLL:
            if (mFlingRunnable == null) {
                mFlingRunnable = new FlingRunnable();
            }
            final VelocityTracker velocityTracker = mVelocityTracker;
            velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//計算目前速率
            final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
            //目前滾動狀态變為抛動狀态
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
            if (Math.abs(initialVelocity) > mMinimumVelocity) {//目前速率超過最低抛動速率
                mFlingRunnable.startOverfling(-initialVelocity);
            } else {
                mFlingRunnable.startSpringback();
            }

            break;
        }

        ......
    }
           

4.4 onTouchUp方法的結尾部分 

       上文提及,onTouchUp方法會針對三種情況的觸摸模式,分别進行處理;而在這三種情況中,無論是哪一種,當onTouchUp分别處理之後,都還會調用一小部分公共的代碼,這一小部分代碼如下所示:

private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
             //三種情況的不同處理
             ......
        }

        setPressed(false);//ListView無按下狀态

        ......

        // Need to redraw since we probably aren't drawing the selector anymore
        //需要重繪,因為我們可能并沒有再繪制選擇器了
        invalidate();
        removeCallbacks(mPendingCheckForLongPress);//删除長按事件
        recycleVelocityTracker();

        mActivePointerId = INVALID_POINTER;

       ......
    }
           

        至此,除了關于ListView的抛動機制之外,整個ListView的滑動,在三大觸摸手勢事件中流程便分析完畢了。

5、ListView的抛動機制

       根據上文的分析,承載ListView抛動效果的是AbsListView之中的内部類FlingRunnable,FlingRunnable是執行了Runnable接口,它是一個抛動行為的響應者,通過start方法,初始化一個抛動,抛動的每一幀都在run方法之中被處理。因為FlingRunnable執行了一個Runnable接口,是以每一個該類的執行個體,在抛動過程中,都會将自己作為一個消息重複發送給UI線程進行處理。         FlingRunnable類中有一個較為重要的屬性,mScroller,它是一個OverScroller對象,OverScroller與Scroller類似,隻不過前者添加了回彈效果的實作,ListView就是通過FlingRunnable類,間接使用OverScroller進行抛動相關的計算,根據計算的結果,來執行trackScrollMotion方法,來實作抛動的效果。         對于FlingRunnable,start方法是一個抛動的開始,run方法是一個抛動的執行和終結,是以着重分析這兩個方法。

5.1FlingRunnable類的start方法。

         由上文可知,當觸摸事件到達up階段時,會調用AbsListView的onTouchUp方法,該方法會根據速率跟蹤器,計算出目前滑動的速率,如果速率超過了抛動的最小速率,那麼就會調用Flingable類的start方法;其調用過程如下:

private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        ......
        case TOUCH_MODE_SCROLL:
            final int childCount = getChildCount();
            if (childCount > 0) {
                ......
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    //計算目前速率,機關為像素/秒
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    //擷取目前速率,機關為像素/秒
                    final int initialVelocity = (int)
                            (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
                    
                    boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
                    ......
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                    mFlingRunnable.start(-initialVelocity);//目前速率的相反速率
                    ......       
            } else {
                mTouchMode = TOUCH_MODE_REST;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            }
            break;

            ......
        }

        ......
    }
           

       在onTouchUp方法之中,通過速率跟蹤器,計算出當去Y軸上的速率,進而将此速率,作為mFlingRunnable.start方法的入參;然後onTouchUp方法并未直接将速率作為入參,而是取了入參的相反數,這是什麼原因呢?        我們暫且不提此問題,而是繼續看start方法的源代碼:

private class FlingRunnable implements Runnable {
        ......
        void start(int initialVelocity) {
            //如果是從上往下抛動,則initialVelocity的值為負數,則上一次抛動的位置為很大            
           //如果是從下往上抛動,則initialVelocity的值為正數,則上一次抛動的位置為0
            int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
            mLastFlingY = initialY;
            //設定插入器,插入器的含義是給定一個對應點,擷取此對應點的速率
            mScroller.setInterpolator(null);//使用預設的插入器
            //開始抛動
            //第一個入參表示x方向上的開始點
            //第二個入參表示y方向上的開始點
            //第三個入參表示x方向上的速率
            //第四個入參表示y方向上的速率
            //第五個入參表示x方向上抛動的最小值
            //第六個入參表示x方向上抛動的最大值
            //第七個入參表示y方向上抛動的最小值
            //第八個入參表示y方向上抛動的最大值
            mScroller.fling(0, initialY, 0, initialVelocity,
                    0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
            //觸摸模式變為抛動模式
            mTouchMode = TOUCH_MODE_FLING;
            //持續抛動
            postOnAnimation(this);
            ......
        }
         ......
}
           

        在高中實體課堂上,我們知道速率的正負符号,代表了此速率的方向;這一定律在此處也适用;而在start方法的源代碼中,首先就會根據速率的正負符号來決定抛動的初始位置;如果是負号,則抛動的初始位置為int的最大值,反之則抛動的初始位置為0。        另一方面,前文已經提過,ListView的抛動本質上則是調用了trackScrollMotion方法;trackScrollMotion的原型如下:

boolean trackMotionScroll(int deltaY, int incrementalDeltaY) ;
           

       第二個入參表示需要滾動的距離。        回顧一下滾動機制中,調用trackMotionScroll方法時,此參數是如何計算而來的?根據scrollIfNeed方法中的代碼:

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
        int rawDeltaY = y - mMotionY;
        ......
        final int deltaY = rawDeltaY;
        int incrementalDeltaY =
                mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
        ......

        if (mTouchMode == TOUCH_MODE_SCROLL) {
            ......
            if (y != mLastY) {
                ......
                boolean atEdge = false;
                if (incrementalDeltaY != 0) {
                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
                }
                ......
            }
        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
            ......
        }
    }
           

       從此代碼可以看出,第二個入參incrementalDeltaY是通過兩方面得來;一方面,如果mLastY為int類型的最小值,則等于deltaY,在onTouchDown方法中可以得出,當此時觸摸事件為down階段時,mLastY為int類型的最小值,在此情況下, incrementalDeltaY的值依賴于deltaY,而deltaY則等于y-mMotionY,其中y為第一次move時的y坐标,mMotionY為down手勢事件,手指按下的y坐标;另一方面,incrementalDeltalY的值等于y-mLastY,其中y表示目前y坐标,mLastY表示上一次move手勢事件。        總體而言,incrementalDeltaY的值等于目前觸摸事件的Y位置減去上一次觸摸事件的Y位置。這是滑動的情況。        對于抛動,incrementalDeltaY的值等于上一次抛動後的Y位置減去目前抛動後的Y位置。恰恰與滑動的情況湊成一對相反數。        是以,滑動與抛動,都是通過速率(位移)的正負,來确定移動的方向;兩者不同的是,滑動,通過正号來表示從上往下移動,而抛動,則是通過負号來表示從上往下移動。        在onTouchUp方法調用Flingable.start方法的過程中,onTouchUp方法獲得的速率,是滑動模式下的速率,而start方法中的速率,則是抛動的速率,是以在onTouchUp方法調用Flingable.start方法時,需要對速率取反,再作為start方法的入參。        在start方法裡的具體源碼中,首先會根據傳來的速率的正負,來設定移動的方向;如果速率是負,則表示手指從上往下抛動,ListView朝上方移動,此時初始的抛動位置為int類型的最大值,且抛動過程中的抛動位置會越來越小;如果速率是正,則表示手指從下往上抛動,ListView朝下方移動,此時初始的抛動位置為0,且抛動過程中的抛動位置會越來越大。如圖所示                                                 

【進階android】ListView源碼分析——ListView的滾動機制1、滑動相關的常量、變量及接口2、down——滑動開始前的準備3、move——滑動開始與持續4、up——滑動結束與後序5、ListView的抛動機制6、ListView滾動機制的總結

        當抛動過程中,抛動的位置越來越大,則表示朝下方滾動,即ListView朝下方移動;抛動的位置越來越小,則說明朝上方滾動,即ListView朝上方移動。        start方法之中,在将抛動的初始位置确定之後,就會調用mScroller(一個OverScroll對象)的fling方法,設定一個抛動的初始狀态;接着将觸摸模式修改為抛動動模式,最後發送一個異步消息,當異步消息被執行時,會調用Flingable的run方法來計算目前的抛動狀态,然後根據抛動狀态,調用trackScrollMotion方法來實作ListView移動的效果;實作完移動效果之後,會根據情況,再次調用postOnAnimation方法發送一個異步消息,如此來實作一種持續抛動的效果。

5.2FlingRunnable類的run方法。

        start是一個抛動的開始;而run方法則是一個抛動過程中的每一幀,每抛動一次,就會調用一次run方法;然而,一個抛動過程中的每一幀,可能存在不同的情況,例如是否繼續抛動,是否停止抛動等等,而這些不同的情況,往往和ListView的觸摸模式息息相關。而run方法則針對不同的觸摸模式,做出了不同的處理。           總的來說,run方法主要處理了TOUCH_MODE_FLING和TOUCH_MODE_OVERFLING這兩種觸摸模式的情況。      先看看run方法對TOUCH_MODE_FLING模式的處理,源碼如下:

public void run() {
            switch (mTouchMode) {
            ......
            case TOUCH_MODE_FLING: {
                if (mDataChanged) {//如果資料改變了,重新布局
                    layoutChildren();
                }
                //如果item為0,或者子視圖為零,結束抛動,并傳回
                if (mItemCount == 0 || getChildCount() == 0) {
                    endFling();
                    return;
                }

                final OverScroller scroller = mScroller;
                //計算目前抛動值,其傳回值為true,則表示還可以繼續滾動
                boolean more = scroller.computeScrollOffset();
                final int y = scroller.getCurrY();//擷取目前的Y坐标

                // Flip sign to convert finger direction to list items direction
                // (e.g. finger moving down means list is moving towards the top)
                // 輕抛信号,此信号表示将手指抛動的方向轉換成清單item移動的方向
                // 例如,手指朝下移動意味着清單正在往頂部移動
                // 此處計算滾動距離的方式和滑動時計算滾動距離的方式相反;前者是上一次減這一次,後者是這一次減上一次
                int delta = mLastFlingY - y;

                // Pretend that each frame of a fling scroll is a touch scroll
                // 假裝将一個抛動滾動的每一幀當做一個觸摸滾動
                if (delta > 0) {//從上往下抛動,開始位置在抛動位置的上方
                    // List is moving towards the top. Use first view as mMotionPosition
                    // 清單正朝上方移動,将第一個子視圖對應的item作為觸摸位置
                    mMotionPosition = mFirstPosition;
                    final View firstView = getChildAt(0);
                    mMotionViewOriginalTop = firstView.getTop();

                    // Don't fling more than 1 screen
                    // 抛動的距離不能超過一屏
                    delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta);
                } else {
                    // List is moving towards the bottom. Use last view as mMotionPosition
                    // 清單正在朝着底部移動,使用最後一個清單對應的item作為觸摸位置
                    int offsetToLast = getChildCount() - 1;
                    mMotionPosition = mFirstPosition + offsetToLast;

                    final View lastView = getChildAt(offsetToLast);
                    mMotionViewOriginalTop = lastView.getTop();

                    // Don't fling more than 1 screen
                    // 抛動的距離不能超過一屏
                    delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
                }

                // Check to see if we have bumped into the scroll limit
                // 檢測我們是否撞到了滾動限制之中
                View motionView = getChildAt(mMotionPosition - mFirstPosition);
                int oldTop = 0;
                if (motionView != null) {
                    oldTop = motionView.getTop();
                }

                // Don't stop just because delta is zero (it could have been rounded)
                // atEdge是否到達邊界
                final boolean atEdge = trackMotionScroll(delta, delta);//進行滾動
                // 是否停止
                final boolean atEnd = atEdge && (delta != 0);
                if (atEnd) {//如果需要停止
                    if (motionView != null) {
                        // Tweak the scroll for how far we overshot
                        int overshoot = -(delta - (motionView.getTop() - oldTop));
                        overScrollBy(0, overshoot, 0, mScrollY, 0, 0,
                                0, mOverflingDistance, false);//復原
                    }
                    if (more) {//還能繼續抛動
                        edgeReached(delta);//已經到達邊界的情況下的抛動處理
                    }
                    break;
                }

                if (more && !atEnd) {//還能繼續抛動,且不需要停止
                    if (atEdge) invalidate();//如果到達邊界,重繪
                    mLastFlingY = y;//更新上一次抛動點的y坐标
                    postOnAnimation(this);//持續抛動
                } else {//如果不能繼續抛動,或者需要停止
                    endFling();//停止抛動
                    ......
                }
                break;
            }

            .......
       }
           

        一般而言,調用了start方法時,就将ListView目前的觸摸模式更改為TOUCH_MODE_FLING,同時,也發送了一個異步消息。不出意外,這個消息被執行時,會調用上面這段源代碼。        這段代碼中,首先會根據mScroller計算出目前抛動的資訊,主要是目前抛動的位置,然後将上一次位置(mLastY)減去目前抛動的位置,來擷取偏移量,調用trackScrollMotion方法來實作滾動效果。        然後,根據trackScrollMotion的傳回值,判斷是否滾動到了ListView的邊緣位置。        如果到達了邊緣位置,且上一次位置和目前位置不同,則需要進行停止,首先調用ListView的overScrollBy方法,滑動ListView本身,如果是ListView朝上抛動,則調用了overScrollBy方法後,會繼續向上移動一點(mScrollY為負數),當然不會繼續移動太多(最多移動的距離為6dp)。随後,判斷是否還會繼續移動ListView,如果時,因為此時已經到達了邊緣,是以會繼續調用edgeReached方法,繪制邊緣效果,并且将觸摸模式改為TOUCH_MODE_OVERFLING模式。        如果還未到達邊緣位置,且還能繼續移動,則再次發生一個異步消息,此消息被執行時,會持續調用run方法。        如果還未到達邊緣位置,且不能繼續移動,則調用endFing方法,停止抛動。        前文曾述,edgeReached方法會在達到ListView的邊緣位置,且抛動還能繼續的情況被調用,該方法中會繪制邊緣效果,修改觸摸模式,而在完成這兩件事情之後,會繼續發送一個異步消息,而此異步消息被執行時,會再次調用run方法;隻不過,此時,由于edgeReached方法已經将觸摸模式修改為TOUCH_MODE_OVERFLING模式,是以會執行run方法中的TOUCH_MODE_OVERFLING模式對應的分支。        run方法中的TOUCH_MODE_OVERFLING模式對應的分支源碼如下:

@Override
        public void run() {
            switch (mTouchMode) {
            ......
            case TOUCH_MODE_OVERFLING: {
                final OverScroller scroller = mScroller;
                if (scroller.computeScrollOffset()) {//計算目前滾動速率
                    final int scrollY = mScrollY;
                    final int currY = scroller.getCurrY();
                    final int deltaY = currY - scrollY;
                    if (overScrollBy(0, deltaY, 0, scrollY, 0, 0,
                            0, mOverflingDistance, false)) {
                        final boolean crossDown = scrollY <= 0 && currY > 0;//從上往下
                        final boolean crossUp = scrollY >= 0 && currY < 0;//從下往上
                        if (crossDown || crossUp) {
                            int velocity = (int) scroller.getCurrVelocity();
                            if (crossUp) velocity = -velocity;

                            // Don't flywheel from this; we're just continuing things.
                            scroller.abortAnimation();
                            start(velocity);
                        } else {
                            startSpringback();//開始回彈
                        }
                    } else {
                        invalidate();
                        postOnAnimation(this);
                    }
                } else {//如果不能繼續抛動
                    endFling();//停止抛動
                }
                break;
            }
            }
        }
           

         至此,ListView的抛動機制就大緻分析完了。

6、ListView滾動機制的總結

        總體而言,ListView的整個滾動機制的生命周期可以分為8個階段,對應着8個不同的觸摸模式;理清這8個不同的觸摸模式的轉換,就大緻明白了整個滾動機制。        ListView的8個觸摸模式的互相轉換可如下圖所示:

【進階android】ListView源碼分析——ListView的滾動機制1、滑動相關的常量、變量及接口2、down——滑動開始前的準備3、move——滑動開始與持續4、up——滑動結束與後序5、ListView的抛動機制6、ListView滾動機制的總結

             如圖所示,圖中一共标出了16個轉換,将ListView中的8個觸摸模式大緻的轉換标記出來(還有些細枝末節沒有列出 )。這16個轉換中1-5表示一次完整的滑動過程(未到達邊際),9、11表示在已經滑動的基礎上一次完成的抛動過程(未達到邊際)。         1-5的過程如下:1)手指按住螢幕,觸發down手勢事件,發出輕觸檢測事件;2)輕觸檢測事件被觸發,發出長按檢測事件;3)長按檢測事件被促發,且沒有執行長按事件;4)手指移動螢幕,觸發move手勢事件,且手指移動的距離超過最小敏感距離(mTouchSlop變量);5)手指離開螢幕,觸發up手勢事件,且目前滑動的速率沒有達到最小抛動速率。         9、11的過程如下:9)手指離開螢幕,觸發up手勢事件,且目前滑動的速率達到或超過最小抛動速率;11)當抛動不能繼續下去時,則停止抛動。         餘下的轉換過程在一下時刻發生:          6)當手指按住螢幕,觸發down手勢事件之後,輕觸檢測事件觸發之前,這段時間中,觸發了move手勢事件;          7)此情況有兩個場景:第一個場景是當手指按住螢幕,觸發down手勢事件之後,長按檢測事件觸發之前,這段時間中,觸發了up手勢事件(如果此時輕觸檢測事件還未促發,則将觸摸模式修改為TOUCH_MODE_TAP,等待一定時間後,再變為TOUCH_MODE_RESET模式);第二個場景是,觸發了長按檢測事件,并且長按事件被成功執行;          8)與7)的第一種場景一緻;        10)當抛動時,達到了ListView的邊緣(edgeReached方法);        12)完成抛動邊緣效果及回填之後;        13)正在抛動過程中,又一次發生了down手勢事件;        14)滑動到ListView的邊緣;        15)滑動到ListView的邊緣,又繼續産生move手勢事件;        16)正抛動到ListView邊緣時,又産生了一次down手勢事件。        總體而言,ListView的滑動、抛動兩大滾動機制的原理,還是将ListView中所有的子視圖進行朝上(下)位移,如果存在子視圖在滾動之後,完全離開了ListView的可視範圍,則将這些子視圖完全回收;如果存在滾動之後,ListView的可視範圍中還餘有足夠的空間,則重新綁定子視圖和資料,重用一個子視圖,直到餘下的空間全部被重用的子視圖填充完畢。         而滑動與滾動最顯著的差別則是手指是否還在螢幕之上;具體而言,前者是借助觸摸事件的三個過程(down、move、up),尤其是move手勢事件來進行與手指的實時關聯;而後者,則主要借助OverScroller類,計算抛動的每一幀結果,根據計算結果來更新ListView的目前效果。

        至此ListView的滾動機制一文便結束!          當然 由于本人自身水準所限,文章肯定有一些不對的地方,希望大家指出! 願大家一起進步,謝謝!