前言
對于Android MotionEvent,我們平時大多關(guān)注的是ACTION_DOWN、ACTION_UP、ACTION_MOVE,本篇將重點分析ACTION_CANCEL 產(chǎn)生的原因及其滑動事件的處理。
通過本篇文章,你將了解到:
1、ACTION_CANCEL 產(chǎn)生的原因
2、手指離開當前View時事件處理
3、手指離開屏幕時事件處理
1、ACTION_CANCEL 產(chǎn)生的原因
從ViewGroup 入手分析
事件分發(fā)是從ViewGroup-->View,因此想要知道View是否收到ACTION_CANCEL,需要從ViewGroup入手,而ViewGroup 分發(fā)的重點即在dispatchTouchEvent(xx)里。

先看看dispatchTouchEvent(xx) 代碼,之前的文章有詳細分析過,此次重點關(guān)注
ACTION_CANCEL的處理邏輯:
#ViewGroup.java
@Override
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) {
//首次Down事件處理------------>(1)
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//ViewGroup是否攔截事件
...
//是否發(fā)送取消事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//尋找接收了Down事件的子View(子布局)
...
if (mFirstTouchTarget == null) {
//沒有子View(子布局)消費事件,于是事件流轉(zhuǎn)到ViewGroup onTouchEvent
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//有子View(子布局)消費事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
//遍歷消費鏈
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//已經(jīng)消費過,直接返回
handled = true;
} else {
//判斷是否需要發(fā)送取消事件------------>(2)
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//分發(fā)事件------------>(3)
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
//如果是取消事件,則子View(子布局)沒必要消費了,因此從消費鏈里摘除
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
}
...
return handled;
}
上面列出了三個重點,將一一分析:
(1)
主要是cancelAndClearTouchTargets(xx)方法:
#ViewGroup.java
private void cancelAndClearTouchTargets(MotionEvent event) {
//子View(子布局) 消費了Down事件
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
...
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
//分發(fā)cancel 事件
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
...
}
}
可以看出,最終調(diào)用了dispatchTransformedTouchEvent(xx)發(fā)送cancel事件。
按照正常流程來說,因為收到Down事件時,mFirstTouchTarget==null,因此此處通常不會執(zhí)行發(fā)送cancel事件。
(2)
resetCancelNextUpFlag(xx)方法,顧名思義:重置取消標記。
#ViewGroup.java
private static boolean resetCancelNextUpFlag(@NonNull View view) {
if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
//之前設(shè)置過取消標記,此處重置
view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
//返回true
return true;
}
return false;
}
PFLAG_CANCEL_NEXT_UP_EVENT 標記的作用是記錄View是否被臨時移出Window。比如View.performButtonActionOnTouchDown(xx)處理鼠標相關(guān)的問題,這個值平時也很少用到。
既要判斷resetCancelNextUpFlag(xx)返回值,也要判斷intercepted值:ViewGroup是否攔截并消費了事件。
若是兩者之一有一者滿足,則認為需要發(fā)送cancel事件。
(3)
(2)點僅僅是判斷是否需要發(fā)送cancel事件,真正發(fā)送事件的方法是dispatchTransformedTouchEvent(xx):
#ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
//判斷參數(shù)cancel 是否為true
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
//將event 事件設(shè)置為cancel
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
//ViewGroup自己消費
handled = super.dispatchTouchEvent(event);
} else {
//子View(子布局) 消費
handled = child.dispatchTouchEvent(event);
}
//處理后,將action重置
event.setAction(oldAction);
return handled;
}
//正常事件流程
}
結(jié)合上述3點可知,在ViewGroup.dispatchTouchEvent(xx)里發(fā)送cancel事件,常用的判斷即是:
1、ViewGroup是否攔截消費了事件,若是則給子View(子布局)發(fā)送cancel事件。
2、發(fā)送給子View cancel事件后,后續(xù)的事件將不會發(fā)給子View。
那么除了ViewGroup 攔截消費事件,還有哪些地方觸發(fā)發(fā)送cancel 事件呢?
ViewGroup 移除View
想象一種場景:
手指在View 上滑動,在此過程中,View 被移出ViewGroup。
來看ViewGroup.remove(xx)的實現(xiàn):
removeView(view)-->removeViewInternal(view)-->removeViewInternal(index, view)
核心功能在removeViewInternal(index, view) 實現(xiàn):
#ViewGroup.java
private void removeViewInternal(int index, View view) {
...
view.clearAccessibilityFocus();
//發(fā)送cancel 事件
cancelTouchTarget(view);
cancelHoverTarget(view);
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
addDisappearingView(view);
} else if (view.mAttachInfo != null) {
//調(diào)用detached
view.dispatchDetachedFromWindow();
}
...
}
可以看出,當View 從ViewGroup 移除后,若是它已經(jīng)消費了事件,那么將會收到cancel 事件。
使用如下代碼測試:
viewGroup.postDelayed(new Runnable() {
@Override
public void run() {
viewGroup.removeAllViews();
}
}, 3000);
手指按在View 上,3s后將View 從ViewGroup里移除。
Window 移除View
當調(diào)用WindowManager.removew(view)方法時,最終會調(diào)用到ViewGroup.dispatchDetachedFromWindow(xx)方法:
##ViewGroup.java
void dispatchDetachedFromWindow() {
//發(fā)送 cancel 事件
cancelAndClearTouchTargets(null);
...
}
使用如下代碼測試:
viewGroup.postDelayed(new Runnable() {
@Override
public void run() {
getWindowManager().removeView(getWindow().getDecorView());
}
}, 3000);
手指按在View 上,3s后將View 從Window里移除。
可以看出,觸發(fā)發(fā)送cancel 事件常見的有三種場景:

