當視圖的層次結構比較復雜的時候,觸摸事件的響應流程也變得復雜。
舉例來說,你也許有一天想要制作一個手勢極其復雜的 Activity
來折磨你的用戶,你經過簡單思索,認為其中應該包含一個 PageViewer
,而 PageViewer
中又應包含一個 ListView
。你的 ListView
中的每一項 ( item ) 還需要響應左右滑動的手勢,來顯示刪除記錄的按鈕,按鈕自然要響應點擊的事件,而整個 ListView
需要響應上下滑動的手勢,用來滾動整個列表,同時你還希望通過多個手指左右滑動的手勢,可以使整個 PageViewer
翻頁,甚至你還希望像 iPad 中一樣,響應五指聚攏的手勢,來關閉整個 Activity
…… 到這里還沒完,你還希望,可以通過點擊 ListView
中的某一項,查看詳細信息;長按某一項,可以彈出上下文菜單,來進行修改。
這時候你就不得不弄清楚,觸摸事件到底是如何一步一步從 Activity
傳遞到 PageViewer
,然后再傳遞到 ListView
…… 以便讓每一個層級的 View
攔截自己需要處理的手勢,而把自己不需要的手勢傳遞給其他 View
。你還要弄清楚,怎么區分這次觸摸事件,究竟是一次簡單的點擊,還是一次長按,還是一個復雜手勢的一部分。
下文中,我們將簡單剖析一下 Android 的觸摸傳遞機制。
涉及到的類和方法
總的來說,觸摸傳遞過程是由上至下的。一個典型的觸摸事件,從 Activity
開始,經過根視圖,再經過層層 ViewGroup
,最終傳遞到某一個 View
或 ViewGroup
上,進行處理。主要涉及到的類自然包括 Activity
,ViewGroup
以及 View
了。
首先,在 Activity
和 View
中,都定義了下面兩個方法 (雖然在這兩個類中,這兩個方法的方法名,參數列表和返回值類型完全一樣,但 Activity
並不是 View
的子類,下面的兩個方法在 Activity
和 View
中被單獨定義)。
// 嘗試將觸摸事件交給自己的子視圖 (如果有的話) 處理: 調用子視圖的 dispatchTouchEvent()
// 或者自己處理: 調用自己的 onTouchEvent() 或 OnTouchListener.onTouch()
// 無論是自己的子視圖,還是自己,完成了事件處理,都返回 true
public boolean dispatchTouchEvent(MotionEvent ev)
// 嘗試自己處理觸摸事件. 如果完成處理 (不需要再交給其他 View 處理), 則返回 true
public boolean onTouchEvent(MotionEvent event)
由於 ViewGroup
是 View
的子類,所以自然 ViewGroup
中也存在這兩個方法。在 ViewGroup
中還單獨定義了方法
// 如果事件需要在該 ViewGroup 截斷 (自己處理該事件, 不再傳遞給其子視圖), 則返回 true
public boolean onInterceptTouchEvent(MotionEvent ev)
以上3個方法,通常只有在我們需要自定義 View
時,才需要 Override。對於現成的 View
,我們可以通過 View
(也包括 ViewGroup
) 的 setOnTouchListener()
方法,添加觸摸事件監聽器來監聽觸摸事件,
someView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 嘗試自己處理觸摸事件, 完成處理 (不需要其他 View 再處理), 則返回 true
// ...
}
});
同樣可以起到和 onTouchEvent()
類似的效果。兩者有什么區別,包括前面的幾個方法的具體作用,會在下文中慢慢解釋。
在這之前,我們應當注意到,
-
它們都具有一個
MotionEvent
類型的參數,里面包含有觸摸事件的詳細信息 (包含事件的類型,手指按下還是松開,以及觸摸的具體坐標位置等),本文涉及的大部分方法都有這個參數,后面就不再重復了。限於篇幅,這里對該類的使用就不詳細介紹了。對MotionEvent
有疑問可以參考官方文檔中對 MotionEvent 的描述。 -
它們都返回一個
boolean
值,通過返回true
,來聲明觸摸事件在自己這里已經完成,或者說“消費”掉了。例如,文章開頭的例子中,ListView
的某一項 ( item ) 對應的視圖View
監聽到一個觸摸事件,發現是左右滑動的手勢,該View
就會選擇將這個事件“消費”掉,這樣其父視圖ListView
,PageViewer
以及Activity
就不會重復處理這一事件。
傳遞機制詳解
先分別看看上面的4個方法的具體作用,以及 boolean
返回值的意義。
Activity
先看第一個方法 dispatchTouchEvent()
,該方法是整個觸摸傳遞機制的核心。一般地,父視圖 ( parent view ) 通過調用子視圖 ( child view ) 的 dispatchTouchEvent()
方法完成觸摸事件的向下傳遞。
焦點所在 Activity
的 dispatchTouchEvent()
方法,是整個觸摸事件的“入口”。該方法首先,無條件地,不可被截斷地 (除非你 Override Activity
的 dispatchTouchEvent()
方法),將事件交給它的下屬處理,即調用該 Activity
的根視圖的 dispatchTouchEvent()
方法。如果它的下屬沒有完成該事件的處理 (調用結果返回 false
),則嘗試自己處理,即調用 Activity
自己的 onTouchEvent()
方法。如果仍然不能完成處理 (調用結果返回 false
),則可以認為該事件的處理宣告失敗,整個方法返回 false
(完成了處理則返回 true
)。
注意,如果第一次調用 (即調用下屬視圖的 dispatchTouchEvent()
方法),返回了 true
,表示下屬已經完成了對事件的處理工作,此時不會再調用 Activity
自己的 onTouchEvent()
方法。
因為 Activity
沒有父視圖,自身不能設置觸摸事件的監聽器 OnTouchListener
,也沒有 onInterceptTouchEvent()
方法,情況相對簡單,就不給大家 show 源代碼了。
沒有子視圖的 View
下面我們再看一下另一個比較簡單的情況,即沒有子視圖的 View
(如果套用二叉樹的概念,Activity
是樹根,這里的 View
指的就是樹葉了)。對於這些視圖的 dispatchTouchEvent()
方法,由於沒有下屬可供派遣,事情只能自己解決。如果該 View
被注冊過觸摸事件監聽器 OnTouchListener
,則優先調用 OnTouchListener.onTouch()
方法。如果沒有注冊過監聽器,或者 OnTouchListener.onTouch()
方法沒有完成處理 (調用結果返回 false
),才會再嘗試調用自己的 onTouchEvent()
方法。下面這段 Android 源代碼 (有所刪減),完成了上述邏輯。
// 沒有子視圖的 View 的 dispatchTouchEvent() 方法
public boolean dispatchTouchEvent(MotionEvent event) {
// ...
// View.setOnTouchLisener() 方法設置的觸摸事件監聽者
ListenerInfo li = mListenerInfo;
// 如果設置了監聽者, 優先調用 OnTouchListener.onTouch()
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// 如果 OnTouchListener.onTouch() 完成了事件處理
return true;
}
// 如果監聽者沒有完成事件處理(或者沒有監聽者), 再調用 onTouchEvent()
if (onTouchEvent(event)) {
// 如果 onTouchEvent() 完成了事件處理
return true;
}
// ...
// 如果 OnTouchListener.onTouch() 和 onTouchEvent() 都沒有完成事件處理
return false;
}
再來看 View
中定義的 onTouchEvent()
方法,
// View (包括 ViewGroup) 的 onTouchEvent() 方法
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
// ...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
// 判斷和處理 onClick 和 onLongClick 事件
// ...
return true;
}
// 非 onClick 或 onLongClick 的普通 onTouch 事件
// 默認的 View.onTouchEvent() 不作處理
return false;
}
ViewGroup
並沒有 Override View
中的 onTouchEvent()
,所以這在 ViewGroup
中同樣適用。有兩點需要注意
-
在
onTouchEvent()
中,會判斷觸摸是否構成一次點擊事件,從而交給其他一些監聽器,如onClickListener
(監聽點擊事件),onLongClickListener
(監聽長按事件) 來處理,同時返回true
。如果你需要自定義自己的View
,並建立自己的觸摸事件響應,Override 了原本的onTouchEvent()
方法,點擊事件OnClickLisener.onClick()
等其他一些事件將永遠不會觸發,並且,這將導致一個ClickableViewAccessibility
的編譯器警告。如果你仍然需要響應點擊事件,你需要在 Override 之后的onTouchEvent()
方法中,模仿基類版本,手動判斷觸摸事件是否形成一次點擊,並手動調用performClick()
方法,來觸發點擊事件OnClickLisenner.onClick()
。 -
如果該
View
注冊了觸摸事件監聽器OnTouchListener
,則OnTouchListener.onTouch()
會被優先調用。只有該調用返回false
,或者沒有注冊監聽器時onTouchEvent()
方法才會被調用。如果onTouchEvent()
沒有被調用,點擊事件OnClickListener.onClick()
等,其他一些事件也不會觸發。
ViewGroup
上有老下有小的 ViewGroup
情況最為復雜。我們先看 ViewGroup
的 dispatchTouchEvent()
方法,對於這個方法而言,沒有父視圖的 Activity
和沒有子視圖的 View
(下文簡稱葉子 View
),某種程度上都可以看成 ViewGroup
的特例。
ViewGroup
的 dispatchTouchEvent()
方法被其父視圖 (可能是 Activity
的根視圖,也可能是其他的 ViewGroup
) 調用,dispatchTouchEvent()
方法所要做的事情就是竭盡所能,利用自己的資源 (包括派遣給自己的子視圖) 來處理觸摸事件,並向父視圖反饋處理結果。
與 Activity
中,首先無條件地將觸摸事件派遣給自己的下屬,我們可以通過調用 ViewGroup
的 onInterceptTouchEvent()
方法,決定觸摸事件是否在此 ViewGroup
處截斷。即,如果 onInterceptTouchEvent()
方法返回 true
,則不再繼續傳遞給自己的子視圖,而是 ViewGroup
自己嘗試處理;如果返回 false
,則不截斷 (就像在 Activity
中一樣),首先嘗試將任務派遣給子視圖完成,如果沒有子視圖或子視圖不能完成 (調用子視圖的 dispatchTouchEvent()
方法返回 false
),那么 ViewGroup
不得不嘗試自己處理觸摸事件。邏輯見下圖。
ViewGroup
所謂“自己處理”的方法與葉子 View
相同,如果注冊了觸摸事件監聽器 OnTouchLisener
,則優先調用 OnTouchLisener.onTouch()
方法,如果沒有注冊監聽器,或OnTouchLisener.onTouch()
方法返回 false
,再嘗試調用 ViewGroup
自身的 onTouchEvent()
方法,且 ViewGroup
的 onTouchEvent()
方法完全繼承自 View
,沒有 Override (可以參看上文源代碼,做了點擊事件和長按事件的判斷)。
下面是 ViewGroup
中,dispatchTouchEvent()
的源代碼骨架
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 省略了關於檢查觸摸事件類型的代碼. 這一部分代碼用於處理,
// 組成一次完整的手勢(從 ACTION_DOWN 到 ACTION_DOWN) 的
// 各個觸摸事件之間的關聯性
// ...
boolean handled = false;
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// ...
// 確認是否將事件在此截斷 (不再傳遞給子 View)
final boolean intercepted;
// ...
// 檢查 onInterceptTouchEvent()
intercepted = onInterceptTouchEvent(ev);
// 這里做了簡化, 源代碼中, 考慮的更多的可能發生截斷的復雜情況
// 確認事件是否被取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
TouchTarget newTouchTarget = null;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
// ...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 觸摸事件位置坐標
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 通過子 View 的位置坐標, 確定應該將事件傳遞給哪一個子 View
final View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled();
// 遍歷所有子 View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ? getChildDrawingOrder(
childrenCount, i) : i;
final View child = children[childIndex];
// 確認這個子 View 是否在合適的位置
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child,
null)) {
continue;
}
// 找到了合適位置的子 View
// ...
}
}
// ...
}
}
// 將觸摸事件傳遞下去
if (mFirstTouchTarget == null) {
// 沒有找到合適的子 View
// 將 GroupView 自己當作一個一般的 View 一樣處理觸摸事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 將觸摸傳遞到子 View
// ...
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
// 子 View 處理了觸摸事件
handled = true;
}
// ...
}
// ...
return handled;
}
在 ViewGroup
中,onInterceptTouchEvent()
的默認實現是直接返回 false
,即不截斷事件, 而是將事件傳遞給子視圖處理 (調用子視圖的 dispatchTouchEvent()
方法)。如果需要,可以在自定義的 ViewGroup
中 Override 該函數,改變截斷行為。
// ViewGroup 的 onInterceptTouchEvent() 方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
如果子視圖是 ViewGroup
(還有子視圖),且仍然沒有截斷的話,會繼續調用子視圖的子視圖,如此遞歸進行。觸摸事件派遣的順序是自上而下的。
直到到達某個葉子 View
(不再有子視圖可以派遣),或者某個 ViewGroup
雖然還有子視圖可以派遣,但其要求截斷 (onInterceptTouchEvent()
方法返回 true
)。這時,觸摸事件真正開始嘗試進行處理 (不再派遣給其他 View
,而是由葉子 View
或要求截斷的 ViewGroup
開始自己進行處理)。
如果該 View
完成了觸摸事件的處理 (返回 true
),那么對於其父視圖而言,dispatchTouchEvent()
方法派遣給子視圖的事件圓滿完成,可以向父視圖自己的父視圖宣稱完成事件了 (返回 true
)。反之,如果該 View
自己沒有完成觸摸事件,對於其父視圖 ViewGroup
而言,派遣子視圖並沒有完成事件處理,只好自己處理。如果再次沒有完成,父視圖會向自己的父視圖返回 false
,如果各層 ViewGroup
均不能完成事件處理,最終會調用 Activity
的 onTouchEvent()
方法,做最后的嘗試。整個實際處理過程順序正好相反,是自下而上的。
從 ACTION_DOWN 到 ACTION_UP
從手指按下,觸發 ACTION_DOWN
開始,到手指離開,觸發 ACTION_UP
為止,這兩次觸摸事件,以及這中間到其他觸摸事件 (如 MOVE 事件),會被視作一次手勢。如果我們對一次手勢的 ACTION_DOWN
不感興趣,即我們在監聽器的 onTouch()
方法,或在 View
的 onTouchEvent()
方法中返回 false
。那么我們將不會再接受到該手勢到后續觸摸事件,直到這一手勢結束 (ACTION_UP
)。
另外,在一次完整手勢中,只要 ViewGroup
的 onInterceptTouchEvent()
方法有一次返回 true
,那么該 ViewGroup
將會截斷這次手勢的全部后續觸發事件,並向之前處理事件的子視圖,傳遞一個 ACTION_CANCEL
事件。所以我們應當總是注意捕獲可能的 ACTION_CANCEL
觸摸事件。
總結
在沒有 Override 的情況下,觸摸事件的派遣將不會被截斷,從 Activity
的根視圖,自上而下的派遣到葉子 View
,然后調用該 View
的 onTouchEvent()
(如果注冊了監聽器的話,則優先調用 OnTouchListener.onTouch()
,返回 false
才會再調用 onTouchEvent()
)。如果該 View
不能處理事件(onTouchEvent()
返回了 false
),其父視圖繼續嘗試處理,直到最后,調用 Activity
的 onTouchEvent()
方法。另外,值得注意的是,如果沒有響應一個手勢的開始事件 (ACTION_DOWN
),則不會接到該手勢的后續事件。