技術背景
從 View 體系中認識 Touch 事件傳遞,暫時留一條線索:
" View 最原始的事件從哪裏來? ”
從 WindowCallbacKWrapper開始的。
那麼,我們開始吧!
tip:閱讀源碼前,建議讀懂 Android View體系之基礎常識及技巧。
千里之行,始於Activity
從 window
層開始下發事件後, Activity
開始處理事件,會調用 ViewGroup#dispatchTouchEvent
Activity.java public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } PhoneWindow.java @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } DecorView.java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... } |
Activity#dispatchTouchEvent
方法最後會通過 DecorView
觸發 ViewGroup#dispatchTouchEvent
開始分發事件。
總結:Activity
下發 Touch
事件到 DecorView
並由 DecorView
開始向下傳遞。
ViewGroup之核心分發
DecorView
調用 dispatchTouchEvent
分發 Touch
事件。代碼很長,可是不難,邏輯比較清晰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
@Override public boolean dispatchTouchEvent(MotionEvent ev) { //默認部分下發 Touch 事件 boolean handled = false; // 1. 檢測是否分發Touch事件(判斷窗口是否被遮擋住) // 如果該 Touch 事件沒有被窗口遮擋,則繼續下面邏輯 if (onFilterTouchEventForSecurity(ev)) { // 獲取 Touch Action final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 2. 判斷事件是否是點擊事件 // 清空所有接收觸摸事件View的引用 // 設置mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT,默認允許攔截事件 // 設置mNestedScrollAxes = SCROLL_AXIS_NONE,默認視圖不滾動 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } // 3. 判斷事件是否需要攔截 - intercepted // 判斷是否運行不允許攔截 // 如果允許攔截,則通過 onInterceptTouchEvent 方法返回 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { //說明傳遞的事件不是ACTION_DOWN,意味着ACTION_DOWN已經被攔截過了。 intercepted = true; } // 4. 判斷事件是否取消事件 - canceled // 如果 view.mPrivateFlags 被設置 FLAG_CANCEL_NEXT_UP_EVENT,則該 view 已經脫離視圖 // 置 view.mPrivateFlags 標誌 // 如果 當前標誌爲 FLAG_CANCEL_NEXT_UP_EVENT 或者 接收 MotionEvent.ACTION_CANCEL 事件,返回 true // 也就是說,如果View detach時,則calceled返回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; // 5. 如果事件沒有被取消且沒有被攔截,走下面邏輯判斷是否需要傳遞 if (!canceled && !intercepted) { // 忽略:檢測無障礙焦點 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; // 6. 如果是點擊事件,區分單指,多指,或者hover(類似鼠標懸浮) // actionIndex 返回 0 ,id則:第一指id爲0,第二指id爲1,依次遞增 // idBitsToassign,第一指爲1,第二指爲2,第三指4,依次指數遞增 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; //清除當前手指觸摸的target引用 removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // 7. 掃描可傳遞的子 View 列表,按照Z軸座標大小排序返回列表 preorderedList // 遍歷子 view 列表並結合 preorderedList 找出符合繪製條件的 view final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); //忽略:如果該 View 存在無障礙焦點,則跳過 if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } // 8. 判斷子 view 是否不可見或者不在子 view 的範圍內 // 如果滿足,則跳過 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } // 9. 遍歷 target 鏈表,找到當前 child 對應的 target // 如果存在,則把手指的觸摸 id 賦值 pointerIdBits 遍歷 // 這個比較難理解,舉個栗子。 // 假如我食指按在一個 view(A) 上,然後中指在食指未起來之前又按在 view(A) // 上,則會把後者的 idBitsToAssign 更新到指向view(A) 的 newTouchTarget 對象上 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } // 清除子 view.mPrivateFlags PFLAG_CANCEL_NEXT_UP_EVENT 標誌 resetCancelNextUpFlag(child); // 10. 返回下發 Touch 事件結果。 // 如果沒有子 view,則返回 view#dispatchTouchEvent 結果,實際上這裏會發生遞歸。 // 如果子 view 消費了 Touch 事件,則會添加到 target 鏈接 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } // 11. 如果沒有子 view 沒有消費 Touch 事件且 mFirstTouchTarget 卻不爲 null // 這就鬱悶了,比較難理解,舉個栗子。 // 前一個 if 條件是 newTouchTarget == null && childrenCount != 0 // 如果我第一根手指按在 viewGroup(A)中 textview(a) // 同時,另一根手指按在除 textview(a)外空白區域(viewGroup(A)內) if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // 12. 如果 mFirstTouchTarget == null 意味着 沒有任何 view 消費該 Touch 事件 // 則 viewGroup 會調用dispatchTransformedTouchEvent處理,但是child==null,會調用 // view#dispatchTouchEvent處理。 if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 13. 當前有子 view 消費了 Touch 事件 TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; //如果當前 target是新加入 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { //是否需要向子 view 傳遞 ACTION_CANCEL 事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 14. 遞歸返回事件分發結果 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; } } // 14. 檢測是否是取消標誌 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; } |
如果看完上述註釋還有點蒙,一定要多擼幾次源碼。有幾個點想說明一下,可能大家會好理解要一點。
TouchTarget mFirstTouchTarget
的作用mFirstTouchTarget
貫穿dispatchTouchEvent
流程,實際上它是一個鏈表,用於記錄所有接受Touch
事件的 子view
。在日常開發中有沒有遇到過這樣一個邏輯“ 如果一個view
沒有接收過ACTION_DOWN
的事件,那麼後續ACTION_MOVE
和ACTION_UP
也一定不會分發到這個view
。 ”。這個邏輯是基於mFirstTouchTarget
記錄的view
實現的。
總結: 根據上述註釋邏輯鏈。
- 過濾’不合法’的
Touch
事件; - 如果是 MotionEvent.ACTION_DOWN ,則初始化一些狀態;
- 判斷事件是否需要攔截,是否需要取消;
- 如果不需要攔截&不是取消事件,則會向子
view
下發Touch
事件; - 如果沒有任何子
view
消費事件,則會自己處理,如果已有子view
消費事件,判斷當前新處理的target
對象是否是mFirstTouchTarget
鏈表最新一個,如果是則默認爲當前傳遞已經傳遞事件,否則返回子view
遞歸結果。
上述有兩個遞歸,在 註釋10
和 註釋14
,這裏你可能會有疑惑,這兩個的關係是什麼。註釋10
實際上是返回以 viewGroup 爲根節點的 view 下是否有節點消費點擊事件,如果有則記錄下當前子 view。註釋14
實際上是返回以 viewGroup 爲根節點的 view 下是否有節點消費事件。
ViewGroup之遞歸入口
在上一章節,dispatchTouchEvent
多次調用 dispatchTransformedTouchEvent
,這裏做下簡單分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { // 1. 是否下發 Touch 事件 final boolean handled; // 2. 如果事件傳遞已經取消 或者當前事件是 ACTION_CANCEL // 如果沒有子 view,則返回 view#dispatchTouchEvent // 如果有子 view,則 返回 child#dispatchTouchEvent 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); } event.setAction(oldAction); return handled; } // 3. 計算當前新手指id final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; if (newPointerIdBits == 0) { return false; } // 4. 處理事件轉化 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); } // 5. 返回是否傳遞 Touch 事件 // 如果沒有子 view,則返回 view#dispatchTouchEvent // 如果有子 view,則 返回 child#dispatchTouchEvent 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); } // Done. transformedEvent.recycle(); return handled; } |
總結:dispatchTransformedTouchEvent
會對非 MotionEvent.ACTION_CANCEL
事件做轉化並遞歸返回所有事件的下發結果。
View也可分發事件?
既然不是 view
,那麼 dispatchTouchEvent
應該不是屬於下發範疇的,那會是什麼呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public boolean dispatchTouchEvent(MotionEvent event) { //... boolean result = false //停止滑動 if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } // 1.過濾不合法的 Touch 事件 if (onFilterTouchEventForSecurity(event)) { // 2. 如果 view 可用且當前事件被當做拖拽事件處理,返回true if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } // 3. 如果 view 可用監聽且 OnTouchListener 處理了 touch 事件 ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 4. 如果 onTouchEvent 消費了事件 if (!result && onTouchEvent(event)) { result = true; } } //... return result; } |
上述邏輯表明有三種場景下會返回 true
結果。
兩種場景爲:第一種是拖拽場景,比如listview等控件存在這種邏輯;另一種是開發者設置了 OnTouchListener 對象並在 onTouch
函數中處理並返回 true
結果。
最後一種場景爲普遍場景,及如果沒有上述兩種場景且是當前是最外層 view
時(事件已經無法再傳遞),則會調用自身的 onTouch
方法處理。
總結:view#dispatchTouchEvent
會在事件下發鏈末端調用,並把當前 view
的 onTouch
返回值作爲 dispatchTouchEvent
返回值。
回看ViewGroup如何攔截
上述篇章的下發邏輯都需要判斷 Touch 事件是否需要被攔截,先看看代碼。
1 2 3 4 5 6 7 8 9 10 |
public boolean onInterceptTouchEvent(MotionEvent ev) { //判斷是否是鼠標事件且是滾輪滑動 if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; } |
上面的代碼在絕大部分情況下都返回 false
。除非你鼠標事件且在上面滾動,這個場景很像你在滾動網頁一樣,那麼當前的頁面就會攔截滾動事件進行頁面滾動。
值得注意的是:如果你在該方法返回 true
進行攔截,那麼你會走下面的調用邏輯:
- viewGroup#dispatchTouchEvent
- viewGroup#dispatchTransformedTouchEvent
- view#dispatchTouchEvent
- view#onTouchEvent
總結:viewGroup#onInterceptTouchEvent
是 ViewGroup 特有的方法。默認情況下 ViewGroup 不會攔截 Touch 事件,如果攔截了 Touch 事件,則會交給 View#onTouch
進行處理。
事件的宿命onTouchEvent
這個方法是處理 Touch 事件,並返回結果給 dispatchTouchEvent
的。可以理解爲:它決定了某個 view
是否真正消費 Touch 事件。直接看源碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 1. 判斷是否是 DISABLED 的 view // 如果是,且已經按下之後擡起,則會消費掉 Touch 事件 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; return clickable; } //2. 如果設置了 mTouchDelegate,則會直接返回 true if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } //3. 如果可點擊或者顯示了 toolTip,則會開始判斷處理 Touch 事件 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { //4. 處理 MotionEvent.ACTION_UP case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } // 5. 顯示 toolTip 時清空對應狀態,返回 true if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } // 確保用戶看到已按壓狀態 if (prepressed) { setPressed(true, x, y); } // 6. 如果調用長按行爲或者且不忽略下一次事件(觸筆) // 移出長按回調 // 如果我們在按壓狀態下,則 post PerformClick 對象(runnable) // 值得一提的是,PerformClick 對象的 run 方法實際上也是調用 // performClick 方法,該方法調用 view 的 onClickListener#onClick 方法 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { removeLongPressCallback(); if (!focusTaken) { 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)) { mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; // 7. 處理 MotionEvent.ACTION_DOWN 事件 case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; // 8. 如果不可點擊,則檢測是否可以長按 // 如果可以,則發送一個延遲 500 ms 的長按事件 if (!clickable) { checkForLongClick(0, x, y); break; } // 9. 檢測是否觸發是鼠標右鍵類行爲,如果是則直接跳過 if (performButtonActionOnTouchDown(event)) { break; } // 10. 是否在可滑動的容器中 // 該方法會遞歸查詢每一個 viewGroup 容器是是否會支持當用戶嘗試滑動 // 內容時阻止pressed state的出現,該 pressed state 會被延遲反饋 boolean isInScrollingContainer = isInScrollingContainer(); if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { //設置按壓狀態,檢測長按 setPressed(true, x, y); checkForLongClick(0, x, y); } break; // 11.處理 MotionEvent.ACTION_CANCEL 事件 // 設置非按壓狀態,清空tap和長按回調,重置狀態 case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; // 12. 處理 MotionEvent.ACTION_MOVE case MotionEvent.ACTION_MOVE: // 如果可以點擊,則需要處理 Hotspot // 這個效果是在5.0以後出現,主要是處理 RippleDrawable 的效果 if (clickable) { drawableHotspotChanged(x, y); } // 如果移出了 view 的範圍,則需要重置狀態 if (!pointInView(x, y, mTouchSlop)) { removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } return false; } |
上述代碼中,有兩處 callback 的邏輯你可能還沒有完全明白,一個是 TapCallback,一個是 LongPressCallback,分別對應 mPendingCheckForTap
和 mPendingCheckForLongPress
,看下完整的代碼。
// 代碼段1,類 CheckForTap // run 內實際上是調用 代碼段5 private final class CheckForTap implements Runnable { public float x; public float y; @Override public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; setPressed(true, x, y); checkForLongClick(ViewConfiguration.getTapTimeout(), x, y); } } // 代碼段2,延遲發送 mPendingCheckForTap // ViewConfiguration.getTapTimeout() == 100 ms if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); // 代碼段3,移出 mPendingCheckForTap 對象 private void removeTapCallback() { if (mPendingCheckForTap != null) { mPrivateFlags &= ~PFLAG_PREPRESSED; removeCallbacks(mPendingCheckForTap); } } // 代碼段4,類 CheckForLongPress // run 內會判斷狀態並簡介調用 view.OnLongClickListener#onLongClick private final class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; private float mX; private float mY; private boolean mOriginalPressedState; @Override public void run() { if ((mOriginalPressedState == isPressed()) && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick(mX, mY)) { mHasPerformedLongPress = true; } } } //... } // 代碼段5,延遲檢測發送處理長按行爲 // ViewConfiguration.getLongPressTimeout() == 500 ms private void checkForLongClick(int delayOffset, float x, float y) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); mPendingCheckForLongPress.rememberPressedState(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } } // 代碼段6,移出 mPendingCheckForTap 對象 private void removeLongPressCallback() { if (mPendingCheckForLongPress != null) { removeCallbacks(mPendingCheckForLongPress); } } |
代碼段1
會調用 代碼段5
,實際上也是延遲發送 “處理長按行爲”。和直接調用 代碼5
不同,代碼5
中延遲 500-delayOffset
ms 執行 “處理長按行爲”,而源碼的調用基本都是默認 delayOffset = 0
。代碼1 則以 delayOffset = 100
先延遲 100 ms之後延遲 400 ms 發送處理長按行爲,同樣需要 500 ms 纔會支持 “處理長按行爲”。那到底爲啥要這麼做呢?
原因是當前處理的 view 位於可滑動的容器內需要延遲處理接收的按壓事件。這樣講有點抽象,你可以這樣理解,android 把 MotionEvent.ACTION_DOWN
場景區分爲 滑動(scroll)
和 輕敲(tap)
,用延遲的時間來判斷手勢已經發生了位移。如果發生了位移,則還依然需要保持判斷有效長按時間(500 ms)不變,所以會追加 400 ms延遲來 post 一個 “處理長按行爲” 任務。
上述6個代碼段用於加深理解 onTouchEvent
內事件的處理而已。從上上段代碼上看,我們總結下整個流程:
- 如果
view
不可用則根據是否可點擊來直接消費MotionEvent.ACTION_UP
- 如果
view
設置了mTouchDelegate
,則默認消費 Touch 事件 - 如果
view
可點擊或者在 tooltip 顯示狀態下默認消費事件,否則返回 false 給dispatchTouchEvent
。MotionEvent.ACTION_UP
分支會設置按壓狀態,觸發點擊或長按事件,最後重置狀態MotionEvent.ACTION_DOWN
分支延遲發送“處理長按行爲”MotionEvent.ACTION_CANCEL
分支重置處理 Touch 過程中設置的狀態MotionEvent.ACTION_MOVE
分支處理滑動 RippleDrawable 效果並在手勢滑出 View 範圍情況下重置狀態
總結: onTouchEvent
是真正完成對 Touch 事件的處理,並把處理結果作爲dispatchTouchEvent
的遞歸結果。
5個案例加強理解
GitHub鏈接上有本次 Touch傳遞測試代碼
測試案例兩個 viewGroup
和 一個 view
- 場景一:
View3#onTouchEvent
返回true
消費所有事件,上層不攔截。
場景一可知:View3
消費所有事件並返回 true
,對於上層下發的任何事件,dispatchTouchEvent
都返回 true
。
- 場景二:
View3#onTouchEvent
返回false
不消費 Touch 事件,上層不攔截,Linearlayout2
返回true
消費所有 Touch 事件。
場景二可知:如果末層不消費所有事件,則 ACTION_DOWN
會開始從末層向上傳遞。Linearlayout2
消費了ACTION_DOWN
之後,其及上層dispatchTouchEvent
都返回 true
。ACTION_DOWN
之後的事件序列(如ACTION_MOVE,ACTION_UP)都會往Linearlayout2
分發,其下層就再也收不到後續事件了。
- 場景三:
Linearlayout2#onInterceptTouchEvent
返回true
攔截 Touch 事件,但是Linearlayout2#onTouchEvent
返回false
不消費事件。
場景三可知:Linearlayout2
攔截了 ACTION_DOWN
之後,其子 View 再也收不到任何事件,其消費結果由 onTouchEvent
決定,如果不消費,則往上層傳,直到找到某層消費事件。如果沒有任何一層消費,則後續事件序列也不會下發了。
- 場景四:
Linearlayout2#onInterceptTouchEvent
返回true
攔截 Touch 事件,但是Linearlayout2#onTouchEvent
返回true
消費事件。
場景四可知:Linearlayout2
攔截了 ACTION_DOWN
之後,其子 View 再也收不到任何事件,如果消費 ACTION_DOWN
,則後續事件序列都往Linearlayout2
下發。
- 場景五:
View3#onTouchEvent
返回true
消費所有除ACTION_CANCEL
事件,但是Linearlayout2#onInterceptTouchEvent
攔截了ACTION_MOVE
事件且不消費任何事件。
場景五可知:ACTION_DOWN
傳遞到 View3
被其消費,後續序列事件本應該傳遞到 View3
。當 ACTION_MOVE
被 Linearlayout2
攔截之後,無論是否消費,View3
再也收不到 ACTION_MOVE
及其後續的事件序列,但是會在事件被一次攔截時收到 ACTION_CANCEL
,是否消費 ACTION_CANCEL
的結果會被當做此次傳遞的結果返回。此後,此次ACTION_MOVE
後續的事件序列往 Linearlayout2
下發。
2張流程圖看懂沒?
出處: yummyLau,原文鏈接(https://yummylau.com/2018/03/05/源碼解析_2018-03-05_Touch事件源碼解析/)