事件分發的順序
Activity->Window->DecorView->ViewGroup->View
事件的類型
ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL
通常一個事件序列是這樣的ACTION_DOWN 事件是一個事件的起點,然後伴隨着多個ACTION_DOWN事件,然後是ACTION_DOWN,中間可能會收到一個ACTION_DOWN事件
Activity的事件分發
//Activity的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看出,Activity 其實是調用了 Window 的 superDispatchTouchEvent 方法,而 Window 的實現類是 PhoneWindow,因此我們直接查看 PhoneWindow 的 superDispatchTouchEvent 方法
Window的事件分發
//PhoneWindow的superDispatchTouchEvent方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
發現是直接調用的 DecorView 的 superDispatchTouchEvent 方法,再進一步查看
//DecorView的superDispatchTouchEvent方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
原來這兒就調用了 ViewGroup 的 dispatchTouchEvent 方法,也就是說界面上的事件直接傳遞給了根佈局的 dispatchTouchEvent 方法
分析ViewGroup是如何分發事件的,來看下ViewGroup的dispatchTouchEvent方法
分析dispatchTouchEvent方法之前,先看一下dispatchTransformedTouchEvent方法,dispatchTouchEvent方法內部會多次調用到了dispatchTransformedTouchEvent方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 如果是取消操作,則直接分發取消事件
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// 如果傳入的 child 不爲空,則調用 child 的 dispatchTouchEvent 方法,否則調用自身的 dispatchTouchEvent 方法
if (child == null) {
//相當於調用了View類的dispatchTouchEvent方法
handled = super.dispatchTouchEvent(event);
} else {
//繼續分發事件到子View
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
......
// 如果傳入的 child 不爲空,則調用 child 的 dispatchTouchEvent 方法,否則調用自身的 dispatchTouchEvent 方法
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
......
handled = child.dispatchTouchEvent(transformedEvent);
}
......
return handled;
}
可以看出 dispatchTransformedTouchEvent 方法主要做了兩件事
- 如果傳入的事件是 ACTION_CANCEL,或者 cancel 參數爲 true,則直接分發 ACTION_CANCEL 事件
- 分發過程中,如果 child 爲空,則調用當前 View 的 super.dispatchTouchEvent 方法,這是因爲 ViewGroup 的 dispatchTouchEvent 方法會被重寫,而此時調用 super 的方法也就是調用 View 的 dispatchTouchEvent 方法;如果 child 不爲空,則調用這個子 View 的 dispatchTouchEvent 方法。
下面分析dispatchTouchEvent的核心代碼部分
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 1. DOWN 事件進行初始化,清空 TouchTargets 和 TouchState mFirstTouchTarget=null,disallowIntercept=false
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 2. 檢查是否攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 是否強制不允許攔截,子 View 可以設置 parent 強制不允許攔截,默認爲 false
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 {
intercepted = true;
}
......
// 3. 如果沒有被攔截, 先處理 DOWN 事件,主要是賦值 TouchTarget
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
......
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
......
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
......
// 找到 Visible 並且處於點擊範圍的子 View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
......
// 相當於調用子 View 的 dispatchTouchEvent 方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
......
// 賦值 TouchTarget,刷新標誌位 mFirstTouchTarget賦值了
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
......
}
......
}
......
}
}
// 4. 是自己處理事件還是交由子 View 處理事件
if (mFirstTouchTarget == null) {
// 沒有子 View 消耗事件,則自己消耗,相當於調用 super.dispatchTouchEvent 方法
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 如果是 DOWN 事件,則上面已經調用了子 View 的 dispatchTouchEvent 方法,則什麼都不用做
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 根據 intercepted 決定是否將事件強制改爲 CANCEL 事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 相當於調用子 View 的 dispatchTouchEvent 方法。如果 intercepted=true,此時會強制將 action 改爲 CANCEL;如果 intercepted=false,則
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果 intercepted=true,則將 mFirstTouchTarget 置爲 null
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
......
}
......
return handled;
}
dispatchTouchEvent 方法主要由4個模塊組成的
- DOWN 事件進行初始化,清空 TouchTargets 和 TouchState mFirstTouchTarget=null,disallowIntercept=false
- 檢查是否攔截
- 如果沒有被攔截, 先處理 DOWN 事件,主要是賦值 TouchTarget
- 是自己處理事件還是交由子 View 處理事件
至此ViewGroup的dispatchTouchEvent說完了,總結下
down事件ViewGroup會清空mFirstTouchTarget並且重置disallowIntercept標記爲false,所以每次down事件來臨時,父容器第一步會調用onInterceptTouchEvent詢問自己是否攔截.如果down事件攔截了,那麼此次down事件會交給ViewGroup處理,此時mFirstTouchTarget=null的,所以下次move/up事件來臨時,父容器直接將intercepted=true,所以ViewGroup一旦將down事件攔截,那麼之後的事件都將由他自己處理,子View不會收到任何事件
當down事件ViewGroup沒有攔截時,此時ViewGroup會遍歷子View,尋找能處理這個事件的View,如果沒有找到,同樣這個事件依然被ViewGroup處理,後續事件都有他處理。當ViewGroup找到了處理down事件的子View,此時會將mFirstTouchTarget賦值,即下次move事件來臨時mFirstTouchTarget!=null的
當move事件來臨時,ViewGroup依然回調用onInterceptTouchEvent詢問自己是否攔截,如果攔截了,此時mFirstTouchTarget!=null,根據上面的分析,此次子View會收到一個cancel事件,並且ViewGroup會清空mFirstTouchTarget=null,之後的事件由於mFirstTouchTarget=null,所以事件都給有ViewGroup自身處理。
分析View是如何分發事件的 dispatchTouchEvent(MotionEvent ev)
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
//這裏判斷是否設置了mOnTouchListener,是否是ENABLED,onTouch的返回值
//所以優先級的onTouch>onTouchEvent
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//這裏會調用onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
總結一下,首先如果設置了了mOnTouchListener,它的優先級是大於onTouchEvent的。當其onTouch方法返回true的時候onTouchEvent不會被調用
分析View是如何消費事件的 onTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//這裏判斷view是否是可以點擊的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//如果view的狀態是禁用的,只要它是可以點擊的就可以消費事件,只是不做任何的響應
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
//這裏如何設置了mTouchDelegate,會先調用mTouchDelegate.onTouchEvent(event)
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//只要是可以點擊的就會進來,且最終一定會return true 消費事件
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
//這裏處理點擊事件onClick
performClickInternal();
break;
case MotionEvent.ACTION_DOWN:
....
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
....
if (isInScrollingContainer) {
....
postDelayed(mPendingCheckForTap,
} else {
....
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
....
case MotionEvent.ACTION_MOVE:
....
break;
}
return true;
}
return false;
}
總結一下,只要View是可以點擊的就可以消費事件,並且如果設置了mTouchDelegate,則會先執行mTouchDelegate的onTouchEvent方法,onLongClick的執行是在action_down中,最終走到
performLongClick方法中,如下
public boolean performLongClick() {
return performLongClickInternal(mLongClickX, mLongClickY);
}
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
if ((mViewFlags & TOOLTIP) == TOOLTIP) {
if (!handled) {
handled = showLongClickTooltip((int) x, (int) y);
}
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
得到調用。onClick的執行是在action_up中,最終走到
performClick方法中,如下
public boolean performClick() {
....
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
,優先級onLongClick>onClick
事件衝突的解決思路
當一個事件來臨時,不只一個View可以處理此次事件,這時候父容器不知道將事件分發給誰處理,這時候就產生了事件衝突,通過以上分析,我們可以得出來兩個思路,第一個外部攔截法,通過改變ViewGroup的onIntercept方法的返回值。第二個通過更改disallowIntercept標記,干擾ViewGroup的分發流程
思路一:外部攔截法模版代碼
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
return intercepted;
}
思路二:內部攔截法模版代碼
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}