前言
美團一面的時候滩愁,問到了這個事件分發(fā)機制,當初只是聽說過,面試完后又趕緊復習了下括堤,這一塊的內(nèi)容其實對于很多Android初級開發(fā)者來說理解挺困難的,也不介意一些剛學Android的萌新一來就去看事件分發(fā)機制這種東西绍移,這篇文章就記錄一下我學習事件分發(fā)機制的過程痊臭,有錯誤也請多多包涵。
事件分發(fā)機制是什么
用戶通過屏幕與手機進行交互的時候登夫,每一次的點擊广匙,長按或者拖動都是一個事件,而這些事件從屏幕經(jīng)過一系列的處理并傳遞給各個View恼策,由View來消費這一事件huozhe或者忽略掉這個事件鸦致,這個整個過程就是事件分發(fā)機制潮剪。
總的來說,事件分發(fā)就是對MotionEvent事件的分發(fā)過程分唾,每當有MotionEvent產(chǎn)生了以后抗碰,系統(tǒng)就要把這個事件傳遞給具體的View來進行處理,這個傳遞的過程就是分發(fā)過程绽乔。而且了解分發(fā)機制也有助于后面更好的分析點擊滑動失效以及滑動沖突的問題弧蝇。
點擊事件的分發(fā)過程是涉及到3個重要的方法來一起共同的完成:
-
dispatchTouchEvent(MotionEvent ev)
這個就是用來進行事件分發(fā)的方法折砸,如果說事件要傳遞給當前的View看疗,那么這個方法一定是被首先調(diào)用,然后返回結果表示當前View的onTouchEvent()
和下級View的dispatchTouchEvent()
方法的影響睦授,表示是否消耗當前的事件两芳。 -
onInterceptTouchEvent(MotionEvent ev)
在上訴方法的內(nèi)部就會開始調(diào)用,用來判斷是否要攔截這個事件去枷,如果當前View攔截了這個事件怖辆,那么在同一個事件序列中,此方法就不會再被調(diào)用了删顶,然后返回的結果表示是否攔截了這個事件竖螃。 -
onTouchEvent(MotionEvent event)
在dispatchTouchEvent()
方法中被調(diào)用,用來處理點擊事件逗余,返回結果表示是否消耗當前事件斑鼻,如果不消耗,則在同一事件事件序列當中猎荠,當前View無法再次接收到事件坚弱。
當一個點擊事件發(fā)生后,它的傳遞過程會遵循如下的順序:
即事件會先傳遞給Activity,然后在傳遞給Window输虱,Window傳遞給DecorView,它是一個頂級的View愁茁,然后接著會傳遞給ViewGroup,最后又ViewGroup按照分發(fā)機制去分發(fā)事件亭病。這里需要注意的是鹅很,一個Activity包含著一個Window罪帖,而Window里面包含著一個DecorView邮屁,DecorView是Activity最頂層的View。
我們按照層級來一步步講菠齿,主要涉及的對象有3個佑吝,分別是Activity绳匀,ViewGroup和View,接下來就從源碼角度來分析具體的分發(fā)事件的流程戈钢。
Activity的分發(fā)過程
//Activity#dispatchTouchEvent源碼
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
點擊事件用MotionEvent來表示,當點擊操作發(fā)生時梨州,會傳遞到當前的Activity田轧,由Activity的dispatchTouchEvent()
進行派發(fā),具體的工作是由Activity的Window來完成的每窖,這里注意一下當事件開始派發(fā)的時候會判斷一下點擊事件是否為ACTION_DOWN弦悉,也就是事件剛剛產(chǎn)生的時候,如果是的話就會調(diào)用onUserInteraction()
方法瀑志,這是一個默認空的實現(xiàn)方法污秆,這個方法就是會在整個事件開始的時候就會被立刻調(diào)用,就可以通過重寫這個方法來監(jiān)聽整個事件的開始的過程良拼。剛剛也講到事件是派發(fā)到Window庸推,如果返回的是false就意味著事件沒人處理痛黎,所有的View都返回了false刮吧,那么Activity的onTouchEvent()
就會被調(diào)用。
我們接著看Window是如何將事件傳遞給ViewGroup的杀捻,點進去Window發(fā)現(xiàn)它是一個抽象方法井厌,這就是說要找到Window類的實現(xiàn)類,它的實現(xiàn)類就是PhoneWindow致讥,也是Window唯一的實現(xiàn)類垢袱,我們就從PhoneWindow類里面看它是怎么處理事件的,代碼如下:
//Window#superDispatchTouchEvent源碼
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
//PhotoWindow#superDispatchTouchEvent源碼
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView#superDispatchTouchEvent源碼
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
這里就很清楚了咳榜,在PhoneWindow將事件傳遞給了DecorView爽锥,這里也可以發(fā)現(xiàn),PhoneWindow包含了一個DecorView臣樱,那個DecorView是什么呢腮考?它就是一個頂級View,它是繼承與FrameLayout的嘴拢,從這里開始寂纪,我爺們也可以從源代碼中看到,最終事件傳遞到頂級View去孝冒,頂級View也叫根View拟杉,一般來說就是ViewGroup搬设,到達頂級View后撕捍,就會調(diào)用ViewGroup的dispatchTouchEvent()
方法泣洞。接下里就是ViewGroup的分發(fā)過程了。
ViewGroup的分發(fā)過程
這里是主要的分發(fā)過程甩挫,源碼內(nèi)容比較多,我們挑幾個重點的來講英遭。
//ViewGroup#dispatchTouchEvent源碼
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
......
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
首先看到的是onFilterTouchEventForSecurity()
方法删壮,這是判斷觸摸事件是否符合安全策略的方法央碟,安全策略就是用戶根據(jù)正常的使用習慣均函,用戶只會嘗試點擊只會看到的view,或者ViewGroup洛勉,看不到的視圖是沒有辦法去進行點擊的如迟,google為事件分發(fā)制定了一個安全策略,如果某一個view不處于視圖的頂部的話此再,也就是說當前的view不是直接與用戶交互的view玲销,并且這個view它的的一個屬性是該view不在頂部時,不去響應這樣一個觸摸事件贤斜,則不會分發(fā)這個事件逛裤,簡單來說带族,如果當前的view被其他視圖遮擋了洽糟,并且view設置了不在頂部時不響應觸摸事件的話,那么該方法就會返回false拍霜。
......
//ViewGroup#dispatchTouchEvent源碼
// Handle an initial down.
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();
}
// 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;
}
......
接著安全策略判斷為true的時候祠饺,首先會把觸摸事件的觸摸目標通過resetTouchState()
方法重置所有狀態(tài)汁政,為新的事件循環(huán)做準備。然后就會開始判斷是否要攔截當前的事件勺鸦,這里的要攔截的事件為MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null
目木,第一個好理解,第二個通過后面的代碼可以看出就是說當ViewGroup不攔截事件將事件交由子元素處理時mFirstTouchTarget != null
军拟,當事件由ViewGroup攔截時誓禁,mFirstTouchTarget != null
是不成立的摹恰,這個條件為false的話,ViewGroup的onInterceptTouchEvent()
就不會被調(diào)用姑宽,并且同一序列的其它事件都會默認交給它處理姜盈。如果ViewGroup不攔截事件的話,事件就會向下分發(fā)交給它的子View來進行處理示血,如果被ViewGroup攔截的話,就不會下發(fā)給子View瘫拣,會調(diào)用ViewGroup父類的dispatchTouchEvent()
進行處理告喊,下面的代碼是不攔截事件后,下發(fā)給子View進行處理的過程拢切。
//ViewGroup#dispatchTouchEvent源碼
final View[] children = mChildren;
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;
}
if (!canViewReceivePointerEvents(child)
|| !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);
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();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
以上代碼就是首先遍歷了所有ViewGroup的子元素淮椰,然后判斷子元素是否能接收到點擊事件主穗,是否能接收點擊事件主要是判斷該點擊位置是不是在子View的布局區(qū)域里面毙芜,如果在的話,事件就交由View來傳遞處理晦雨。
其實總結一下ViewGroup的dispatchTouchEvent()
主要就是做了3件事灯抛。
- 去判斷是否要攔截事件音瓷。
- 在當前ViewGroup中找到用戶真正點擊的View。
- 分發(fā)事件到View上纵竖。
View的分發(fā)過程
//View#dispatchTouchEvent源碼
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
//這里開始判斷有沒有設置mOnTouchListener
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
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
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
View的點擊事件處理就會簡單多了,因為它是一個單獨的元素堕担,沒有辦法再向下傳遞事件曲聂,所以只能自己處理事件,首先會判斷有沒有設置mOnTouchListener齐疙,如果mOnTouchListener里的onTouch()
返回true的話旭咽,那么onTouchEvent()
就不會被調(diào)用了穷绵,意思就是說mOnTouchListener的優(yōu)先級比onTouchEvent()
高。
//View#onTouchEvent源碼
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;
}
}
接著看View里面的onTouchEvent()
實現(xiàn)催训,這里是當View處于不可用的狀態(tài)下也會消耗點擊事件宗收,意思就是說,不可用狀態(tài)下的View照樣會消耗點擊事件采驻,最后就是看onTouchEvent()
對點擊事件的具體處理匈勋。
public boolean onTouchEvent(MotionEvent event) {
......
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);
}
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)) {
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 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();
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;
}
return false;
}
這里就直接放上源碼了饿自,看不懂沒事(我也沒看懂),翻了下書昭雌,意思就是View的CLICKABLE和LONG_CLICKABLE有一個為true烛卧,那么就會消耗掉這個事件,就是onTouchEvent()
為true,然后就是當ACTION_UP事件發(fā)生時就會觸發(fā)PerformClick()
方法跟磨,而且如果View設置了點擊事件監(jiān)聽攒盈,就會調(diào)用PerformClick()
里的onClick()
方法,到這里也就差不多結束了僵蛛。
//View#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;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
參考
- [任玉剛]Android開發(fā)藝術探索
- 慕課網(wǎng)-Android事件分發(fā)機制