Android中ACTION_CANCEL的觸發(fā)機制與滑出子view的情況
看完本文你將了解:
- ACTION_CANCEL的觸發(fā)時機
- 滑出子View區(qū)域會發(fā)生什么?為什么不響應onClick()事件
首先看一下官方的解釋:
/** * Constant for {@link #getActionMasked}: The current gesture has been aborted. * You will not receive any more points in it. You should treat this as * an up event, but not perform any action that you normally would. */ public static final int ACTION_CANCEL = 3;
說人話就是:當前的手勢被中止了,你不會再收到任何事件了,你可以把它當做一個ACTION_UP事件,但是不要執(zhí)行正常情況下的邏輯。
ACTION_CANCEL的觸發(fā)時機
有四種情況會觸發(fā)ACTION_CANCEL
:
- 在子View處理事件的過程中,父View對事件攔截
- ACTION_DOWN初始化操作
- 在子View處理事件的過程中被從父View中移除時
- 子View被設(shè)置了PFLAG_CANCEL_NEXT_UP_EVENT標記時
1,父view攔截事件
首先要了解ViewGroup什么情況下會攔截事件,Look the Fuck Resource Code:
/** * {@inheritDoc} */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; ... // Check for interception. 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 { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } ... } ... }
有兩個條件
- MotionEvent.ACTION_DOWN事件或者mFirstTouchTarget非空也就是有子view在處理事件
- 子view沒有做攔截,也就是沒有調(diào)用
ViewParent#requestDisallowInterceptTouchEvent(true)
如果滿足上面的兩個條件才會執(zhí)行onInterceptTouchEvent(ev)
。
如果ViewGroup攔截了事件,則intercepted
變量為true,接著往下看:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { ... // Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 當mFirstTouchTarget != null,也就是子view處理了事件 // 此時如果父ViewGroup攔截了事件,intercepted==true intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } ... // Dispatch to touch targets. if (mFirstTouchTarget == null) { ... } else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { ... } else { // 判斷一:此時cancelChild == true final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 判斷二:給child發(fā)送cancel事件 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } ... } ... } } ... } ... return handled; }
以上判斷一處cancelChild
為true,然后進入判斷二中一看究竟:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { // 將event設(shè)置成ACTION_CANCEL event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { ... } else { // 分發(fā)給child handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } ... }
當參數(shù)cancel為ture時會將event設(shè)置為MotionEvent.ACTION_CANCEL,然后分發(fā)給child。
2,ACTION_DOWN初始化操作
public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. // 取消并清除所有的Touch目標 cancelAndClearTouchTargets(ev); resetTouchState(); } ... } ... }
系統(tǒng)可能會由于App切換、ANR等原因丟失了up,cancel事件。
因此需要在ACTION_DOWN時丟棄掉所有前面的狀態(tài),具體代碼如下:
private void cancelAndClearTouchTargets(MotionEvent event) { if (mFirstTouchTarget != null) { boolean syntheticEvent = false; if (event == null) { final long now = SystemClock.uptimeMillis(); event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); event.setSource(InputDevice.SOURCE_TOUCHSCREEN); syntheticEvent = true; } for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { resetCancelNextUpFlag(target.child); // 分發(fā)事件同情況一 dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits); } ... } }
PS:在dispatchDetachedFromWindow()
中也會調(diào)用cancelAndClearTouchTargets()
3,在子View處理事件的過程中被從父View中移除時
public void removeView(View view) { if (removeViewInternal(view)) { requestLayout(); invalidate(true); } } private boolean removeViewInternal(View view) { final int index = indexOfChild(view); if (index >= 0) { removeViewInternal(index, view); return true; } return false; } private void removeViewInternal(int index, View view) { ... cancelTouchTarget(view); ... } private void cancelTouchTarget(View view) { TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (target.child == view) { ... // 創(chuàng)建ACTION_CANCEL事件 MotionEvent event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); event.setSource(InputDevice.SOURCE_TOUCHSCREEN); 分發(fā)給目標view view.dispatchTouchEvent(event); event.recycle(); return; } predecessor = target; target = next; } }
4,子View被設(shè)置了PFLAG_CANCEL_NEXT_UP_EVENT標記時
在情況一種的兩個判斷處:
// 判斷一:此時cancelChild == true final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 判斷二:給child發(fā)送cancel事件 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; }
當 resetCancelNextUpFlag(target.child)
為true時同樣也會導致cancel,查看代碼:
/** * Indicates whether the view is temporarily detached. * * @hide */ static final int PFLAG_CANCEL_NEXT_UP_EVENT = 0x04000000; private static boolean resetCancelNextUpFlag(View view) { if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) { view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT; return true; } return false; }
根據(jù)注釋大概意思是,該view暫時detached,detached是什么意思?就是和attached相反的那個,具體什么時候打了這個標記,我覺得沒必要深究。
以上四種情況最重要的就是第一種,后面的只需了解即可。
滑出子View區(qū)域會發(fā)生什么?
了解了什么情況下會觸發(fā)ACTION_CANCEL
,那么針對問題:滑出子View區(qū)域會觸發(fā)ACTION_CANCEL
嗎?這個問題就很明確了:不會。
實踐是檢驗真理的唯一標準,代碼擼起來:
public class MyButton extends androidx.appcompat.widget.AppCompatButton { @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: LogUtil.d("ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: LogUtil.d("ACTION_MOVE"); break; case MotionEvent.ACTION_UP: LogUtil.d("ACTION_UP"); break; case MotionEvent.ACTION_CANCEL: LogUtil.d("ACTION_CANCEL"); break; } return super.onTouchEvent(event); } }
一波操作以后日志如下:
(MyButton.java:32) -->ACTION_DOWN
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:39) -->ACTION_UP
滑出view后依然可以收到ACTION_MOVE
和ACTION_UP
事件。
為什么有人會認為滑出view后會收到ACTION_CANCEL
呢?
我想是因為滑出view后,view的onClick()
不會觸發(fā)了,所以有人就以為是觸發(fā)了ACTION_CANCEL
。
那么為什么滑出view后不會觸發(fā)onClick
呢?再來看看View的源碼:
在view的onTouchEvent()
中:
case MotionEvent.ACTION_MOVE: // Be lenient about moving outside of buttons // 判斷是否超出view的邊界 if (!pointInView(x, y, mTouchSlop)) { // Outside button if ((mPrivateFlags & PRESSED) != 0) { // 這里改變狀態(tài)為 not PRESSED // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; } } break; case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; // 可以看到當move出view范圍后,這里走不進去了 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ... performClick(); ... } mIgnoreNextUpEvent = false; break;
1,在ACTION_MOVE
中會判斷事件的位置是否超出view的邊界,如果超出邊界則將mPrivateFlags
置為not PRESSED
狀態(tài)。
2,在ACTION_UP
中判斷只有當mPrivateFlags
包含PRESSED
狀態(tài)時才會執(zhí)行performClick()
等。
因此滑出view后不會執(zhí)行onClick()
。
結(jié)論:
- 滑出view范圍后,如果父view沒有攔截事件,則會繼續(xù)受到
ACTION_MOVE
和ACTION_UP
等事件。 - 一旦滑出view范圍,view會被移除
PRESSED
標記,這個是不可逆的,然后在ACTION_UP
中不會執(zhí)行performClick()
等邏輯。
到此這篇關(guān)于Android中ACTION_CANCEL的觸發(fā)機制與滑出子view的情況的文章就介紹到這了,更多相關(guān)Android ACTION_CANCEL內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android音頻錄制MediaRecorder之簡易的錄音軟件實現(xiàn)代碼
這篇文章主要介紹了Android音頻錄制MediaRecorder之簡易的錄音軟件實現(xiàn)代碼,有需要的朋友可以參考一下2014-01-01Android開發(fā)之多線程中實現(xiàn)利用自定義控件繪制小球并完成小球自動下落功能實例
這篇文章主要介紹了Android開發(fā)之多線程中實現(xiàn)利用自定義控件繪制小球并完成小球自動下落功能的方法,涉及Android多線程編程及圖形繪制相關(guān)技巧,需要的朋友可以參考下2015-12-12Android?IntentFilter的匹配規(guī)則示例詳解
這篇文章主要為大家介紹了Android?IntentFilter的匹配規(guī)則示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12Android中使用Gradle來構(gòu)建App項目的入門指南
Gradle是Java世界中一個高人氣自動化構(gòu)建工具,在安卓開發(fā)領(lǐng)域同樣備受追捧,這里為大家?guī)鞟ndroid中使用Gradle來構(gòu)建App項目的入門指南,來看看Gradle的作用與基本結(jié)構(gòu).2016-06-06