Android進階知識:事件分發與滑動衝突(二)
接著看代碼塊3,在這段很長的代碼里,首先一個if
中判斷了該事件是否滿足沒有被攔截和被取消,之後第二個if
判斷了事件類型是否為DOWN
,滿足了沒有被攔截和取消的DOWN
事件,接下來ViewGroup才會循環其子View找到點擊事件在其內部並且能夠接受該事件的子View,再通過調用dispatchTransformedTouchEvent
方法將事件分發給該子View處理,返回true說明子View成功消費事件,於是調用addTouchTarget
方法,方法中通過TouchTarget.obtain
方法獲得一個包含這View的TouchTarget
節點並將其添加到鏈表頭,並將已經分發的標記設置為true
。
// Dispatch to touch targets.
//走到這裡說明在循環遍歷所有子View後沒有找到接受該事件或者事件不是DOWN事件或者該事件已被攔截或取消
if (mFirstTouchTarget == null) {
//mFirstTouchTarget為空說明沒有子View響應消費該事件
//所有調用dispatchTransformedTouchEvent方法分發事件
//注意這裡第三個參數傳的是null,方法里會調用super.dispatchTouchEvent(event)即View.dispatchTouchEvent(event)方法
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// mFirstTouchTarget不為空說明有子View能響應消費該事件,消費過之前的DOWN事件,就將這個事件還分發給這個View
// 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) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//這裡傳入的是target.child就是之前響應消費的View,把該事件還交給它處理
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
之前在代碼塊3中處理分發了沒被攔截和取消的DOWN
事件,那麼其他MOVE
、UP
等類型事件怎麼處理呢?還有如果遍歷完子View卻沒有能接受這個事件的View又怎麼處理呢?代碼塊4中就處理分發了這些事件。首先判斷mFirstTouchTarget
是否為空,為空說明沒有子View消費該事件,於是就調用dispatchTransformedTouchEvent
方法分發事件,這裡注意dispatchTransformedTouchEvent
方法第三個參數View傳的null
,方法里會對於這種沒有子View能處理消費事件的情況,就調用該ViewGroup的super.dispatchTouchEvent
方法,即View的dispatchTouchEvent
,把ViewGroup當成View來處理,把事件交給ViewGroup處理。具體看dispatchTransformedTouchEvent
方法中的這段代碼:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
dispatchTransformedTouchEvent
方法中child
即傳入的View為空則調用super.dispatchTouchEvent
方法分發事件,就是View類的分發方法,不為空則調用子View方法,即child.dispatchTouchEvent
分發事件,所以歸根結底都是調用了View類的dispatchTouchEvent
方法處理。
至此,ViewGroup中的分發過流程結束,再來總結一下這個過程:首先過濾掉不安全的事件,接著如果事件類型是DOWN
事件認為是一個新的事件序列開始,就清空TouchTarget
鏈表重置相關標誌位(代碼塊一),然後判斷是否攔截該事件,這裡有兩步判斷:一是如果是DOWN
事件或者不是DOWN
事件但是mFirstTouchTarget
不等於null
(這裡mFirstTouchTarget
如果等於null
說明之前沒有View消費DOWN
事件,在代碼塊三末尾,可以看到如果有子View消費了DOWN
事件,會調用addTouchTarget
方法,獲得一個保存了該子View的TouchTarget
,並將其添加到mFirstTouchTarget
鏈表頭),則進入第二步禁止攔截標記的判斷,否則直接設置為需要攔截,進入第二步判斷設置過禁止攔截標記為true
的就不攔截,否則調用ViewGroup的onInterceptTouchEvent
方法根據返回接過來決定是否攔截(代碼塊二)。接下來如果事件沒被攔截也沒被取消而且還是DOWN
事件,就循環遍歷ViewGroup中的子View找到事件在其範圍內並且能接受事件的子View,通過dispatchTransformedTouchEvent
方法將事件分發給該子View,然後通過addTouchTarget
方法將包含該子View的TouchTarget
插到鏈表頭(代碼塊三)。最後如果沒有找到能夠接受該事件的子View又或者是MOVE
、UP
類型事件等再判斷mFirstTouchTarget
是否為空,為空說明之前沒有View能接受消費該事件,則調用dispatchTransformedTouchEvent
方法將事件交給自身處理,不為空則同樣調用dispatchTransformedTouchEvent
方法,但是是將事件分發給該子View處理。
ViewGroup的onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
在ViewGroup的dispatchTouchEvent
中沒設置過禁止攔截的事件默認都會通過onInterceptTouchEvent
方法來決定是否攔截,onInterceptTouchEvent
方法里可以看到默認是返回false
,只有在事件源類型是滑鼠並且是DOWN
事件是滑鼠點擊按鈕和是滾動條的手勢時才返回true
。所以默認一般ViewGroup的onInterceptTouchEvent
方法返回都為false
,也就是說默認不攔截事件。
ViewGroup的onTouchEvent方法:
ViewGroup中沒有覆蓋onTouchEvent
方法,所以調用ViewGroup的onTouchEvent
方法實際上調用的還是它的父類View的onTouchEvent
方法。
View的dispatchTouchEvent方法:
在ViewGroup中將事件無論是分發給子View的時候還是自己處理的,最終都會執行默認的View類的dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
......
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
這裡同樣省略一些代碼只看關鍵的,首先同樣和ViewGroup一樣,做了事件安全性的過濾,接著先判斷了mOnTouchListener
是否為空,不為空並且該View是ENABLED
可用的,就會調用mOnTouchListener
的onTouch
方法,如果onTouch
方法返回true
說明事件已經被消費了,就將result
標記修改為true
,這樣他就不會走接下來的if
了。如果沒有設置mOnTouchListener
或者onTouch
方法返回false
,則會繼續調用onTouchEvent
方法。這裡可以發現mOnTouchListener
的onTouch
方法的優先順序是在onTouchEvent
之前的,如果在代碼中設置了mOnTouchListener
監聽,並且onTouch
返回true
,則這個事件就被在onTouch
里消費了,不會在調用onTouchEvent
方法。
//這個mOnTouchListener就是經常在代碼里設置的View.OnTouchListener
mMyView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//這裡返回true事件就消費了,不會再調用onTouchEvent方法
return true;
}
});
View的onTouchEvent方法:
public boolean onTouchEvent(MotionEvent event) {
/---------------代碼塊-1-------------------------------------------------------------------
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
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 doesnt respond to them.
return clickable;
}
/---------------代碼塊-1------完-------------------------------------------------------------
/---------------代碼塊-2-------------------------------------------------------------------
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
/---------------代碼塊-2------完-------------------------------------------------------------
/---------------代碼塊-3-------------------------------------------------------------------
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we dont have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//調用了OnClickListener
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if were 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) {
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(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
/---------------代碼塊-3------完-------------------------------------------------------------
return false;
}
onTouchEvent
方法里的代碼也不少,不過大部分都是響應事件的一些邏輯,與事件分發流程關係不大。還是分成三塊,先看第一個代碼塊:
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//這裡CLICKABLE、CONTEXT_CLICKABLE和CONTEXT_CLICKABLE有一個滿足,clickable就為true
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//這裡先判斷當前View是否可用,如果是不可用進入if代碼塊
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//如果是UP事件並且View處於PRESSED狀態,則調用setPressed設置為false
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 doesnt respond to them.
//這裡如果View是不可用狀態,就直接返回clickable狀態,不做任何處理
return clickable;
}
代碼塊1中首先獲得View是否可點擊clickable
,然後判斷View如果是不可用狀態就直接返回clickable
,但是沒做任何響應。View默認的clickable
為false
,Enabled
為ture
,不同的View的clickable
默認值也不同,Button
默認clickable
為true
,TextView
默認為false
。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
代碼塊2中會對一個mTouchDelegate
觸摸代理進行判斷,不為空會調用代理的onTouchEvent
響應事件並且返回true
。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we dont have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//調用了OnClickListener
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if were 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) {
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(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
代碼塊3中首先判斷了 clickable || (viewFlags & TOOLTIP) == TOOLTIP
滿足了這個條件就返回true
消費事件。接下來的switch
中主要對事件四種狀態分別做了處理。這裡稍微看下在UP
事件中會調用一個performClick
方法,方法中調用了OnClickListener
的onClick
方法。
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;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
最後看到onTouchEvent
的最後一行默認返回的還是false
,就是說只有滿足上述的條件之一才會返回ture
。
ViewGroup的dispatchTouchEvent邏輯: