觸摸事件傳遞機制是Android中一塊比較重要的知識體系,瞭解並熟悉整套的傳遞機制有助於更好的分析各類滑動衝突、滑動失效問題,更好去擴展控件的事件功能和開發自定義控件。bash
在Android設備中,觸摸事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和雙擊,另外還包括單指操做和多指操做等。一個最簡單的用戶觸摸事件通常通過如下幾個流程:ide
Android把這些事件的每一步抽象爲MotionEvent
這一律念,MotionEvent包含了觸摸的座標位置,點按的數量(手指的數量),時間點等信息,用於描述用戶當前的具體動做,常見的MotionEvent有下面幾種類型:源碼分析
ACTION_DOWN
ACTION_UP
ACTION_MOVE
ACTION_CANCEL
其中,ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
就分別對應於上面的手指按下、手指滑動、手指擡起操做,即一個最簡單的用戶操做包含了一個ACTION_DOWN
事件,若干個ACTION_MOVE
事件和一個ACTION_UP
事件。post
事件分發過程當中,涉及的主要方法有如下幾個:ui
dispatchTouchEvent
: 用於事件的分發,全部的事件都要經過此方法進行分發,決定是本身對事件進行消費仍是交由子View處理onTouchEvent
: 主要用於事件的處理,返回true表示消費當前事件onInterceptTouchEvent
: 是ViewGroup
中獨有的方法,若返回true
表示攔截當前事件,交由本身的onTouchEvent()
進行處理,返回false
表示不攔截咱們的源碼分析也主要圍繞這幾個方法展開。this
咱們從Activity的dispatchTouchEvent
方法做爲入口進行分析:spa
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
複製代碼
這個方法首先會判斷當前觸摸事件的類型,若是是ACTION_DOWN
事件,會觸發onUserInteraction
方法。根據文檔註釋,當有任意一個按鍵、觸屏或者軌跡球事件發生時,棧頂Activity的onUserInteraction
會被觸發。若是咱們須要知道用戶是否是正在和設備交互,能夠在子類中重寫這個方法,去獲取通知(好比取消屏保這個場景)。code
而後是調用Activity內部mWindow
的superDispatchTouchEvent
方法,mWindow
實際上是PhoneWindow的
實例,咱們看看這個方法作了什麼:orm
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
...
}
}
複製代碼
原來PhoneWindow內部調用了DecorView的同名方法,而DecorView實際上是FrameLayout的子類,FrameLayout並無重寫dispatchTouchEvent方法,因此事件開始交由ViewGroup的dispatchTouchEvent開始分發了,這個方法將在下一節分析。cdn
咱們回到Activity的dispatchTouchEvent
方法,注意當getWindow().superDispatchTouchEvent(ev)
這一語句返回false時,即事件沒有被任何子View消費時,最終會執行Activity的onTouchEvent
:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
複製代碼
小結: 事件從Activity的dispatchTouchEvent開始,經由DecorView開始向下傳遞,交由子View處理,若事件未被任何Activity的子View處理,將由Activity本身處理。
由上節分析可知,事件來到DecorView後,通過層層調用,來到了ViewGroup的dispatchTouchEvent方法中:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
...
// 先檢驗事件是否須要被ViewGroup攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 校驗是否給mGroupFlags設置了FLAG_DISALLOW_INTERCEPT標誌位
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 走onInterceptTouchEvent判斷是否攔截事件
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
if (!canceled && !intercepted) {
// 注意ACTION_DOWN等事件纔會走遍歷全部子View的流程
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
// 開始遍歷全部子View開始逐個分發事件
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
// 判斷觸摸點是否在這個View的內部
final View child = children[i];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
...
// 事件被子View消費,退出循環,再也不繼續分發給其餘子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
// addTouchTarget內部將mFirstTouchTarget設置爲child,即不爲null
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
// 事件未被任何子View消費,本身處理
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 將MotionEvent.ACTION_DOWN後續事件分發給mFirstTouchTarget指向的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 若是已經在上面的遍歷過程當中傳遞過事件,跳過本次傳遞
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
return handled;
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// 注意傳參child爲null時,調用的是本身的dispatchTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 默認不攔截事件
return false;
}
複製代碼
這個方法比較長,只要把握住主要脈絡,修枝剪葉後仍是很是清晰的:
(1) 判斷事件是夠須要被ViewGroup攔截
首先會根據mGroupFlags
判斷是否能夠執行onInterceptTouchEvent
方法,它的值能夠經過requestDisallowInterceptTouchEvent
方法設置:
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { // 層層向上傳遞,告知全部父View不攔截事件 mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } } 複製代碼
因此咱們在處理某些滑動衝突場景時,能夠從子View中調用父View的requestDisallowInterceptTouchEvent
方法,阻止父View攔截事件。
若是view沒有設置FLAG_DISALLOW_INTERCEPT
,就能夠進入onInterceptTouchEvent方法,判斷是否應該被本身攔截, ViewGroup的onInterceptTouchEvent直接返回了false,即默認是不攔截事件的,ViewGroup的子類能夠重寫這個方法,內部判斷攔截邏輯。
**注意:**只有當事件類型是ACTION_DOWN
或者mFirstTouchTarget不爲空時,纔會走是否須要攔截事件這一判斷,若是事件是ACTION_DOWN
的後續事件(如ACTION_MOVE
、ACTION_UP
等),且在傳遞ACTION_DOWN
事件過程當中沒有找到目標子View時,事件將會直接被攔截,交給ViewGroup本身處理。mFirstTouchTarget的賦值會在下一節提到。
(2) 遍歷全部子View,逐個分發事件:
執行遍歷分發的條件是:當前事件是ACTION_DOWN
、ACTION_POINTER_DOWN
或者ACTION_HOVER_MOVE
三種類型中的一個(後兩種用的比較少,暫且忽略)。因此,若是事件是ACTION_DOWN
的後續事件,如ACTION_UP
事件,將不會進入遍歷流程!
進入遍歷流程後,拿到一個子View,首先會判斷觸摸點是否是在子View範圍內,若是不是直接跳過該子View; 不然經過dispatchTransformedTouchEvent
方法,間接調用child.dispatchTouchEvent
達到傳遞的目的;
若是dispatchTransformedTouchEvent
返回true,即事件被子View消費,就會把mFirstTouchTarget設置爲child,即不爲null,並將alreadyDispatchedToNewTouchTarget設置爲true,而後跳出循環,事件再也不繼續傳遞給其餘子View。
能夠理解爲,這一步的主要做用是,在事件的開始,即傳遞ACTION_DOWN
事件過程當中,找到一個須要消費事件的子View,咱們能夠稱之爲目標子View
,執行第一次事件傳遞,並把mFirstTouchTarget設置爲這個目標子View
(3) 將事件交給ViewGroup本身或者目標子View處理
通過上面一步後,若是mFirstTouchTarget仍然爲空,說明沒有任何一個子View消費事件,將一樣會調用dispatchTransformedTouchEvent,但此時這個方法的View child
參數爲null,因此調用的實際上是super.dispatchTouchEvent(event)
,即事件交給ViewGroup本身處理。ViewGroup是View的子View,因此事件將會使用View的dispatchTouchEvent(event)方法判斷是否消費事件。
反之,若是mFirstTouchTarget不爲null,說明上一次事件傳遞時,找到了須要處理事件的目標子View,此時,ACTION_DOWN
的後續事件,如ACTION_UP
等事件,都會傳遞至mFirstTouchTarget中保存的目標子View中。這裏面還有一個小細節,若是在上一節遍歷過程當中已經把本次事件傳遞給子View,alreadyDispatchedToNewTouchTarget的值會被設置爲true,代碼會判斷alreadyDispatchedToNewTouchTarget的值,避免作重複分發。
小結: dispatchTouchEvent方法首先判斷事件是否須要被攔截,若是須要攔截會調用
onInterceptTouchEvent
,若該方法返回true,事件由ViewGroup本身處理,不在繼續傳遞。 若事件未被攔截,將先遍歷找出一個目標子View,後續事件也將交由目標子View處理。 若沒有目標子View,事件由ViewGroup本身處理。此外,若是一個子View沒有消費
ACTION_DOWN
類型的事件,那麼事件將會被另外一個子View或者ViewGroup本身消費,以後的事件都只會傳遞給目標子View(mFirstTouchTarget)或者ViewGroup自身。簡單來講,就是若是一個View沒有消費ACTION_DOWN
事件,後續事件也不會傳遞進來。
如今回頭看上一節的第二、3步,不論是對子View分發事件,仍是將事件分發給ViewGroup自身,最後都異曲同工,調用到了View的dispatchTouchEvent
,這就是咱們這一節分析的目標。
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
// 判斷事件是否先交給ouTouch方法處理
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
// onTouch未消費事件,傳給onTouchEvent
if (onTouchEvent(event)) {
return true;
}
}
...
return false;
}
複製代碼
代碼量很少,主要作了三件事:
onTouchEvent
方法繼續處理這樣,咱們的分析轉到了View的onTouchEvent
方法:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
// 若是一個View處於DISABLED狀態,可是CLICKABLE或者LONG_CLICKABLE的話,這個View仍然能消費事件
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away mPrivateFlags |= PRESSED; refreshDrawableState(); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } return true; } return false; } public final boolean isFocusable() { return FOCUSABLE == (mViewFlags & FOCUSABLE_MASK); } public final boolean isFocusableInTouchMode() { return FOCUSABLE_IN_TOUCH_MODE == (mViewFlags & FOCUSABLE_IN_TOUCH_MODE); } 複製代碼
onTouchEvent
方法的主要流程以下:
ACTION_UP
分支,這個分支內部通過重重判斷以後,會調用到performClick方法:public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
複製代碼
能夠看到,若是設置了OnClickListener,就會回調咱們的onClick方法,最終消費事件。
經過上面的源碼解析,咱們能夠總結出事件分發的總體流程:
下面作一個整體歸納:
事件由Activity的dispatchTouchEvent()
開始,將事件傳遞給當前Activity的根ViewGroup:mDecorView,事件開始自上而下進行傳遞,直至被消費。
事件傳遞至ViewGroup
時,調用dispatchTouchEvent()
進行分發處理:
1.檢查送否應該對事件進行攔截:onInterceptTouchEvent()
,若爲true,跳過2步驟; 2.將事件依次分發給子View,若事件被某個View消費了,將再也不繼續分發; 3.若是2中沒有子View對事件進行消費或者子View的數量爲零,事件將由ViewGroup本身處理,處理流程和View的處理流程一致;
事件傳遞至View
的dispatchTouchEvent()
時, 首先會判斷OnTouchListener
是否存在,假若存在,則執行onTouch()
,若onTouch()
未對事件進行消費,事件將繼續交由onTouchEvent
處理,根據上面分析可知,View的onClick
事件是在onTouchEvent
的ACTION_UP
中觸發的,所以,onTouch事件優先於onClick
事件。
若事件在自上而下的傳遞過程當中一直沒有被消費,並且最底層的子View也沒有對其進行消費,事件會反向向上傳遞,此時,父ViewGroup
能夠對事件進行消費,若仍然沒有被消費的話,最後會回到Activity的onTouchEvent
。
若是一個子View沒有消費ACTION_DOWN
類型的事件,那麼事件將會被另外一個子View或者ViewGroup本身消費,以後的事件都只會傳遞給目標子View(mFirstTouchTarget)或者ViewGroup自身。簡單來講,就是若是一個View沒有消費ACTION_DOWN
事件,後續事件也不會傳遞進來。
若是你看到了這裏,以爲文章寫得不錯就給個讚唄?若是你以爲那裏值得改進的,請給我留言。必定會認真查詢,修正不足。謝謝。