1.?事件基礎
1.?MotionEvent
- 手指在屏幕的動作被封裝成了
MotionEvent
洞翩。 - 常用事件類型分為如下幾種:
-
MotionEvent.ACTION_DOWN
---->手指剛剛接觸屏幕 -
MotionEvent.ACTION_MOVE
---->手指在屏幕上移動 -
MotionEvent.ACTION_UP
------>手指從屏幕上松開的瞬間 -
MotionEvent.ACTION_CANCEL
-->這個比較復雜遍希,下文詳細分析
- 每個手勢操作都是以
ACTION_DOWN
開始绽媒,以ACTION_UP
結束愉择,中間夾雜著多個或者零個ACTION_MOVE
籍凝。
2. 所謂的事件分發(fā)機制屠缭,就是對MotionEvent
對象的分發(fā)過程。當我們的手勢操作產生了一個MotionEvent
對象時,系統(tǒng)需要把它傳遞給一個具體的View相寇。
3. 涉及到的三個主要方法:
1. public boolean dispatchTouchEvent(MotionEvent ev)
如果一個事件可以傳遞到當前View慰于,那么此View的這個方法一定可以被調用。此方法用來對事件進行分發(fā)唤衫,它的返回結果表示當前事件是否被處理婆赠,也就是此事件是否被消耗。
2. public boolean onInterceptTouchEvent(MotionEvent event)
這個方法表示當前View是或否攔截此事件佳励,在dispatchTouchEvent(MotionEvent ev)
中被調用休里。這個方法只在ViewGroup中存在,但是默認返回false
植兰,也就是View默認攔截傳遞到它的事件份帐,而ViewGroup默認不攔截任何事件。
3. public boolean onTouchEvent(MotionEvent event)
用來處理點擊事件楣导,我們設置的點擊事件最終是在這個方法中被調用。它同樣在dispatchTouchEvent(MotionEvent ev)
中被調用畜挨。
2.?事件分發(fā)的源碼解析
- 一個事件最先傳遞給Activity然后向下進行事件分發(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ā)現mWindow = new PhoneWindow(this, window, activityConfigCallback);
/****PhoneWindow***/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
這個mDecor
是DecorView的一個對象
/***DecorView***/
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}
我們知道,DecorView
就是通過setContentView()
這個方法設置的View的父View巴元。而它繼承自FrameLayout
顯然是一個View毡咏,更進一步說是一個ViewGroup,這樣我們的事件就從Activity傳遞到了View逮刨。</br>
所以我們說一個事件總是從ViewGruop開始分發(fā)呕缭,期間經歷多個或零個ViewGroup,最終以ViewGroup或者View終結修己。
- 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就會被賦值
dispatchTransformedTouchEvent(ev, false, children[i], idBitsToAssign)
}
}
}
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 調用的這個方法會去調用view.dispatchTouchEvent(),這樣onTouchEvent()就會被調用了
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;
}
以上源碼經過了精簡,只能表示分發(fā)流程睬愤,幫助理解片仿。詳細分析過程就不寫了。ViewGroup是個抽象類尤辱,我們不能直接用砂豌,干活的都是它的子類,但是我看了一圈貌似常用的幾種布局LinearLayout
光督、FrameLayout
等等都沒有重寫上述方法阳距,也就是說它們的分發(fā)過程都是上面這個過程。
- 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.?一些結論
1.某個View一旦決定攔截结借,那么這個事件序列都只能由它來處理筐摘。如果它是ViewGroup,那么它的onInterceptTouchEvent()
不會被調用。因為條件actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
為false蓄拣。
2.正常情況下扬虚,一個事件序列只能被一個View攔截消耗。因為一旦一個元素攔截了此事件球恤,那么同一個事件序列中的所有事件都會被直接交給它處理辜昵,因此同一個事件序列中的事件不能分別由兩個View同時處理蚕涤,但是通過特殊手段可以做到蹋笼,比如一個View將本該自己處理的事件通過onTouchEvent()
強行傳遞給其它View處理。
3.某個View一旦開始處理事件茄唐,如果它不能消耗ACTION_DOWN事件张惹,那么同一事件序列中的其它事件都不會再交給它來處理舀锨,并且事件將重新交給它的父元素去處理,即父元素的的onTouchEvent()
會被調用宛逗。意思是事件一旦交給了一個View處理坎匿,那么它就必須消耗掉,否則同一事件序列中剩下的事件就不會再交給它來處理了雷激。
4.如果View不消耗ACTION_DOWN以外的其它事件替蔬,那么這個點擊事件會消失,此時父元素的onTouchEvent()并不會被調用屎暇,并且當前View可以持續(xù)收到后續(xù)的事件承桥,最終這些消失的事件會傳遞給Activity處理。
5.View的onTouchEvent()
默認消耗事件(返回true)根悼,除非它是不可點擊的(clickable和longClickable都是false)凶异。View的longClickable默認都為false,clickable分情況。
6.onClick發(fā)生的前提是當前View可點擊并且接收到了Down和up事件挤巡。
7.事件分發(fā)總是從父元素傳遞給子元素剩彬,但是在子元素中可以通過requestDisallowInterceptTouchEvent()
來干預父元素中除了ACTION_DOWN之外的事件。
4.?ACTION_CANCEL
當控件收到前驅事件(什么叫前驅事件玄柏?一個從DOWN一直到UP的所有事件組合稱為完整的手勢襟衰,中間的任意一次事件對于下一個事件而言就是它的前驅事件)之后,后面的事件如果被父控件攔截粪摘,那么當前控件就會收到一個CANCEL事件瀑晒,并且把這個事件會傳遞給它的子事件。(注意:這里如果在控件的onInterceptTouchEvent中攔截掉CANCEL事件是無效的徘意,它仍然會把這個事件傳給它的子控件)之后這個手勢所有的事件將全部攔截苔悦,也就是說這個事件對于當前控件和它的子控件而言已經結束了。
在設計設置頁面的滑動開關時椎咧,如果不監(jiān)聽ACTION_CANCEL玖详,在滑動到中間時把介,如果你手指上下移動,就是移動到開關控件之外蟋座,則此時會觸發(fā)ACTION_CANCEL拗踢,而不是ACTION_UP,造成開關的按鈕停頓在中間位置向臀。意思就是巢墅,當用戶保持按下操作,并從你的控件轉移到外層控件時券膀,會觸發(fā)ACTION_CANCEL當前的手勢被中斷君纫,不會再接收到關于它的記錄。推薦將這個事件作為 ACTION_UP 來看待芹彬,但是要區(qū)別于普通的 ACTION_UP