1. 簡介
先來看一個表:
Touch事件相關方法 |
功能 |
Activity |
ViewGroup |
View |
public boolean dispatchTouchEvent(MotionEvent ev) |
事件分發 |
Yes |
Yes |
Yes |
public boolean onInterceptTouchEvent(MotionEvent ev) |
事件攔截 |
No |
Yes |
No |
public boolean onTouchEvent(MotionEvent ev) |
事件響應 |
Yes |
Yes |
Yes |
從表中可以看出,Activity、ViewGroup、View都關心Touch事件,其中ViewGroup的關心的事件有三個:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。Activity和View關心的事件只有兩個:dispatchTouchEvent、onTouchEvent。也就是說只有ViewGroup可以對事件進行攔截。
Android的UI結構是一個樹形結構,樹的最頂層(根節點)是Activity內包含的一個ViewGroup,下面有若干個ViewGroup節點,每個節點下面又有若干個ViewGroup節點或者View節點,以此類推。Touch事件的傳遞和處理與這種結構密切相關。一次完整的觸摸事件由一個ACTION_DOWN、零個或一個或多個ACTION_MOVE、一個ACTION_UP組成。
在分析源碼之前,先簡單了解下這三個函數。
(1) 事件分發: public boolean dispatchTouchEvent(MotionEvent ev)
當Touch事件發生時,Activity的dispatchTouchEvent()方法會以隧道方式(從根節點依次往下傳遞直到最內層子節點,或在中間某一節點中由於某一條件停止傳遞)將事件傳遞給最外層View的dispatchTouchEvent()方法,並由該View的dispatchTouchEvent()方法對事件進行分發。
dispatchTouchEvent 的事件分發邏輯如下:
- return true :事件會分發給當前View並由dispatchTouchEvent()方法進行消費,同時事件會停止向下傳遞。
- return false :將事件返還給當前View的上一級的onTouchEvent()進行消費。(這個上一級可能是Activity,也可能是父View)
- return super.dispatchTouchEvent(ev) :事件會自動的分發給當前View的onInterceptTouchEvent方法。
注意,View響應dispatchTouchEvent()和onInterceptTouchEvent()的前提是可以向該View中添加子View,也就是說該View有子節點才談得上能分發和攔截。
(2) 事件攔截: public boolean onInterceptTouchEvent(MotionEvent ev)
在外層View的dispatchTouchEvent()方法返回super.dispatchTouchEvent(ev)時,事件會自動的分發給當前View的onInterceptTouchEvent()方法。
onInterceptTouchEvent 的事件攔截邏輯如下:
- return true :將對事件進行攔截,並將攔截到的事件交由當前View的onTouchEvent()進行處理。
- return false :將對事件進行放行,當前View上的事件會被傳遞到子View 上,再由子View的dispatchTouchEvent()來繼續對這個事件進行分發。
- return super.onInterceptTouchEvent(ev) :事件默認會被攔截,並將攔截到的事件交由當前View的onTouchEvent()進行處理。
(3) 事件響應: public boolean onTouchEvent(MotionEvent ev)
由(1)和(2)可以知道onTouchEvent()被調用的條件。
onTouchEvent 的事件響應邏輯如下:
- return false :事件將會從當前View向上傳遞,並且都是由上層View的onTouchEvent()來接收,如果傳遞到上層的onTouchEvent()也返回false,那么這 個事件就會"消失",而且接收不到下一次事件。
- return true :接收並消費掉該事件。
- return super.onTouchEvent(ev) :默認處理事件的邏輯和return false相同。
下面看下源碼。
1.1 Activity對Touch事件的處理
當Touch事件發生生,最先被觸發的是Activity的dispatchTouchEvent()函數,再由這個函數觸發根節點的dispatchTouchEvent()。如果想讓Activity不響應觸摸事件,可以直接重寫這個函數。
dispatchTouchEvent()@Activity.java
// 處理觸摸屏事件。可以重寫這個函數來攔截所有的觸摸屏事件,不讓它們分發到window。為了應該被正常處理的觸摸屏幕事件,一定要確保調用這個實現。 public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { // 從ACTION_DOWN事件開始本次觸摸事件 onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { // 會調用window中的根ViewGroup進行事件分發 return true; // 如果子View消費了這個事件,返回true. } return onTouchEvent(ev); // 沒有View處理本次事件,調用Activity的onTouchEvent。 }
onTouchEvent()@Activity.java
// 當一個觸摸屏事件沒有被它下面的任意view處理的時候,調用這個函數。這個處理對於觸摸事件超出window范圍的時候很有用,這個時候沒有view接收到事件。 public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
1.2 View對Touch事件的處理
dispatchTouchEvent()@View.java
// 將屏幕觸摸事件向下傳遞給目標view. public boolean dispatchTouchEvent(MotionEvent event) { ...... if (onFilterTouchEventForSecurity(event)) { // ListenerInfo類保存了設給這個view的各種listener,如OnClickListener,OnTouchListener等。 ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { // 當mOnTouchListener不為null,當前控件是enable的,且mOnTouchListener.onTouch返回true, 那么dispatchTouchEvent返回true. // 從這里可以看到onTouch執行要先於onTouchEvent及onClick return true; } // 上面的條件都不成立,才會執行onTouchEvent() if (onTouchEvent(event)) { // 如果一個控件是可點擊的,那么onTouchEvent必定返回true, 因此dispatchTouchEvent也返回true. return true; } } ...... return false; }
從View的dispatchTouchEvent()的實現知道,如果給一個view設置了OnTouchListener,那么onTouch函數就在dispatchTouchEvent()函數中被回調,只有onTouch()返回false時,才會繼續執行onTouchEvent()。
onTouchEvent()@View.java
// 實現這個方法處理屏幕觸摸手勢事件 // 如果這個方法被用於檢測點擊動作,那么推薦通過調用performClick來實現。這樣做可以確保系統行為一致,包括服從點擊聲音喜好,調度OnClickListener調用,當輔助 // 功能啟用的時候處理ACTION_CLICK ACTION_CLICK。 public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); // 設置按下UI狀態 } // 一個可點擊的但是disabled的view仍然消費touch事件,只是不作出回應。 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 & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 如果我們沒有擁有它,我們應該獲取焦點,而且應該在Touch模式下。 boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // 按鈕在我們實際顯示它按下之前被釋放。現在使它顯示按下狀態(在調度click之前),以確保用戶看到它。 setPressed(true); } if (!mHasPerformedLongPress) { // 這是一個閥門,所以移除長按的檢查 removeLongPressCallback(); // 只有在按下狀態中時才執行click動作。 if (!focusTaken) { // 使用Runnable和post執行performClick,而不是直接執行。這樣可以讓這個view的其它可見狀態在click操作開始前更新。 if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); // 注意,在MotionEvent.ACTION_UP時執行click。 } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // 沿層次結構走,以確定我們是否在一個滾動的容器內。 boolean isInScrollingContainer = isInScrollingContainer(); // 對於在一個滾動容器內的views,延遲一會按下的反饋,以防這是一個滾動。 if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // 如果不是在一個正在滾動的容器內,那么馬上顯示反饋。 setPressed(true); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // 按鈕之外的移動要容忍。 if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // 移除后面長按/閥門的檢查 removeLongPressCallback(); setPressed(false); } } break; } return true; // 如果一個控件是可點擊的,那么點擊該控件時,onTouchEvent的返回值必定是true } return false; }
performClick()@View.java
// 如果定義了OnClickListener,則調用。執行所有與click關聯的動作:報告可訪問性事件,播放按鍵聲音。 public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); return true; } return false; }
setOnClickListener()@View.java
public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); // 如果這個view不是clickable,將它設置為clickable。 } getListenerInfo().mOnClickListener = l; }
如果給一個View設置了OnClickListener,那么onClick()事件是在onTouchEvent()函數中被調用的。
1.3 ViewGroup對Touch事件的處理
onInterceptTouchEvent()@ViewGroup.java
// 實現這個方法來攔截所有觸摸屏移動事件。這可以讓你在事件被分配給孩子的時候看到事件,並在任意時刻獲取當前手勢的所有權。 // 使用這個功能需要特別注意,因為它與View.onTouchEvent有着相當復雜的相互作用,並且使用它還需要以正確的方式實現這個方法。 // 事件將會以下面順序被接收: // (1) ViewGroup會在這里接收到DOWN事件,這個DOWN事件會被這個ViewGroup的一個子View處理掉,或者給這個ViewGroup自己的onTouchEvent函數進行處理。那就意味着ViewGroup應該實現onTouchEvent()函數並且返回true, 這樣你就會繼續看到手勢的后續事件(而不是尋找父View來處理)。 // (2) 通過從ViewGroup自己的onTouchEvent()返回true, ViewGroup的onInterceptTouchEvent()中將不會再收到任何后續事件,即后續事件不用再經過onInterceptTouchEvent()了,所有觸摸的處理都像正常情況一樣發生在onTouchEvent里了。 // (3) 只要ViewGroup從這個函數返回false,接下來的每個事件(包括最后的UP)都會首先傳遞到這里,然后再傳遞給目標View的onTouchEvent()。 // (4) 如果ViewGroup從這個函數返回true,說明從子view攔截了事件,並將它們通過onTouchEvent()傳遞給這個ViewGroup。當前目標View將接收到相同事件,但是Action為ACTION_CANCEL,並且沒有進一步消息在這里傳遞。 public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
dispatchTouchEvent()@ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 處理最初的down事件 if (actionMasked == MotionEvent.ACTION_DOWN) { // 開始一個新的觸摸手勢時,丟掉所有之前的狀態。framework可能由於app切換,ANR或者其它狀態變化而放棄前一個手勢的up或者cancel事件。 cancelAndClearTouchTargets(ev); resetTouchState(); } // 檢查攔截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 是否禁用事件攔截功能,默認為false. if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev);// 根據onInterceptTouchEvent返回值決定是否攔截,返回false表示不攔截,返回true表示攔截。 ev.setAction(action); // 重新保存action,以防它發生變化。 } else { intercepted = false; } } else { // 沒有觸摸目標且action不是最初的DOWN事件,那么這個ViewGroup繼續攔截觸摸事件。 intercepted = true; } // 檢查取消 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // 如果需要,更新觸摸目標列表給向下的指針。 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { // 如果不是CANCEL,而且不攔截事件,則往下走。intercepted的值在前面被賦值。intercepted為true,則事件不再往下傳。 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // DOWN事件這個值通常為0 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // 為該指針id除早期的觸摸目標,以防它們已經不同步了。 removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // 從前往后掃描,找一個可以接收事件的子view。 final View[] children = mChildren; final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = children[childIndex]; if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // 子view已經在它的范圍內接收觸摸事件了。 // 給它新指針,除了它正在處理的指針。 newTouchTarget.pointerIdBits |= idBitsToAssign; break; // 找到了接收事件的target,跳出循環。 } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // dispatchTransformedTouchEvent函數中調用子view的dispatchTouchEvent。 // 子view在它的范圍內接收觸摸事件。 mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = childIndex; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } if (newTouchTarget == null && mFirstTouchTarget != null) { // 沒有找到一個子view來接收事件,將指針指向最近添加的目標。 newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // 分發給觸摸目標 if (mFirstTouchTarget == null) { // 沒有觸摸目標,那么把它當作一個普通的view。 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 分發給觸摸目標,如果我們已經分發給它那么就排除新的觸摸目標。如有必要取消觸摸目標。 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; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } // Update list of touch targets for pointer up or cancel, if needed. // 如果需要,為指針UP或者CANCEL更新觸摸目標列表。 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); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
dispatchTransformedTouchEvent()@ViewGroup.java
// 將手勢事件轉換成特定子view的坐標空間,過濾無關的指針ID,如有必要覆蓋它的action。如果child為null,則取而代之的是假定手勢事件會發送給該ViewGroup。 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // 取消手勢動作是一種特殊情況。不需要執行任何轉換或者過濾操作。重點關注的是action,而不是內容。 final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); // 調用child的dispatchTouchEvent。 } event.setAction(oldAction); return handled; } // 結算要傳遞的指針數量。 final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // 如果因為某種原因,我們在一個不符的狀態中結束了,在這個狀態中看起來我們可能會產生一個不帶指針的手勢事件,那么就放棄這個事件。 if (newPointerIdBits == 0) { return false; } // 如果指針的數量是相等的,那么我們就不需要執行任何代價高的不可逆轉的轉換,那么只要我們小心恢復我們做的任何修改,我們就可以在這次分發中重用手勢事件。 // 否則,我們就需要復制一個手勢事件。 final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } // 執行任何需要的轉換和分發。 if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } handled = child.dispatchTouchEvent(transformedEvent); //若一個控件是可點擊的,則點擊該控件時,dispatchTouchEvent的返回值必定是true } // 完成 transformedEvent.recycle(); return handled; }