1.?事件基礎(chǔ)
1.?MotionEvent
- 手指在屏幕的動(dòng)作被封裝成了
MotionEvent
。 - 常用事件類型分為如下幾種:
-
MotionEvent.ACTION_DOWN
---->手指剛剛接觸屏幕 -
MotionEvent.ACTION_MOVE
---->手指在屏幕上移動(dòng) -
MotionEvent.ACTION_UP
------>手指從屏幕上松開的瞬間 -
MotionEvent.ACTION_CANCEL
-->這個(gè)比較復(fù)雜,下文詳細(xì)分析
- 每個(gè)手勢操作都是以
ACTION_DOWN
開始,以ACTION_UP
結(jié)束,中間夾雜著多個(gè)或者零個(gè)ACTION_MOVE
。
2. 所謂的事件分發(fā)機(jī)制,就是對(duì)MotionEvent
對(duì)象的分發(fā)過程。當(dāng)我們的手勢操作產(chǎn)生了一個(gè)MotionEvent
對(duì)象時(shí),系統(tǒng)需要把它傳遞給一個(gè)具體的View。
3. 涉及到的三個(gè)主要方法:
1. public boolean dispatchTouchEvent(MotionEvent ev)
如果一個(gè)事件可以傳遞到當(dāng)前View,那么此View的這個(gè)方法一定可以被調(diào)用。此方法用來對(duì)事件進(jìn)行分發(fā),它的返回結(jié)果表示當(dāng)前事件是否被處理,也就是此事件是否被消耗。
2. public boolean onInterceptTouchEvent(MotionEvent event)
這個(gè)方法表示當(dāng)前View是或否攔截此事件,在dispatchTouchEvent(MotionEvent ev)
中被調(diào)用。這個(gè)方法只在ViewGroup中存在,但是默認(rèn)返回false
,也就是View默認(rèn)攔截傳遞到它的事件,而ViewGroup默認(rèn)不攔截任何事件。
3. public boolean onTouchEvent(MotionEvent event)
用來處理點(diǎn)擊事件,我們設(shè)置的點(diǎn)擊事件最終是在這個(gè)方法中被調(diào)用。它同樣在dispatchTouchEvent(MotionEvent ev)
中被調(diào)用。
2.?事件分發(fā)的源碼解析
- 一個(gè)事件最先傳遞給Activity然后向下進(jìn)行事件分發(fā)。我們從Activity的
dispatchTouchEvent()
開始分析
/***Activity.dispatchTouchEvent(MotionEvent ev)***/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public Window getWindow() {
return mWindow;
}
通過在Activity中查找我們可以發(fā)現(xiàn)mWindow = new PhoneWindow(this, window, activityConfigCallback);
/****PhoneWindow***/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
這個(gè)mDecor
是DecorView的一個(gè)對(duì)象
/***DecorView***/
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}
我們知道,DecorView
就是通過setContentView()
這個(gè)方法設(shè)置的View的父View。而它繼承自FrameLayout
顯然是一個(gè)View,更進(jìn)一步說是一個(gè)ViewGroup,這樣我們的事件就從Activity傳遞到了View。</br>
所以我們說一個(gè)事件總是從ViewGruop開始分發(fā),期間經(jīng)歷多個(gè)或零個(gè)ViewGroup,最終以ViewGroup或者View終結(jié)。
- ViewGroup的事件分發(fā)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
if (!canceled && !intercepted) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
//如果子元素成功處理了事件,則mFirstTouchTarget就會(huì)被賦值
dispatchTransformedTouchEvent(ev, false, children[i], idBitsToAssign)
}
}
}
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 調(diào)用的這個(gè)方法會(huì)去調(diào)用view.dispatchTouchEvent(),這樣onTouchEvent()就會(huì)被調(diào)用了
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.
dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits);
}
return handled;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
以上源碼經(jīng)過了精簡,只能表示分發(fā)流程,幫助理解。詳細(xì)分析過程就不寫了。ViewGroup是個(gè)抽象類,我們不能直接用,干活的都是它的子類,但是我看了一圈貌似常用的幾種布局LinearLayout
、FrameLayout
等等都沒有重寫上述方法,也就是說它們的分發(fā)過程都是上面這個(gè)過程。
- View的事件分發(fā)
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
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;
}
public boolean onTouchEvent(MotionEvent event) {
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) {
// 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) {
performClickInternal()-->performClick()-->onClick();
return true;
}
return false;
}
3.?一些結(jié)論
1.某個(gè)View一旦決定攔截,那么這個(gè)事件序列都只能由它來處理。如果它是ViewGroup,那么它的onInterceptTouchEvent()
不會(huì)被調(diào)用。因?yàn)闂l件actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
為false。
2.正常情況下,一個(gè)事件序列只能被一個(gè)View攔截消耗。因?yàn)橐坏┮粋€(gè)元素?cái)r截了此事件,那么同一個(gè)事件序列中的所有事件都會(huì)被直接交給它處理,因此同一個(gè)事件序列中的事件不能分別由兩個(gè)View同時(shí)處理,但是通過特殊手段可以做到,比如一個(gè)View將本該自己處理的事件通過onTouchEvent()
強(qiáng)行傳遞給其它View處理。
3.某個(gè)View一旦開始處理事件,如果它不能消耗ACTION_DOWN事件,那么同一事件序列中的其它事件都不會(huì)再交給它來處理,并且事件將重新交給它的父元素去處理,即父元素的的onTouchEvent()
會(huì)被調(diào)用。意思是事件一旦交給了一個(gè)View處理,那么它就必須消耗掉,否則同一事件序列中剩下的事件就不會(huì)再交給它來處理了。
4.如果View不消耗ACTION_DOWN以外的其它事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的onTouchEvent()并不會(huì)被調(diào)用,并且當(dāng)前View可以持續(xù)收到后續(xù)的事件,最終這些消失的事件會(huì)傳遞給Activity處理。
5.View的onTouchEvent()
默認(rèn)消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable都是false)。View的longClickable默認(rèn)都為false,clickable分情況。
6.onClick發(fā)生的前提是當(dāng)前View可點(diǎn)擊并且接收到了Down和up事件。
7.事件分發(fā)總是從父元素傳遞給子元素,但是在子元素中可以通過requestDisallowInterceptTouchEvent()
來干預(yù)父元素中除了ACTION_DOWN之外的事件。
4.?ACTION_CANCEL
當(dāng)控件收到前驅(qū)事件(什么叫前驅(qū)事件?一個(gè)從DOWN一直到UP的所有事件組合稱為完整的手勢,中間的任意一次事件對(duì)于下一個(gè)事件而言就是它的前驅(qū)事件)之后,后面的事件如果被父控件攔截,那么當(dāng)前控件就會(huì)收到一個(gè)CANCEL事件,并且把這個(gè)事件會(huì)傳遞給它的子事件。(注意:這里如果在控件的onInterceptTouchEvent中攔截掉CANCEL事件是無效的,它仍然會(huì)把這個(gè)事件傳給它的子控件)之后這個(gè)手勢所有的事件將全部攔截,也就是說這個(gè)事件對(duì)于當(dāng)前控件和它的子控件而言已經(jīng)結(jié)束了。
在設(shè)計(jì)設(shè)置頁面的滑動(dòng)開關(guān)時(shí),如果不監(jiān)聽ACTION_CANCEL,在滑動(dòng)到中間時(shí),如果你手指上下移動(dòng),就是移動(dòng)到開關(guān)控件之外,則此時(shí)會(huì)觸發(fā)ACTION_CANCEL,而不是ACTION_UP,造成開關(guān)的按鈕停頓在中間位置。意思就是,當(dāng)用戶保持按下操作,并從你的控件轉(zhuǎn)移到外層控件時(shí),會(huì)觸發(fā)ACTION_CANCEL當(dāng)前的手勢被中斷,不會(huì)再接收到關(guān)于它的記錄。推薦將這個(gè)事件作為 ACTION_UP 來看待,但是要區(qū)別于普通的 ACTION_UP