需要搞懂的疑問
- 當按住一個在Linearlayout裡的button後滑動出這個button,為什麼這個button還能繼續接收處理觸摸事件
- 觸摸事件是如何傳遞給Activity,才繼續進行Activity->Window->View的分發的
- 事件分發遞歸調用的流程整理
- 上層ViewGroup設定onTouchListener并且在onTouch方法傳回true,為什麼它的子view還是可以接收到觸摸事件
- 了解為什麼設定setEnable(false)後,onTouchEvent()事件還可以發生,onclick事件為什麼沒有
- 當touch事件繼續傳遞給一個坐标不在該view内的view對象時,它是怎麼處理的
- 當一個viewGroup有并列的子view時,是先把事件傳遞給哪個子view的
- MotionEvent裡的x,y坐标都是相對于誰的坐标值
事件源碼分析
簡化ViewGroup的dispatchTouchEvent()方法如下:
//子view可設定該變量為true禁止攔截touch事件
private boolean isDisallowIntercept;
public boolean dispatchTouchEvent(MotionEvent ev) {
//********預先定義處理結果變量********
boolean handled = false;
//重置标記位和變量
if(actionMasked == MotionEvent.ACTION_DOWN){
mFirstTouchTarget = null; //mFirstTouchTarget設定為null,并且清空單連結清單
isDisallowIntercept = false; //重置失效攔截标志位,是以ACTION_DOWN的時,父view想攔截是一定可以攔截的
}
//********檢查是否攔截********
final boolean intercepted;
//如果ACTION_DOWN的時候攔截了事件,則mFirstTouchTarget=null,後續的ACTION_MOVE和ACTION_UP事件
// 則不會進入這個判斷,onInterceptTouchEvent也就沒有再執行的機會和必要了
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
if (!isDisallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//********判斷分發********
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 != ) {
final View[] children = mChildren;
for (int i = childrenCount - ; i >= ; i--) {
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
//如果之前該child已經處理過該序列事件中的事件,找到後直接傳回
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
break;
}
//調用child的dispatchTouchEvent()傳回,如果傳回true,則建立新的TouchTarget并加在連結清單頭
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
//如果周遊中沒找到處理的子view,并且單連結清單不為null,則newTouchTarget指向連結清單尾部元素
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
}
}
}
//********處理分發結果********
if (mFirstTouchTarget == null) {
// 沒有子view處理,則自己調用自己的處理方法,看自己是否處理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
//如果是上面處理ACTION_DOWN時新到的處理子view,則直接傳回true
if (alreadyDispatchedToNewTouchTarget && newTouchTarget == mFirstTouchTarg
handled = true;
} else {
//可以看到intercepted到後面處理時還是有用的,如果一系列事件開始已經交給某個子view處理
//當處理到中間某一個事件的時候突然攔截
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//如果攔截了某個中間事件,則會給之前處理的子view發送CANCEL事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
//如果是第一次循環,且連結清單隻有一個頭元素,則next=null,相當于清空了mFirstTouchTarget
//則下一次事件來的時候intercepted直接為true,不會再傳遞給子view處理
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
return handled;
}
簡化View的dispatchTouchEvent()方法如下:
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//判斷OnTouchListener是否不為空,并且viw是否是ENABLED,如果都是則會執行OnTouchListener
//的onTouch()事件。如果的onTouch事件傳回true,則直接傳回,不會執行view本身的onTouchEvent方法
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//當上面的判斷傳回false時,才有機會執行view本身的onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, );
}
//OnTouchListener的onTouch()和view本身的onTouchEvent隻要有一個傳回true,
// 則表示該view消費事件
return result;
}
簡化View的onTouchEvent()方法如下:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != ) {
setPressed(false);
}
//如果view的ENABLED為false,則當view的clickable或者longClickable有一個是true的話,則
//消費事件,但是什麼都不做,是以onClick()方法也不會觸發。否則則直接傳回不消費事件
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
//如果設定了TouchDelegate類,則調用TouchDelegate的onTouchEvent()方法處理,
//如果TouchDelegate的onTouchEvent()傳回true則直接傳回消費事件。
//從TouchDelegate類文檔看該類用于幫助擴大touch區域
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != ;
//當在PREPRESSED或者PRESSED狀态的時候才進入
if ((mPrivateFlags & PFLAG_PRESSED) != || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//觸發OnClickListener的onClick()的方法,用post方式
if (!post(mPerformClick)) {
performClick();
}
}
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
//設定為PREPRESSED狀态
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
//檢查是否為長按事件
checkForLongClick();
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
//移動出view所在的區域
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != ) {
removeLongPressCallback();
//當移出view區域時,如果目前為PRESSED狀态,則會清除PRESSED狀态,并且重新整理view
//比如view背景設定為一個selector,則會恢複顯示normal狀态下的背景
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
小結
由于ViewGroup的onInterceptTouchEvent()方法預設實作為直接傳回false和當ViewGroup沒找到子view處理touch事件的時候,會調用super.dispatchTouchEvent(event),即view的dispatchTouchEvent()方法,是以從上面的源碼可以得出下面結論:
- 當某一個ViewGroup或者View的dispatchTouchEven()方法傳回true的時,則相對于它們的父view表示該ViewGroup或者View消費了這次touch事件。對于ViewGroup有可能是自身的OnTouchListener或onTouchEvent方法消費了,也有可能是子view消費了,而對于View而言,則隻能是前者。但對于它們父view來說是沒有差別的,父view的mFirstTouchTarget中儲存的是該ViewGroup或者View。
- 整個調用過程有點類遞歸的感覺,遞歸的深度則是整個布局中view層次的深度,是以會有往下深入和網上冒出的過程,俗稱的往下隧道方式,往上冒泡方式的傳遞過程。而且這樣的過程最多隻有ACTION_DOWN的時候能夠完整走一遍,當ACTION_MOVE和ACTION_UP的時候,DecorView的mFirstTouchTarget已經記錄了消費的直接子view,如果這個直接子view是ViewGroup的話,則它的mFirstTouchTarget又記錄了它裡面消費事件的子view,如此循環,最後交給最深的處的消費子view,沒有了ACTION_DOWN時周遊尋找消費子view的過程。
- 每一個事件ViewGroup都會周遊mFirstTouchTarget單連結清單裡所有記錄的view,如果不是cancelChild則會給他們發送正常事件,如果是則會給該child發送ACTION_CANCEL事件,并且從連結清單中移除該child。判斷一個view是不是cancelChild則通過判斷該child的标記位和ViewGroup是否攔截來決定,隻有有一個成立,則是cancelChild。不管是發送正常事件還是ACTION_CANCEL事件,都會調用子view的dispatchTouchEven()方法,隻要有一個傳回true,則ViewGroup會向上傳回true表示消費該事件。
- 當動态改變ViewGroup的onInterceptTouchEvent()方法的傳回值時,比如在ACTION_DOWN時傳回false不攔截,ACTION_MOVE的時候傳回true,則ViewGroup在第一次ACTION_MOVE事件到來時把事件轉變為ACTION_CANCEL發送給之前處理事件的子view,并且會把mFirstTouchTarget置為null,下次ACTION_MOVE或ACTION_UP時來到時,intercepted則為true表示攔截,ViewGroup會調用自己的OnTouchListener或onTouchEvent方法。
- 當動态改變子view的dispatchTouchEven()方法傳回值時,比如在ACTION_DOWN時傳回true消費事件,ACTION_MOVE的時候傳回false,由于父ViewGroup的mFirstTouchTarget單連結清單裡儲存的該子view,隻有在它是cancelChild的時候才會移除,就算ACTION_MOVE的時候傳回false導緻子view的dispatchTouchEvent()方法傳回false,父ViewGroup的mFirstTouchTarget單連結清單還是會儲存該子view,所有後續的ACTION_MOVE和ACTION_UP事件還是會繼續發送給該子view。可以看出子view的dispatchTouchEvent()方法傳回值隻有在ACTION_DOWN事件時才能起到決定事件走向的作用。但是由于子view的dispatchTouchEvent()方法傳回false,而且也不會走父ViewGroup的OnTouchListener或onTouchEvent方法,是以最終到Activity的時,DecorView的dispatchTouchEvent()方法也會傳回false,這時候會執行Activity的onTouchEvent()方法。
回答疑問
-
當按住一個在Linearlayout裡的button後滑動出這個button,為什麼這個button還能繼續接收處理觸摸事件
因為Linearlayout的mFirstTouchTarget裡面儲存有該button,并且判斷它不是cancelChild,是以會一直轉發事件發給它,button内部的onTouchEvent()方法,ACTION_MOVE時會判斷是否滑出button邊界,如果是則會清除PRESSED狀态,并且重新整理背景,當ACTION_UP的時候判斷不是PRESSED狀态,是以OnClickListener的onclick()方法也不會觸發。
- 觸摸事件是如何傳遞給Activity,才繼續進行Activity->Window->View的分發的
- 事件分發遞歸調用的流程整理
-
上層ViewGroup設定onTouchListener并且在onTouch方法傳回true,為什麼它的子view還是可以接收到觸摸事件
因為如果子view的dispatchTouchEven()方法傳回true的話,則會先發送給子view消費,不會走到ViewGroup的onTouchListener和onTouchEvent()方法。隻有當點選ViewGroup中未包含子view的地方才會不經過子view直接自己消費。
-
了解為什麼設定setEnable(false)後,onTouchEvent()事件還可以發生,onclick事件為什麼沒有
因為setEnable(false)後,view的CLICKABLE或者LONG_CLICKABLE還是true的,是以還可以繼續消費事件,隻是接收後,什麼都不做直接傳回true,是以onclick事件不會觸發
-
當touch事件繼續傳遞給一個坐标不在該view内的view對象時,它是怎麼處理的
同第一個問題,ACTION_MOVE時會判斷是否滑出button邊界,如果是則會清除PRESSED狀态,并且重新整理背景,當ACTION_UP的時候判斷不是PRESSED狀态,是以OnClickListener的onclick()方法也不會觸發。
-
當一個viewGroup有并列的子view時,是先把事件傳遞給哪個子view的
preorderedList中的順序是按照addView或者XML布局檔案中的順序來的,後addView添加的子View,會因為Android的UI後重新整理機制顯示在上層,是以有覆寫的并列子view時,會先傳遞給上層view(已驗證源碼待分析)
-
MotionEvent裡的x,y坐标都是相對于誰的坐标值
MotionEvent有四個方法getRawX(), event.getRawY(), getX(),getY()。awX和rawY是相對于螢幕的坐标,x和y是相對于目前控件的坐标。rawX和X 向右移動都是增大,向左移動都是減小,rawY和 Y 向下移動都是增大,向上移動都是減小。