2、手指離開當前View時事件處理

如上圖,手指在View 上按下,View消費了Down事件,此時手指滑出View,并在ViewGroup上滑動,那么整個事件分發(fā)是怎么樣的呢?
通常來說,單指滑動的時候事件分為三個過程:
1、按下事件--->Down 事件。
2、滑動事件--->Move 事件。
3、抬起事件--->Up 事件。
從事件分發(fā)流程可知,當手指按下的時候:
1、若View 消費了Down事件,那么之后的Move、Up事件都會傳遞給View(不考慮ViewGroup攔截情況)。
2、手指在滑動的過程中,滑出了View,在ViewGroup上滑動,此時Move、Up事件依然會傳遞給View。
而View 本身是支持響應(yīng)長按與點擊動作的,若是當前手指還在View 上,那么很好理解,若是當前手指已經(jīng)滑動到ViewGroup上,長按與點擊動作如何響應(yīng)呢?
從直覺上來說,應(yīng)該不響應(yīng)的。
如何做到不響應(yīng)的?理論上來說:因為可以知道每個MotionEvent的觸摸坐標,通過該坐標就可以得知當前MotionEvent是否落在目標View 上,若坐標不在目標View(消費Down事件的View),那么就不響應(yīng)長按與點擊動作。
來看看源碼里是如何處理的:
#View.java
public boolean onTouchEvent(MotionEvent event) {
//點擊的坐標
final float x = event.getX();
final float y = event.getY();
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
//處在按下狀態(tài)
...
//沒有發(fā)生長按動作
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//移除長按動作
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//響應(yīng)點擊動作
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
//重置按下狀態(tài)
setPressed(false);
}
//移除延時單擊動作
removeTapCallback();
//移除長按動作
removeLongPressCallback();
...
break;
case MotionEvent.ACTION_MOVE:
...
if (!pointInView(x, y, touchSlop)) {
//移除延時單擊動作
removeTapCallback();
//移除長按動作
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
////重置按下狀態(tài)
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
...
break;
}
return true;
}
return false;
}
A、正常響應(yīng)點擊/長按動作
對于正常情況來說,比如上面的對ACTION_UP 事件的處理。
先說單擊動作:
1、首先需要當前View 處在按下狀態(tài)。
2、其次長按動作沒有發(fā)生。
再說長按動作:
長按動作是個延時執(zhí)行任務(wù),若是沒被取消,則終將會被執(zhí)行。
B、移動時處理點擊/長按動作
由A點可知,若是手指沒有滑出當前View,那么點擊與長按按照正常流程走。若是滑動出了View,在ACTION_MOVE事件處理邏輯如下:
1、一直在監(jiān)測當前的事件點坐標是否還在目標View里,若不在,則進行第2步處理。
2、發(fā)現(xiàn)事件點坐標已經(jīng)不在目標View里,于是移除延時單擊動作、移除長按動作、重置按下狀態(tài)。
3、當目標View收到ACTION_UP事件后,發(fā)現(xiàn)按下狀態(tài)為false,于是不響應(yīng)單擊動作,而延時長按動作已經(jīng)被取消了,也不會響應(yīng)長按動作。
C、總結(jié)
1、一旦View消費了Down事件,那么后續(xù)的Move、Up、Cancel等事件都會交給它處理(ViewGroup沒有攔截消費的前提下)
2、即使滑動超出了當前View的范圍,它依然能夠收到上述事件。
3、若是最終的Up事件不是發(fā)生在當前View之上,那么該View不響應(yīng)單擊與長按動作。
4、若是收到Cancel事件,那么View就不會再響應(yīng)單擊與長按動作,并且后續(xù)的事件將不會再收到。
3、手指離開屏幕時事件處理
當手指滑動離開屏幕時,如下圖:

其處理邏輯與手指離開當前View時事件處理是一致的。
本文基于Android 10。
各種事件Demo請移步:相關(guān)代碼演示