前面幾篇文章主要在介紹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: | 在觸摸滾動期間決定速率; |
| 當手指滑動一定距離後,才開始産生滑動效果,此變量表示所謂的“一定距離”。 |
當然,與滑動相關的變量遠遠不止這9個,在下文分析遇到時,在進行說明;除了這些屬性變量外,還有些屬于方法的本地變量,在分析時,在做特殊說明。
1.3滑動變量的相關接口
如果說常量和變量盡可能的來描述一個滑動,那麼與滑動相關的接口則是來定義一個滑動在不同的狀态,不同的時刻下應該做些什麼。此處的接口,除了interface之外還有一些ListView自己定義好了的Runnable。 OnScrollChangeListener接口: 當ListView滾動時,用來回調的接口;該接口主要側重于滾動狀态的改變。接口的内部定義了三個常量,用來描述ListView的3中滾動狀态,分别如下:
SCROLL_STATE_IDLE: | 空閑狀态,及此狀态下ListView沒有滾動 |
| 滑動狀态;即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的可視範圍的算法,如下圖所示(以從上往下為例):
如圖所示,這個黑線框就是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為例):
如圖所示,如果向下滑動的距離超過了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,且抛動過程中的抛動位置會越來越大。如圖所示
當抛動過程中,抛動的位置越來越大,則表示朝下方滾動,即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個觸摸模式的互相轉換可如下圖所示:
如圖所示,圖中一共标出了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的滾動機制一文便結束! 當然 由于本人自身水準所限,文章肯定有一些不對的地方,希望大家指出! 願大家一起進步,謝謝!