在瞭解view的時間分發機制之前,我們先了解MotionEvent這個對象
MotionEvent
在手指接觸屏幕後所產生的一系列事件中,典型的時間類型有如下幾種:
- ACTION_DOWN--------手機剛接觸屏幕‘
- ACTION_MOVE----------手指在屏幕上移動
- ACTION_UP -----------手指從屏幕上鬆開的一瞬間
上述三種情況是典型的事件序列,同時通過MotionEvent對象我們可以得到點擊事件發生的x和y座標,爲此係統提供了兩組方法,getX/getY和getRawX/getRawY。它們的區別其實很簡單,getX/getY返回的是相對於當前view左上角的x和y座標,而getRawX/getRawY返回的是相對於手機屏幕左上角的x和y座標。
點擊事件的傳遞規則
所謂的點擊事件的事件分發,其實即使對MotionEvent事件的分發過程,即當一個MotionEvent產生以後,系統需要把這個事件傳遞給一個具體的view,而這個傳遞的過程就是分發過程。點擊事件的分發過程由三個很重要的方法來完成:
-
public boolean dispatchTouchEvent(MotionEvent ev) :用來進行事件的分發。如果事件能夠傳遞給當前view,那麼此方法一定會被調用,返回結果受當前view的ontouchEvent和下級view的dispatchTouchEvent方法的影響,表示是否消耗當前事件
- public boolean onTouchEvent(MotionEvent event): 在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中(即action_down,action_move,actiong_up),當前view無法再次接收到事件,也就是說假如在action-down事件中沒有消耗,那麼action-move,action-up就不會再次觸發這個方法,只會走activity中的dispatchTouchEvent和ontouchevent方法
-
public boolean onInterceptTouchEvent(MotionEvent ev):在dispatchTouchEvent方法內部調用,用來判斷是否攔截某個事件,如果當前view攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。
上述三個方法的區別用下面的僞代碼表示:
/**
* 點擊事件產生後
*/
// 步驟1:調用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否會消費事件
// 步驟2:判斷是否攔截事件
if (onInterceptTouchEvent(ev)) {
// a. 若攔截,則將該事件交給當前View進行處理
// 即調用onTouchEvent ()方法去處理點擊事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不攔截,則將該事件傳遞到下層
// 即 下層元素的dispatchTouchEvent()就會被調用,重複上述過程
// 直到點擊事件被最終處理爲止
consume = child.dispatchTouchEvent (ev) ;
}
// 步驟3:最終返回通知 該事件是否被消費(接收 & 處理)
return consume;
}
當一個view需要處理事件時,如果它設置了onTouchListener,那麼OnTouchListener中的onTouch方法會被回調,這是事件如何處理還要看onTouch的返回值,如果返回flase,則當前view的OnTouchEvent方法會被調用,如果返回true,那麼此方法就不會被調用。由此可見,給view設置的OnTouchListener,其優先級比ontouchevent要高。根據源碼來看,我們平時用的onClickListener,其優先級最低。
當一個點擊事件產生後,它的傳遞順序過程遵循如下順序:
Activity-----》Window----》View
即事件總是先傳遞給activity,activity再傳給window,最後window在傳給頂級的view,頂級的view接收到事件後,就會按照時間分發機制去分發事件,及先後調用上面三個方法。考慮一種情況,如果一個view的onTouchEvent返回false,那麼他的父容器的OnTouchEvent將會調用,以此類推。也就是說 如果各個層級都不消耗事件的話,那麼從頂級view開始分發後,一直到最上面的view,這是分發,然後經過最上面view的ontouchevent方法去處理此事件,如果ontouchevent不去處理的話,就返回到他的上層的ontouchevent方法去處理,其實就是一個U型。大家慢慢去理解。
事件源碼分析
Activity對點擊事件的分發過程
當一個點擊操作發生時,事件最先傳遞給當前的activity,由activity的dispatchTouchEvent來進行事件的派發,具體的工作有activity內部的window來完成的。window會將事件傳遞給decorview,decorview一般就是當前界面的底層容器(即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分發最後是到了頂層的view,中間過程就不詳細說了。
viewGroup對點擊事件的分發過程
點擊事件達到頂級view(一般是一個viewGroup)以後,會調用viewgroup的diapatchtouchevent方法,如果viewGroup攔截事件即onInterceptTouchEvent返回true,則事件由viewGroup處理,這是如果viewGroup的ontouchlistener被設置了,則onTouch會被調用,如果onTouch返回true,就會屏蔽掉onTouchEvent,如果返回false,會接着執行OnTouchEvent方法,好了 下面我們看一下dispatchtouchevent方法的源碼:
// 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:當前由我們觸發的點擊事件,也即是說ACTION_MOVE和ACTION_UP事件來時,則不觸發攔截事件
- mFirstTouchTarget != null:當ViewGroup不攔截事件並將事件交給子View的時候該不等式成立。反過來,事件被ViewGroup攔截時,該不等式不成立
然後接着看viewgroup遍歷子view:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
final View[] children = mChildren;
//遍歷所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判斷子View是否能接收點擊事件
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);
//事件傳遞到子View,下面追蹤該方法
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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
......
}
ViewGroup直接使用for遍歷所有子View,對子View的各種狀態進行判斷,最後調用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)將事件傳遞給子View,下面是dispatchTransformedTouchEvent()方法的部分源碼
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
其最後就是分發給子View的dispatchTouchEvent()方法,在這裏,我有一些不明白的地方,但是經過demo測試,如果嵌套了好幾個viewgroup類型的view,那麼就會多次執行viewgroup的dispatchTouchEvent()方法,如果最上層是一個view的話,比如textivew,那麼就會走view的dispatchTouchEvent(),那麼接下里就會進入view的事件分發,我自己感覺,也就是說,如果說嵌套的佈局裏面沒有view(類似textview這一類),那麼就不會走view的dispatchTouchEvent,只能走viewgroup的dispatchTouchEvent,不知道這樣理解對不對,希望看到的大佬給個建議
view對點擊事件的分發過程
view的dispatchTouchEvent源碼分析
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
......
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;
}
}
......
return result;
}
從源碼判斷處看出,首先會判斷有沒有設置mOnTouchListener,如果mOnTouchListener不爲空,那麼onTouchEvent就不會被調用,這裏可以得到一個結論,若在View中設置了OnTouchListener,那麼它的優先級是高於onTouchEvent的,這樣可以更好的讓我們自己setOnTouchEventListener()處理點擊事件
onTouchEvent源碼處理事件的具體做法部分
public boolean onTouchEvent(MotionEvent event) {
......
//當View處於不可用狀態下,也會消耗點擊事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
......
//對點擊事件的具體處理
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
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();
}
}
}
......
}
}
return true;
}
......
}
從對點擊事件的具體處理中看出,只要View的CLICKABLE和LONG_CLICKABLE有一個爲true,那麼它就會消耗這個事件,即onTouchEvent方法返回true。在ACTION_UP事件中,會觸發PerformClick()方法,如果View設置了OnClickListener,那麼PerformClick()方法內部會調用它的onClick()方法