點選事件的傳遞規則:
當一個MotionEvent産生後,系統需要把這個事件傳遞給一個具體的View,而這個過程就是事件的分發過程,這個過程由3個很重要的方法組成,dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent,
public boolean dispatchTouchEvent(MotionEvent ev)(ViewGroup裡面有的方法)
用來進行事件的分發.如果事件能夠傳遞到目前的View,那麼此方法一定會被調用,傳回結果受目前View的onTouchEvent和下級子View的dispatchTouchEvent方法的影響,表示是否消耗目前事件
public boolean onInterceptTouchEvent(MotionEvent ev)(ViewGroup裡面有的方法)
在上面的dispatchTouchEvent方法裡面調用,用來判斷是否攔截某個事件,如果目前View攔截了某個事件(例如down事件),那麼在同一個事件序列當中,此方法不會被再次調用,傳回結果表示是否攔截目前事件
說明:
同一個事件 序列是指:從手機接觸螢幕的那一刻起,到手機離開螢幕的那一刻結束,在這個過程中所産生的一系列事件,這個事件序列以down事件開始,中間含有數量不定的move事件,最終以up事件結束
public boolean onTouchEvent(MotionEvent event)(View裡面有的方法)
在上面的dispatchTouchEvent方法裡面調用,用來處理點選事件,傳回結果表示是否消耗目前事件,如果不消耗(down事件傳回false),則在同一個事件序列當中,目前view無法再次接受到(move和up)事件
這3個方法的關系可以用下面的為代碼來表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume=false;
if (onInterceptTouchEvent(ev)) {
//如果父類ViewGroup要攔截事件,那麼它就會調用自己的onTouchEvent方法來處理事件
consume=this.onTouchEvent(ev);
}else{
//如果父類ViewGroup不要事件,那麼它就會讓自己的孩子去分發事件
consume=child.dispatchTouchEvent(ev);
}
return consume;
}
當一個點選事件産生後,它的傳遞過程遵循如下順序:Activity–>window–>view,即事件總是先傳遞給Activity,Activity再傳遞給window,最後window再傳遞給頂級View
當一個View需要處理事件時,如果它設定了OnTouchListener,那麼OnTouchListener中的onTouch方法會被回調,如果onTouch方法傳回false,則目前的onTouchEven方法會被調用,如果傳回true,就不會調用onTouchEven方法,是以OnTouchListener的優先級大于
onTouchEven,在onTouchEvent方法中,如果目前設定的有onCLickListener,那麼它的onClick方法會被調用
優先級:OnTouchListener>onTouchEven(onClick()方法在這個此方法裡面)
正常情況下:一個事件序列隻能被一個View攔截且消耗
當一個事件傳到view上時:首先是觸發action_down方法,action_down傳回true表示此View要消耗這個事件,那麼同一個事件序列的action_move和action_up都會傳給它,action_down傳回false 表示此view不要這個事件,那麼同一個事件序列的action_move和action_up都不會傳到它那裡
事件的傳遞過程是由外向内,即事件總是先傳遞給父元素,然後再由父元素分發給子View,通過getParent().requestDisallowInterceptTouchEvent(boolean disallowIntercept);方法可以在子元素中幹預父元素的事件分發過程,但是父元素的action_down事件子元素幹預不了
事件分發的源碼解析:
1.Activity對點選事件的分發過程:
點選操作發生時,事件最先傳遞給目前的acivity,由activity的dispatchTouchEvent方法來進行分發,具體的工作是由activity内部的window來完成,window會把事件傳遞給decor view,decor view一般就是目前界面的底層容器(即setContentView所設定的View的父容器),通過Activity.getWindow.getDecorView()可以獲得.decor view繼承幀布局FrameLayout
源碼:Activity#dispatchTouchEvent(MotionEvent ev)
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
* @param ev The touch screen event.
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//這個方法是個空實作 interaction:互相作用,互相影響
//我們可以重寫添加我們的邏輯
onUserInteraction();
}
//交由activity所屬的window來派發事件
//傳回true;表示有View消耗了這個事件
//傳回false;表示沒有View消耗事件,需要act自己來處理
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果沒有view消耗事件,那麼act自己的onTouchEven會調用
return onTouchEvent(ev);
}
2.window.對點選事件的分發過程:
接下來看一下window是如何把事件傳遞給ViewGroup的
/* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {...}
源碼:window#superDispatchTouchEvent(MotionEvent event)
/**
* 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);
通過源碼和它的英文解釋,我們知道Window 是一個抽象類,window的唯一實作類是PhoneWindow
(如何進到PhoneWindow源碼裡面看具體代碼呢,這個有個小技巧:找到window裡面的一個抽象方法 例如這裡的getDecorView()方法,然後按住ctrl按鍵同時滑鼠點選getView(),出現如下圖的情況
找到PhoneWindow.java類,點選進去,就可以進到PhoneWindow.java類裡面了)
/** @hide */
public class PhoneWindow extends Window implements MenuBuilder.Callback {...}
源碼:PhoneWindow#superDispatchTouchEvent
//PhoneWindow實作window的superDispatchTouchEvent抽象方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
由上所知PhoneWindow把事件直接傳遞給了DecorView,至此window的事件就分發完成了
3.DecorView.對點選事件的分發過程:
首先我們看一下DecorView是什麼東西
源碼: phoneWindow#mDecor
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
/** @hide */
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {...}
由以上的源碼和英文解釋可以知道DecorView 是頂級View即根View,它繼承與FrameLayout.通過getWindow().getDecorView()獲得,我們常在activity中見的setContentView所設定的View,就可以通過mDecorView.findViewById(android.R.id.content).getChildAt(0)來得到
我們大緻看一下ViewGroup對事件的分發:
源碼 ViewGroup#dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
...代碼省略
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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;
}
....代碼省略
// 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;
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;
}
}
....代碼省略
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
這裡我們再回顧一下viewGroup事件的分發過程:
點選事件到達頂級View(一般是一個ViewGroup)以後,會調用ViewGroup的DispatchTouchEvent方法,如果頂級View攔截事件即onInterceptTouchEven方法傳回true,則事件由頂級ViewGroup自己處理,這時候,如果ViewGroup的onTouchListener被設定了,則onTouchListener的onTouch()方法會被調用,否則onTouchEven方法會被調用,也就是說如果都提供的話,onTouchListener()方法會屏蔽onTouchEven方法(),在onTouchEven方法中如果設定了onClickListerner,則onClick方法會被調用.如果viewGroup不攔截事件,事件會分發給它的子View,這時子View的DispatchTouchEvent方法會被調用,到此事件已經從頂級VIewGroup傳給了它的子view,即下來的傳遞過程和頂級ViewGroup的過程是一樣的,如此循環完成整個事件的分發
說明:
1當子view通過設定父View不要攔截事件後即
getParent().requestDisallowInterceptTouchEvent(true);
父ViewGroup就攔截不了ACTION_MOVE和 ACTION_UP事件,但是 還是可以攔截ACTION_DOWN事件,
理由:ViewGrop會在ACTION_DOWN事件到來時做重置狀态的操作,resetTouchState()方法中會對FLAG_DISALLOW_INTERCEPT進行重置,是以子view設定requestDisallowInterceptTouchEvent(true)不能影響ViewGroup對ACTION_DOWN事件的處理,源碼如下
源碼 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();
}
4.View對點選事件的分發
源碼: view#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...省略代碼
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;
}
}
...省略代碼
}
首先View會判斷有沒有設定OnTouchListener,如果有,則執行li.mOnTouchListener.onTouch(this, event)傳回true,否則onTouchEvent(event)方法會被調用,我們在看一下onTouchEvent(event)方法裡面的源碼
源碼:view#onTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event) {
...省略代碼
switch (action) {
case MotionEvent.ACTION_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)) {
performClick();
}
}
}
...省略代碼
}
由以上源碼可知在 MotionEvent.ACTION_UP事件裡 如果 if (!post(mPerformClick)) 條件為真的情況下,會執行performClick()方法
源碼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;
}
在performClick() 裡面如果 if (li != null && li.mOnClickListener != null) 條件為真的情況下,會執行 li.mOnClickListener.onClick(this);
至此,事件分發機制已經講完啦~(容我喝口水,吃跟辣條休息一下,精彩馬上繼續^- ^)