編輯:Android資訊
觸摸事件傳遞機制是Android中一塊比較重要的知識體系,了解並熟悉整套的傳遞機制有助於更好的分析各種滑動沖突、滑動失效問題,更好去擴展控件的事件功能和開發自定義控件。
在Android設備中,觸摸事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和雙擊,另外還包括單指操作和多指操作等。一個最簡單的用戶觸摸事件一般經過以下幾個流程:
Android把這些事件的每一步抽象為MotionEvent
這一概念,MotionEvent包含了觸摸的坐標位置,點按的數量(手指的數量),時間點等信息,用於描述用戶當前的具體動作,常見的MotionEvent有下面幾種類型:
ACTION_DOWN
ACTION_UP
ACTION_MOVE
ACTION_CANCEL
其中,ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
就分別對應於上面的手指按下、手指滑動、手指抬起操作,即一個最簡單的用戶操作包含了一個ACTION_DOWN
事件,若干個ACTION_MOVE
事件和一個ACTION_UP
事件。
事件分發過程中,涉及的主要方法有以下幾個:
dispatchTouchEvent
: 用於事件的分發,所有的事件都要通過此方法進行分發,決定是自己對事件進行消費還是交由子View處理onTouchEvent
: 主要用於事件的處理,返回true表示消費當前事件onInterceptTouchEvent
: 是ViewGroup
中獨有的方法,若返回true
表示攔截當前事件,交由自己的onTouchEvent()
進行處理,返回false
表示不攔截我們的源碼分析也主要圍繞這幾個方法展開。
我們從Activity的dispatchTouchEvent
方法作為入口進行分析:
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
會被觸發。如果我們需要知道用戶是不是正在和設備交互,可以在子類中重寫這個方法,去獲取通知(比如取消屏保這個場景)。
然後是調用Activity內部mWindow
的superDispatchTouchEvent
方法,mWindow
其實是PhoneWindow的
實例,我們看看這個方法做了什麼:
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開始分發了,這個方法將在下一節分析。
我們回到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
事件,後續事件也不會傳遞進來。
現在回頭看上一節的第2、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()
進行分發處理:
onInterceptTouchEvent()
,若為true,跳過2步驟;事件傳遞至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
事件,後續事件也不會傳遞進來。
當有人問我關於動畫性能表現不佳問題的時候,我首先會詢問他們是否使用了Hardware Layer層。 你的View可能在執行動畫期間的每一幀都進行重繪,如果使用V
一 IntentService介紹 IntentService定義的三個基本點:是什麼?怎麼用?如何work? 官方解釋如下: //IntentService定義
Android安全加密專題文章索引 Android安全加密:對稱加密 Android安全加密:非對稱加密 Android安全加密:消息摘要Message Dig
本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! 本文將介紹一種有效改變Android按鈕顏色的方法。 按鈕可以在狀