看完本文你將了解:
- ACTION_CANCEL的觸發(fā)時機
- 滑出子View區(qū)域會發(fā)生什么椒功?為什么不響應(yīng)
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時同樣也會導(dǎo)致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()
等邏輯肥矢。