一、事件定義
定義:當(dāng)用戶觸摸屏幕時,將產(chǎn)生的觸摸行為(Touch事件)
事件類型
- MotionEvent.ACTION_DOWN 手指剛接觸屏幕
- MotionEvent.ACTION_MOVE 手指在屏幕上滑動
- MotionEvent.ACTION_UP 手指從屏幕上松開
- MotionEvent.ACTION_CANCEL 非人為因素取消
二、事件序列
正常情況下一次手指觸摸屏幕的行為會觸發(fā)一系列事件
- 點擊屏幕后立即松開,事件序列為DOWN -> UP
- 點擊屏幕滑動一段距離后松開,事件序列為DOWN -> MOVE -> ... -> MOVE -> UP
事件序列流程圖如下
三、事件分發(fā)對象
- Activity:控制生命周期&處理事件
- ViewGroup:一組View的集合
- View:所有UI組件的基類
四、事件分發(fā)主要方法
-
dispatchTouchEvent(MotionEvent ev)
:用來進行事件分發(fā)
-
onInterceptTouchEvent(MotionEvent ev)
:判斷是否攔截事件(ViewGroup)
-
onTouchEvent(MotionEvent ev)
::處理觸摸事件
五、事件分發(fā)源碼分析
按照慣例,為了方便大家理解源碼,我們先來看看整個事件分發(fā)的流程圖
Activity的事件分發(fā)
Activity是最先得到用戶觸摸事件的,首先我們來看Activity#dispatchTouch
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
這里有個判斷這個觸摸事件是不是DOWN事件,如果是則調(diào)用onUserInteraction()
,這個方法是一個空方法,我們繼續(xù)往下看,getWindow()
會返回一個PhoneWindow
對象,接著調(diào)用PhoneWindow#superDispatchTouchEvent,如果返回true則事件結(jié)束,否則表示事件沒有被消費,則 調(diào)用Activity#onTouchEvent來處理事件,繼續(xù)跟進PhoneWindow#superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor是頂層布局容器DecorView,繼續(xù)跟進DecorView#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
可以看到DecorView中是直接調(diào)用了 super.dispatchTouchEvent(event)
,因為DecorView
繼承自FrameLayout
,所以最后會調(diào)用到ViewGroup#sdispatchTouchEvent(event),我們來總結(jié)下Activity的事件分發(fā):Activity->PhoneWindow->DecorView
ViewGroup的事件分發(fā)
現(xiàn)在事件已經(jīng)從Activity分發(fā)到了ViewGroup,我們接著來看ViewGroup的事件分發(fā)
ViewGroup#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;
// Handle an initial down.
// --------注釋1--------
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.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// --------注釋2--------
// 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;
}
...
// --------注釋3 --------
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 對子View倒敘遍歷
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// --------注釋4 --------
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// --------注釋5 --------
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// --------注釋6 --------
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
...
// --------注釋7 --------
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} 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;
// --------注釋8 --------
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
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;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
...
return handled;
}
看到注釋1處,如果是一個DOWN事件,表明是一個新的事件的開始,會調(diào)用兩個方法進行一些標(biāo)記的清除,cancelAndClearTouchTargets(ev)
主要的作用是將全局變量mTouchTarget
置null,resetTouchState()
主要作用是將全局變量mGroupFlags
清除,接下來繼續(xù)往下看注釋2處的代碼,這塊代碼主要是用來檢測是否需要對事件進行攔截,必須是DOWNS事件或者mTouchTarget
不為null才會可能去執(zhí)行onInterceptTouchEvent(ev)
,否則攔截標(biāo)記intercepted
直接賦值為true,然后繼續(xù)根據(jù)mGroupFlags
是不是設(shè)置為FLAG_DISALLOW_INTERCEPT
,是的話表明不攔截該事件,攔截標(biāo)記intercepted
賦值為false,否則執(zhí)行onInterceptTouchEvent(ev)
,當(dāng)子View調(diào)用requestDisallowInterceptTouchEvent()
方法來請求父ViewGroup不攔截事件時會給mGroupFlags
賦值為FLAG_DISALLOW_INTERCEPT
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
在DOWN事件時,已經(jīng)對mTuchTarget
置null,mGroupFlags
清除,導(dǎo)致在DOWN事件intercepted
賦值為true,表示攔截該事件,所以requestDisallowInterceptTouchEvent
只能作用DOWN事件之后的事件(MOVE、UP)。接著看注釋3處,在DOWN事件時,如果newTouchTarget == null && childrenCount != 0
,則直接對子View進行倒敘遍歷,繼續(xù)往下看注釋4處child.canReceivePointerEvents()
和isTransformedTouchPointInView()
,先看View#canReceivePointerEvents
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
這個方法表示View可見或者正在執(zhí)行動畫,接著看
MotionEvent#isTransformedTouchPointInView
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
這個方法主要作用是判斷觸摸區(qū)域是否在View的區(qū)域內(nèi) ,回到注釋4處,這個條件判斷表示如果當(dāng)前View不可見并且沒有在執(zhí)行動畫,或者觸摸區(qū)域不在View的區(qū)域內(nèi),則直接continue,進行下次遍歷。繼續(xù)看到注釋5處,如果找到了觸摸的View,調(diào)用dispatchTransformedTouchEvent()
并將這個View傳進去,跟進ViewGroup#dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
...
// Done.
transformedEvent.recycle();
return handled;
}
如果child
不為空,則調(diào)用child.dispatchTouchEvent(event)
將事件傳遞給child
,如果child
為null,則直接調(diào)用super.dispatchTouchEvent(event)
,此時如果 child
為View,則直接調(diào)用View的dispatchTouchEvent(event)
處理事件,如果child
為ViewGroup,則事件最終會來到ViewGroup的onTouchEvent()
方法 ,回到注釋5處條件判斷,如果dispatchTransformedTouchEvent()
返回true, 表示子View消費了事件,接著調(diào)用注釋6處的addTouchTarget(child, idBitsToAssign)
,ViewGroup#addTouchTarget
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
這里對mFirstTouchTarget
賦值為消費了事件的子View的TouchTarget,回到調(diào)用該方法的注釋6處,將消費了事件的子View的TouchTarget賦值給newTouchTarget
,并將alreadyDispatchedToNewTouchTarget
設(shè)為true
我們先來總結(jié)下到目前為止ViewGroup#dispatchTouchEvent所做的事情:在DOWN事件時,遍歷所有子View,找到消費事件的子View,并將子View賦值給mFirstTouchTarget
,即mFirstTouchTarget
指向了消費事件的子View,如果沒有子View消費事件,則 mFirstTouchTarget
依舊為null
繼續(xù)看到ViewGroup#dispatchTouchEvent中注釋7處,如果mFirstTouchTarget
為null,表明沒有子View消費事件,則調(diào)用dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)
,并將第三個參數(shù)child傳null,上面我們已經(jīng)分析了,如果child
為null,則直接調(diào)用super.dispatchTouchEvent(event)
,此時如果 child
為View,則直接調(diào)用View的dispatchTouchEvent(event)
處理事件,如果child
為ViewGroup,則事件最終會來到ViewGroup的onTouchEvent()
方法
繼續(xù)看注釋8處,如果mFirstTouchTarget
不為null,表示DOWN事件已經(jīng)找到了一個子View來消費事件,條件判斷(alreadyDispatchedToNewTouchTarget && target == newTouchTarget)
當(dāng)找到消費事件的子View時值為true,然后直接給handles
賦值為true,DOWN事件結(jié)束,如果上面條件判斷為false,則else里面的代碼是對MOVE和UP事件的處理,繼續(xù)調(diào)用dispatchTransformedTouchEvent()
將MOVE和UP事件直接分發(fā)給消費了DOWN事件的 子View
我們來簡單總結(jié)下ViewGroup#dispatchTouchEvent所做的全部事情:ViewGroup中可以通過onInterceptTouchEvent()
對事件進行攔截,返回true表示攔截,返回false表示不攔截,子View可以調(diào)用getParent().requestDisallowInterceptTouchEvent(true)
來請求父ViewGroup不攔截事件(只對MOVE和UP事件生效),接著遍歷子View,找到消費了DOWN事件的子View,并將后續(xù)MOVE和UP事件直接分發(fā)給消費了DOWN事件的子View,若沒有子View消費DOWN事件,則會調(diào)用View的dispatchTouchEvent()
,若返回true,則會調(diào)用此ViewGroup的onTouchEvent()
將事件交給自己的onTouchEvent()
處理,后續(xù)的MOVE和UP事件將不再向下分發(fā),直接交給ViewGroup#onTouchEvent處理
View的事件分發(fā)
現(xiàn)在事件已經(jīng)從ViewGroup分發(fā)到了View,我們接著來看View的事件分發(fā)
View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// --------注釋9 --------
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
...
return result;
}
從代碼中可以看出,如果View正在被拖拽,則直接消費掉事件,mListenerInfo
是ListenerInfo的對象,在給View設(shè)置一些監(jiān)聽的時候貴初始化mListenerInfo
,看到注釋9處的條件判斷,View默認(rèn)就是enable的,所以只要設(shè)置了OnTouchListener,會調(diào)用OnTouchListener#onTouch方法,返回true,則事件被消費,返回false,則繼續(xù)執(zhí)行onTouchEvent(event)
方法,onTouchEvent返回true,則事件被消費,繼續(xù)跟進View#onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// --------注釋10 --------
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 doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return 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 don't 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);
}
// --------注釋12 --------
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)) {
performClickInternal();
}
}
}
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 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) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
// --------注釋11 --------
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;
}
// --------注釋13 --------
return true;
}
return false;
}
看到注釋10處,只要View是可點擊的或者可長按的clickable就為true,并且View即使設(shè)置為DISABLED,也不會對clickable產(chǎn)生影響 ,依舊返回clickable,如果clickable為true,則說明事件被消費了,只不過對這個事件沒有響應(yīng),看到注釋13處直接返回clickable,表明如果View是可以點擊的,那么直接消費掉事件,我們繼續(xù)往下看在DOWN事件中給mHasPerformedLongPress
標(biāo)記位設(shè)為false,表示目前還沒有處理長按事件,接著繼續(xù)判斷當(dāng)View是在可滑動容器時,注釋11處發(fā)送一個延遲100ms的mPendingCheckForTap
任務(wù)來檢查是否是長按事件,ViewConfiguration.getTapTimeout()
返回的是100ms,CheckForTap代碼如下
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}
如果View不是在可滑動容器里面,則直接調(diào)用checkForLongClick()
方法來檢查長按事件,跟進View#checkForLongClick
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
注意這里mHasPerformedLongPress
也被賦值false,ViewConfiguration.getLongPressTimeout()
返回的是500ms,這段代碼又發(fā)送了一個500ms的延遲任務(wù)mPendingCheckForLongPress
來表示長按事件,看到UP事件里面的注釋12處,大家應(yīng)該還記得之前在DOWN事件中和checkForLongClick()
方法中我們將mHasPerformedLongPress
賦值為false,如果在UP事件時mHasPerformedLongPress
依然為false,則表明沒有長按事件,調(diào)用removeLongPressCallback()
移除長按事件回調(diào),繼續(xù)回到checkForLongClick()
,欄看下mPendingCheckForLongPress的run方法
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
private boolean mOriginalPressedState;
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
public void setAnchor(float x, float y) {
mX = x;
mY = y;
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
public void rememberPressedState() {
mOriginalPressedState = isPressed();
}
}
可以看到run方法里面調(diào)用了performLongClick(mX, mY)
,當(dāng)返回值為true時,將mHasPerformedLongPress
標(biāo)記設(shè)為true,所以若從DOWN事件到UP時間超過了500ms,則認(rèn)為事件是長按事件,否則是點擊事件,繼續(xù)跟進performLongClick方法發(fā)現(xiàn)最終會調(diào)用performLongClickInternal(float x, float y)
,并且返回performLongClickInternal(float x, float y)
的返回值,繼續(xù)跟進
View#performLongClickInternal
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;
}
可以看到這里調(diào)用了mOnLongClickListener.onLongClick
回調(diào),就是我們代碼中設(shè)置長按事件的回調(diào),如果我們在onLongClick回調(diào)中返回true,則mHasPerformedLongPress
標(biāo)記設(shè)為true,在onTouchEvent()
方法的UP事件中就會判定該事件為長按事件,來看到這段代碼
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)) {
performClickInternal();
}
}
}
如果mHasPerformedLongPress
標(biāo)記為true,則不會執(zhí)行條件里面的
post(mPerformClick)
,跟進PerformClick
private final class PerformClick implements Runnable {
@Override
public void run() {
performClickInternal();
}
}
run方法中調(diào)用了performClickInternal()
方法,跟進performClickInternal()
發(fā)現(xiàn)繼續(xù)調(diào)用了performClick()
,跟進View#performClick
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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;
}
可以看到其中調(diào)用了mOnClickListener.onClick(this)
,就是我們在代碼中設(shè)置的點擊監(jiān)聽回調(diào),到此事件分發(fā)就分析完畢了。
六、事件分發(fā)總結(jié)
Activity事件分發(fā)
- Activity#dispatchTouchEvent
- PhoneWindow#superDispatchTouchEvent
- DecorView#superDispatchTouchEvent
- ViewGroup#dispatchTouchEvent
- 若事件沒有被消費,則最終調(diào)用Activity#onTouchEvent
ViewGroup事件分發(fā)
- ViewGroup#dispatchTouchEvent(對down事件特殊處理:cancelAndClearTouchTargets(),resetTouchState(),清除標(biāo)記后down事件一定會執(zhí)行onInterceptTouchEvent(),onInterceptTouchEvent()默認(rèn)返回false,不攔截)
- 若沒有攔截,ViewGroup#dispatchTansformedTouchEvent,for循環(huán)遍歷子View,調(diào)用子view的dispatchTouchEvent,若沒有子view消費事件,則直接調(diào)用super.dispatchTouchEvent(event) ,此時如果 child 為View,則直接調(diào)用View的dispatchTouchEvent(event)處理事件,如果child 為ViewGroup,則事件最終會來到ViewGroup的onTouchEvent()方法
- 若有子view消費事件,則繼續(xù)將down事件后續(xù)的move和up事件分發(fā)給子view處理,ViewGroup不做處理
- 若沒有view消費事件,則down事件后續(xù)的move和up事件不再向下分發(fā),直接交給ViewGroup#onTouchEvent處理
- 子view可以調(diào)用getParent().requestDisalowInterceptTouchEvent()請求ViewGroup不攔截事件(只對MOVE和UP事件有效)
View事件分發(fā)
- View#dispatchTouchEvent(若view被設(shè)置了監(jiān)聽器,則會先調(diào)用監(jiān)聽器的 onTouch()方法,若onTouch()方法返回true,則事件被消費,不會調(diào)用onTouchEvent(), 若返回false,則會繼續(xù)調(diào)用onTouchEvent() )
- onTouch()返回false,View#onTouchEvent(若view是可以被點擊的(clickable == true),則直接返回true消費掉事件),在down事件里面會做一個長按事件的檢測 ,在up事件中檢測沒有長按事件則移除長按事件回調(diào)并響應(yīng)點擊事件,如果設(shè)置了長按事件并且長按事件的onLongClick()返回為true,則不執(zhí)行onClick(),若onLongClick()返回為false,則會繼續(xù)執(zhí)行onClick()事件
- 行先后順序:dispatchTouchEvent-> onTouch -> onTouchEvent -> onLongClick() -> onClick()