上面介紹了View的基礎知識以及View的滑動,本節將介紹View的一個核心知識點:事件分發機制。事件分發截止不僅僅是核心知識點更是難點,不少初學者甚至中介開發者面對這個問題是都會覺得很困惑。另外View的另一大難題滑動衝突,它的解決辦法的理論基礎就是事件分發機制,因此掌握好View的事件分發機制是十分重要的。本節將深入介紹View的事件分發機制。
1.點擊事件的傳遞規則
在介紹點擊事件的傳遞規則之前,首先我們要明白這裏要分析的對象是MotionEvent,即點擊事件,關於MotionEvent在之前已經進行了介紹。所謂點擊事件的分發,其實就是對MotionEvent事件的分發過程,即當一個MotionEvent產生了以後,系統需要把這個事件傳遞給一個具體的View,而這個傳遞的過程就是分發的過程。點擊事件的分發過程由三個很重要的方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面我們先介紹一下這幾個方法。
public boolean dispatchTouchEvent(MotionEvent ev)
用來進行事件的分發。如果事件能夠傳遞給當前View,那麼此方法一定會被調用,返回結果受當前View的ouTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。
public boolean onTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接受到事件。
上述的三個方法到底由什麼區別呢?它們是什麼關係?其實它們的關係可以用如下僞代碼表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchenevt(ev);
}
return consume;
}
上述僞代碼已經將三者的關係表現得淋漓盡致。通過上面得僞代碼,我們也可以大致瞭解點擊事件的傳遞規則:對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接着事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調用;如果這個ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會被調用,如此反覆直到事件被最終處理。
當一個View需要處理事件時,如果它設置了onTouchListener,那麼onTouchListener中的onTouch方法會被回調。這時事件如何處理還要看onTouch的返回值,如果返回false,則當前View的onTouchEvent方法會被調用;如果返回true,那麼onTouchEvent方法將不會被調用。由此可見,給View設置的OnTouchListener,其優先級比onTouchEvent要高。在onTouchEvent方法中,如果當前設置的有OnTouchListener,那麼它的onClick方法會被調用。可以看出,平常我們常用的OnClickListener,其優先級最低,即處於事件傳遞的尾端。
當一個點擊事件產生後,它的傳遞過程遵循如下順序:Activity->Window->View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最後Window再傳遞給頂級View。頂級的View接收到事件後,就會按照事件分發機制去分發事件。考慮一種情況,如果一個View的onTouchEvent返回false,那麼它的父容器的ouTouchEvent將會被調用,以次類推。如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理,即Activity的onTouchEvent方法會被調用。這個過程其實也很好理解,我們可以換一種思路,加入點擊事件是一個難題,這個難題最終被上級領導分給了一個程序員去處理(這是事件的分發過程),結果這個程序員搞不定(ouTouchEvent返回false),那麼現在該怎麼辦呢?難題必須要解決,那隻能交給水平更高的上級解決(上級的onTouchEvent被調用),如果上級再搞不定,那智能交給上級的上級去解決,這樣將難題一層層地向上拋,這是公司內部一種很常見地處理問題地過程。從這個角度來看,View的事件傳遞過程還是很貼近顯示的,畢竟程序員也生活在現實中。
關於事件傳遞的機制,這裏給出一些結論,根據這些結論可以更好地理解整個傳遞機制,如下所示。
(1)同一個事件序列是指從手指觸摸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中所產生的一些列事件,這個事件序列以down事件開始,中間含有數量補丁的move,最終以up事件結束。
(2)正常情況下,一個事件序列只能被一個View攔截且消耗。這一條的原因可以參考(3),因爲一旦一個蒜素攔截了某次事件,那麼同一個事件序列內的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理,但是通過特殊的手段可以做到,比如一個View將本該自己處理的事情通過onTouchEvent強行傳遞給其他View處理。
(3)某個View一旦決定攔截,那麼這個事件序列只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的onInterceptTouchEvent不再會被調用。這條也很好理解,就是說當一個View決定攔截一個事件後,那麼系統就會把同一個事件序列內的其他方法都直接交給它來處理,因此就不再調用這個View的onInterceptTouchEvent去詢問它是否要攔截了。
(4)某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會被調用。意思是事件一旦交給一個View處理,那麼它必須消耗掉,否則同意事件序列中剩下的事件就不再交給它來處理了,這就好比上級交給程序員意見事,如果這件事沒有處理好,短期內上就就不敢再把事情交給這個程序員了,二者事類似的道理。
(5)如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點擊事件會消失,此時父元素的onTouchEvent並不會被調用,並且當前View可以持續收到後續事件,最終這些消失的點事件會傳遞給Activity處理。
(6)ViewGroup默認不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認返回false。
(7)View沒有onInterceptTouchEvent方法,一點由點擊事件傳遞給它,那麼它的onTouchEvent方法就會被調用。
(8)View的onTouchEvent默認都會消耗事件(返回true),除非他是不可點擊(clickable和longClickable同時爲false)。View的longClickable屬性默認爲false,clickable屬性要分情況,比如Button的clickable屬性默認爲true,而TextView得clickable屬性默認爲false。
(9)View得enable屬性不影響onTouchEvent得默認返回值。哪怕一個View時disable狀態的,只要它的clickable或者longClickable有一個爲true,那麼它的onTouchEvent就返回true。
(10)onClick會發送的前提時當前View是可點擊的,並且它收到了down和up的事件。
(11)事件傳遞額過程是由外向內的,及時間總是先傳遞給父元素,然後再由父元素分發給子元素,通過requestDisallowInterceptTouchEvent方法可以再子元素中干預父元素的事件分發過程,但是ACTION_DOWN事件除外。
2.事件分發的源碼解析
上一節分析了View的事件分發機制,本節將會從源碼的角度去進一步分析、證實上面的結論。
1.Activity對點擊事件的分發過程。
點擊事件用MotionEvent來表示,當一個點擊操作發生時,事件最先傳給給當前Activity,由Activity的dispatchTouchEvent來進行事件派發,具體的工作是由Activity內部的Window來完成的。Window會將事件傳遞給decor view,decor view一般是當前界面的底層容器(setContentView所設置的View的父容器),通過Activity.getWindow.getDecorView()可以獲得。我們先從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進行分發,如果返回true,整個事件循環就結束了,返回false意味着事件沒人處理,所有View的onTouchEvent都返回了false,那麼Activity的onTouchEvent就會被調用。
接下來看Window是如何將書劍傳遞給ViewGroup的。通過源碼我們知道,Window是個抽象類,而Window的superDispatchTouchEvent方法也是個抽象方法,因此我們必須找到Window的實現類才行。
public abstract boolean superDispatchTouchEvent(MotionEvent event);
那麼到底Window的實現類是什麼呢?其實是PhoneWindow,這一點從Window的源碼中也可以看出來,再Window的說明中,有這麼一段話:
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
上面這段話的大概意思是:window類可以控制頂級View的外觀和行爲策略,它的唯一實現位於android.policy.PhoneWindow中,當你要實例化這個Window類的時候,你並知道它的細節,因爲這個類會被重構,只有一個工程方法可以使用。儘管着看起來有點模糊,不過我們可以看一下android.policy.PhoneWindow這個類,儘管實例化的時候此類會被重構,進式重構而已,功能是類似的。
由於Window的唯一實現類是PhoneWindow,因此接下來看一下PhoneWindow是如何處理點擊事件的, 如下所示。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
到這裏邏輯就很清晰了,PhoneWindow將事件直接傳遞給了DecorView,這個DecorView是什麼呢?請看下面:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
我們知道,通過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)這種方式就可以獲取Activity所設置的View,這個mDecor顯然就是getWindow().getDecorView()返回的View,而我們通過setContentView設置的View是它的一個子View。目前事件傳遞到了DecorView這裏,由於DecorView繼承自FrameLayout且是父View,所以最終事件傳遞給View。換句話說,事件肯定會傳遞到View,不然應用如何響應點擊事件呢?不過這不是我們的重點,重點是事件到了View以後該如何傳遞,這對我們更有好處。從這裏開始,事件意見傳遞到頂級View了,即再Activity中通過setContentView所設置的View,另外頂級View也叫根View,頂級View一般來說是ViewGroup。
2.頂級View對點擊事件的分發過程
關於點擊事件如何在View中進行分發,上一節一屆做了詳細的介紹,這裏再大致回顧一下。點擊事件到達頂級View(一般是一個ViewGroup)以後,會調用ViewGroup的dispatchTouchEvent方法,然後的邏輯是這樣的:如果頂級ViewGroup攔截事件即onInterceptTouchEvent返回true,則事件由ViewGroup處理,這時如果ViewGroup的mOnTouchListener被設置,則onTouch會被調用,否則onTouchEvent會被調用。也就是說,如果提供的話,onTouch會屏蔽調onTouchEvent。再onTouchEvent中,如果設置了mOnClickListener,則onClick會被調用。如果頂級ViewGroup不攔截事件,則事件會傳遞給它所在的點擊事件鏈上的子View,這時子View的dispatchTouchEvent會被調用。到此爲止,事件已經從頂級View傳遞給了下一層View,接下來的傳遞過程和頂級View是一致的,如此循環,完成整個事件的分發。
首先看ViewGroup對點擊事件的分發過程,其主要實現再ViewGroup的dispatchTouchEvent方法中,這個方法比較長,這裏分段說明。先看下面一段,很顯然,它描述的是當前View是否攔截點擊事情這個邏輯。
// 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;
}
從上面代碼我們可以看出,ViewGroup在如下兩種情況下會判斷是否要攔截當前事件:事件類型爲ACTION_DOWN或者mFirstTouchTarget!=null是什麼意思呢?這個從後面的代碼邏輯可以看出來,當事件由ViewGroup的子元素成功處理時,mFirstTouchTarget會被賦值並指向子元素,換句話來說,當ViewGroup不攔截事件並將事件交由子元素處理時mFirstTouchTarget !=null。反過來,一旦事件由當前ViewGroup攔截時,mFirstTouchTarget !=null就不成立。那麼當ACTION_MOVE和ACTION_UP事件到來時,由於(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)這個條件爲false,將導致ViewGroup的onInterceptTouchEvent不會再被調用,並且同一序列中的其他事件都會默認交給它處理。
// 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();
}
從上面的源碼分析,我們可以得出結論:當ViewGroup決定攔截事件後,那麼後續的點擊事件將會默認交給它處理並且不再調用它的onInterceptTouchEvent方法,這正式了上節中結尾處的第3條結論。FLAG_DISALLOW_INTERCEPT這個標識的作用時讓ViewGroup不再攔截事件,當然前提時ViewGroup不攔截ACTION_DOWN事件,這證實了上節末尾處的第11條結論。那麼這段分析對我們有什麼價值呢?總結起來有兩點:第一點onInterceptTouchEvent不是每次事件都會被調用的,如果我們想提前處理所有的點擊事件,要選擇dispatchTouchEvent方法,只有這個方法能確保每次都會被調用,當然前提時事件能夠傳遞到當前的ViewGroup;另外一點,FLAG_DISALLOW_INTERCEPT標記位的作用給我們提供了一個思路,當面對滑動衝突時,我們是不是可以考慮用這種方法去解決問題?關於滑動衝突將在霞姐中進行詳細分析。
接下再看當VIewGroup不攔截事件的時候,事件會向下分發由他的子View進行處理,這段源碼如下所示。
final View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
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();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
上面這段代碼邏輯也很清晰,首先遍歷ViewGroup的所有子元素,然後判斷子元素是否能夠接收到到點擊事件。是否能夠接受點擊事件主要由兩點來衡量:子元素是否再播放動畫和點擊事件的座標是否落在子元素的區域內。如果某個子元素滿足這兩個條件,那麼事件就會傳遞給他來處理。可以看到,dispatchTransformedTouchEvent實際上調用的就是子元素的dispatchTouchEvent方法,在它的內部有如下一段內容,而在上面的代碼中child傳遞的不是null,因此它會直接調用子元素的dispatchTouchEvent方法,這樣事件就交由子元素處理了,從而完成了一輪事件分發。
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
如果子元素的dispatchTouchEvent返回true,這時我們暫時不用考慮事件在子元素內部是怎麼分發的,那麼mFirstTouchTarget就會被賦值同事跳出for循環,如下所示。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
這幾行代碼完成了mFirstTouchTarget的賦值並終止對子元素的遍歷。如果子元素的dispatchTouchEvent返回false,ViewGroup就會把事件分發給下一個子元素(如果還有下一個子元素的話)。
其實mFirstTouchTarget真正的賦值過程是在addTouchTarget內部完成的,從下面的addTouchTarget方法內部結構可以看出,mFirstTouchTarget其實是一種單鏈表結構。mFirstTouchTarget是否被賦值將直接影響到ViewGroup對事件的攔截策略,如果mFirstTouchTarget爲null,那麼ViewGroup就默認攔截下來同意序列中所有的點擊事件,這一點在前面已經做了分析。
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果遍歷所有的子元素後事件都沒有被合適地處理,這包含兩種情況:第一種是ViewGroup沒有子元素;第二種是子元素處理了點擊事件,但在dispatchTouchEvent中返回了false,這一版是因爲子元素在onTouchEvent中返回了false。這兩種情況下,ViewGroup會自己處理點擊事件,這裏就證實了上節中地第4條結論,代碼如下所示。
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
注意上面這段代碼,這裏第三個參數chind爲null,從前面地分析可以知道,它會調用super.dispatchTouchEvent(event),很顯然,這裏就轉到View地dispatchTouchEvent方法,即點擊事件開始交由View來處理,請看下面的分析。
4.View對點擊事件的處理過程
View對點擊事件的處理過程稍微簡單一些,注意這裏的View不包含ViewGroup。先看它的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();
}
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對點擊事件的處理過程就比較簡單了,因爲View(這裏不包含ViewGroup)是一個單獨的元素,它沒有子元素因此無法向下傳遞事件,所以它只能自己處理事件。從上面的源碼可以看出View對點擊事件的處理過程,首先會判斷有沒有設置OnTouchListener。如果OnTouchListener中的onTouch方法返回true,那麼onTouchEvent就不會被調用,可見OnTouchListener的優先級高於OnTouchEvent,這樣做的好處是方便在外界處理點擊事件。
接着在分析onTouchEvent的實現。先看當View處於不可剪輯狀態下點擊事件的處理過程,如下所示。很顯然,不可用狀態下的View照樣會消耗點擊事件,儘管它看起來不可用。
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;
}
接着,如果View設置有代理,那麼還會執行TouchDelegate的onTouchEvent方法,這個onTouchEvent的工作機制看起來和onTouchListener類似,這裏不深入研究了。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
下面再看下onTouchEvent中對點擊事件的具體處理,如下所示。
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;
}
從上面的代碼來看,只要View的CLICKABLE和LOG_CLICKABLE有一個爲true,那麼它就會消耗這個事件,即onTouchEvent方法返回true,不管他是不是DISABLE狀態,這就證實了上節末尾處第8、第9和第10條結論。然後就是當ACTION_UP事件發生時,會觸發performClick方法,如果View設置了onClickListener,那麼performClick方法內部會調用它的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;
}
View的LONG_CLICKABLE的屬性默認爲false,而CLICKABLE屬性是否爲false和具體的Vie有關,確切來說時可點擊的View其CLICKABLE爲true,不可點擊的View其CLICKABLE爲false,比如Button時可點擊的,TextView是不可點擊的。通過setClickable和setLongClickAble可以分別改變View的CLICKABLE和LONG_CLICKABLE屬性。另外,setOnClickListener會自動將View的CLICKABLE設爲true,setOnLongClickListener則會自動將View的LONG_CLICKABLE設爲true,這一點從源碼中可以看出來,如下所示。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
到這裏,點擊事件的分發機制的源碼實現已經分析完了。