什么是事件分發(fā)。當(dāng)我們點(diǎn)擊屏幕時(shí),就產(chǎn)生了點(diǎn)擊事件,這個(gè)事件被封裝成了一個(gè)類(lèi):MotionEvent。而當(dāng)這個(gè) MotionEvent產(chǎn)生后,那么系統(tǒng)就會(huì)將這個(gè)MotionEvent傳遞給View的層級(jí),MotionEvent在View中的層級(jí)傳遞過(guò)程就是點(diǎn)擊事件分發(fā)。
1.MotionEvent
MotionEvent類(lèi)就是記錄手指接觸屏幕后所產(chǎn)生的一系列的事件,下面看幾個(gè)常用的事件的類(lèi)型與含義:
| 事件 | 含義 |
|---|---|
| MotionEvent.ACTION_DOWN | 手指按下時(shí)觸發(fā) |
| MotionEvent.ACTION_UP | 手指抬起時(shí)觸發(fā) |
| MotionEvent.ACTION_MOVE | 手指移動(dòng)時(shí)觸發(fā) |
| MotionEvent.ACTION_CANCEL | 事件被攔截時(shí)觸發(fā) |
| MotionEvent.ACTION_OUTSIDE | 手指不在控件區(qū)域時(shí)觸發(fā) |
- 點(diǎn)擊事件: ACTION_DOWN-->ACTION_UP
- 滑動(dòng)事件:ACTION_DOWN-->ACTION_MOVE--> ACTION_UP
2.點(diǎn)擊事件分發(fā)的傳遞規(guī)則
點(diǎn)擊事件是由三個(gè)很重要的方法來(lái)實(shí)現(xiàn)的,分別是dispatchTouchEvent(MotionEvent ev)、onInterceptTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent event)。
-
dispatchTouchEvent(MotionEvent ev)
用來(lái)進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前View,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。 -
onInterceptTouchEvent(MotionEvent ev)
在上述方法內(nèi)部調(diào)用,用來(lái)判斷是否攔截某個(gè)事件,如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列中,此方法不會(huì)被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件。 -
onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用來(lái)處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗,則在同一個(gè)事件序列中,當(dāng)前View無(wú)法再次接收到事件。
這三個(gè)方法就是事件分發(fā)機(jī)制中的核心三個(gè)方法,也是我們下面在源碼中重要去分析的三個(gè)方法。他們?nèi)咧g的關(guān)系可以概述如下 (注意這是一段偽代碼,只是為了對(duì)解釋三個(gè)方法關(guān)系):
//點(diǎn)擊事件產(chǎn)生后
// 步驟1:調(diào)用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否會(huì)消費(fèi)事件
// 步驟2:判斷是否攔截事件
if (onInterceptTouchEvent(ev)) {
// a. 若攔截,則將該事件交給當(dāng)前View進(jìn)行處理
// 即調(diào)用onTouchEvent ()方法去處理點(diǎn)擊事件
consume = onTouchEvent(ev);
} else {
// b. 若不攔截,則將該事件傳遞到下層
// 即 下層元素的dispatchTouchEvent()就會(huì)被調(diào)用,重復(fù)上述過(guò)程
// 直到點(diǎn)擊事件被最終處理為止
consume = child.dispatchTouchEvent(ev);
}
// 步驟3:最終返回通知 該事件是否被消費(fèi)(接收 & 處理)
return consume;
}
根據(jù)上面這段偽代碼能夠很好的理解三者的關(guān)系。當(dāng)我們點(diǎn)擊事件產(chǎn)生后,它的傳遞過(guò)程如下順序:Activity->ViewGroup->View。即事件總是先傳遞給Activity,Activity傳遞給ViewGroup, 再由ViewGroup傳遞給View。頂級(jí)View接收到事件后,就會(huì)按照事件分發(fā)機(jī)制去分發(fā)事件。
對(duì)于一個(gè)根ViewGroup,點(diǎn)擊事件產(chǎn)生后,首先會(huì)傳遞給它,這時(shí)會(huì)調(diào)用它的dispatchTouchEvent方法,如果這個(gè)ViewGroup的onInterceptTouchEvent返回true,就表示要攔截當(dāng)前事件,接著事件就會(huì)交給這個(gè)ViewGroup來(lái)處理,即它的onTouchEvent方法被調(diào)用;返回false,就表示當(dāng)前事件不需要被攔截,當(dāng)前時(shí)間就會(huì)被繼續(xù)傳遞給它的子元素,接著會(huì)調(diào)用子元素的dispatchTouchEvent方法,如此返回直到事件最終被處理。
為什么Activity向下分發(fā)第一個(gè)就是ViewGroup,如果我們布局中只有一個(gè)簡(jiǎn)單View控件(如TextView)呢?因?yàn)槲覀儾季旨虞d中的頂級(jí)View是DecorView(繼承FrameLayout),他本是就是一個(gè)ViewGroup。
3.源碼分析
上面我們分析了View的事件分發(fā)機(jī)制,下面我們從源碼的角度去學(xué)習(xí)分析。
3.1 Activity對(duì)點(diǎn)擊事件的分發(fā)過(guò)程
點(diǎn)擊事件使用MotionEvent來(lái)表示,當(dāng)點(diǎn)擊事件時(shí),事件最先傳遞給當(dāng)前的Activity,由Activity的dispatchTouchEvent來(lái)進(jìn)行事件派發(fā),具體工作是由Activity內(nèi)部的Window來(lái)完成的。Window會(huì)將事件傳遞給DecorView,DecorView一般就是當(dāng)前界面的底層容器(即setContentView 所設(shè)置的View的父容器),通過(guò)Activity.getWindow.getDecorView()可以獲得。
關(guān)于activity的構(gòu)成學(xué)習(xí)可以參考
劉望舒 Android View體系(六)從源碼解析Activity的構(gòu)成
下面看下Activity的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
// 一般事件列開(kāi)始都是DOWN事件 = 按下事件,故此處基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//當(dāng)此activity在棧頂時(shí),觸屏點(diǎn)擊按home,back,menu鍵等都會(huì)觸發(fā)此方法
onUserInteraction();
}
//注釋1
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//當(dāng)一個(gè)點(diǎn)擊事件未被Activity下任何一個(gè)View接收或處理
//或是發(fā)生在Window邊界外的觸摸事件就會(huì)調(diào)用
//注釋2
return onTouchEvent(ev);
}
分析上面的代碼,從注釋1處可以看出事件開(kāi)始交給Activity的Window進(jìn)行分發(fā)的。返回true,整個(gè)事件循環(huán)就結(jié)束了,返回false意味著沒(méi)人處理,即調(diào)用Activity的onTouchEvent方法。
從注釋1處點(diǎn)擊去就是Window#superDispatchTouchEvent(),這是一個(gè)抽象方法。我們得找到它的實(shí)現(xiàn)類(lèi)。
//Window#superDispatchTouchEvent()
public abstract boolean superDispatchTouchEvent(MotionEvent event);
而Window的實(shí)現(xiàn)類(lèi)也就是PhoneWindow,我們看下PhoneWindow的superDispatchTouchEvent方法,其源碼如下所示:
// com.android.internal.policy.PhoneWindow
//PhoneWindow#superDispatchTouchEvent()
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor是PhoneWindow中一個(gè)DecorView類(lèi)型的變量,DecorView代表了當(dāng)前Window最頂級(jí)的View,可以看做是根View。由上代碼看出,后面會(huì)執(zhí)行DecorView的superDispatchTouchEvent方法,其源碼如下所示:
//DecorView#superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
//DecorView繼承FrameLayout 那么他本是就是一個(gè)ViewGroup
//那么這個(gè)方法最后就會(huì)調(diào)用到ViewGroup#dispatchTouchEvent()
return super.dispatchTouchEvent(event);
}
可以看到最后Activity的分發(fā)過(guò)程最后就是將事件交給頂級(jí)DecorView(即ViewGroup)去進(jìn)行事件分發(fā)。然后它又會(huì)調(diào)用ViewGroup#dispatchTouchEvent()。到這里我們就將我們的事件由Activity->ViewGroup的傳遞。并將返回值設(shè)置成true。表示這個(gè)事件已經(jīng)被我們消耗掉了。從注釋2中我們可以看出只有Window沒(méi)處理觸摸事件的情況下,Activity才會(huì)調(diào)用onTouchEvent方法去處理事件:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
只有當(dāng)觸摸事件沒(méi)有被任何的View或ViewGroup處理過(guò)的時(shí)候,Activity才會(huì)執(zhí)行自己的onTouchEvent去處理觸摸事件。一種典型的情形就是,當(dāng)前觸摸點(diǎn)在Window范圍之外,這樣Window里面所有的View都不會(huì)接收更不會(huì)處理該觸摸事件,這時(shí)候我們可以重寫(xiě)該方法實(shí)現(xiàn)一些自己的邏輯處理這種情形。如果我們處理了,就返回true,否則返回false。其默認(rèn)一直返回false。
3.2 ViewGroup對(duì)點(diǎn)擊事件的發(fā)分過(guò)程
通過(guò)上面的分析,當(dāng)Activity接收到觸摸事件之后,會(huì)通過(guò)DectorView調(diào)用ViewGroup的dispatchTouchEvent方法。那么我們就分析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();
}
// Check for interception.
//檢查是否事件攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//調(diào)用事件攔截方法
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;
}
從上面我們可以看出,事件在開(kāi)始的時(shí)候會(huì)調(diào)用resetTouchState方法清空mFirstTouchTarget 。 ViewGroup在兩種情況下會(huì)判斷是否要攔截當(dāng)前事件:事件類(lèi)型為ACTION_DOWN或者mFirstTouchTarget != null。ACTION_DOWN是按下,mFirstTouchTarget 用于保存當(dāng)前ViewGroup中處理了觸摸事件的子View。
下面看下ViewGroup不攔截事件,將事件向下分發(fā),傳遞給子View進(jìn)行處理
//對(duì)子元素進(jìn)行遍歷
for(int i = childrenCount - 1;i >=0;i--){
//子view是否在做動(dòng)畫(huà)
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//事件坐標(biāo)是否在子元素的區(qū)域內(nèi)
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
//判斷是否接受點(diǎn)擊事件
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);
//如果有子元素,將進(jìn)行事件分發(fā)
//注釋1
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遍歷其子View,通過(guò)子元素是否在播放動(dòng)畫(huà)和點(diǎn)擊事件的坐標(biāo)是否在子元素的區(qū)域內(nèi),來(lái)判斷子元素是否能夠接收到點(diǎn)擊事件。對(duì)于符合點(diǎn)擊事件的子View并調(diào)用dispatchTransformedTouchEvent()進(jìn)行事件分發(fā)。當(dāng)子元素處理點(diǎn)點(diǎn)擊事件就會(huì)調(diào)用addTouchTarget方法對(duì)mFirstTouchTarget進(jìn)行賦值,alreadyDispatchedToNewTouchTarget 設(shè)置為true,表示只要有子View處理了觸摸事件,就表示當(dāng)前的ViewGroup也處理了觸摸事件,并且這種情況下ViewGroup不會(huì)調(diào)用從View中繼承來(lái)的dispatchTouchEvent方法,從而不會(huì)觸發(fā)ViewGroup的onTouchEvent方法的執(zhí)行。
下面看下如何對(duì)mFirstTouchTarget進(jìn)行賦值:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
我們可以看到mFirstTouchTarget的賦值是在addTouchTarget方法中進(jìn)行的,mFirstTouchTarget是否被賦值,將影響到ViewGroup對(duì)事件的攔截。
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//注釋2
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
如果mFirstTouchTarget ==null 即表示ViewGroup沒(méi)有子元素,或者是子元素處理了點(diǎn)擊事件,下一步將會(huì)把點(diǎn)擊事件交給View來(lái)處理。
我們從上面的注釋1和注釋2處可以看到,都調(diào)用了dispatchTransformedTouchEvent方法,而第三個(gè)參數(shù)中 注釋2傳遞的是一個(gè)null。下面看下源碼:
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;
}
......
}
可以可看到,如果ViewGroup有子元素同時(shí)子元素可以處理點(diǎn)擊事件。那么就會(huì)調(diào)用子元素的child.dispatchTouchEvent方法。如果是child是ViewGroup繼續(xù)上面的循環(huán),如果子元素是View,那么就會(huì)調(diào)用View.dispatchTouchEvent方法。關(guān)于這個(gè)方法我們后面分析。如果child為空,就會(huì)調(diào)用super.dispatchTouchEvent方法,那么就會(huì)調(diào)用ViewGroup的父類(lèi),即View.dispatchTouchEvent方法,ViewGroup自己處理點(diǎn)擊事件。最后都會(huì)默認(rèn)調(diào)用onTounchEvent方法。
3.3 View對(duì)點(diǎn)擊事件的處理過(guò)程
view對(duì)點(diǎn)擊事件處理過(guò)程稍微簡(jiǎn)單些。這里的view是不包含ViewGroup。下面看下dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//如果設(shè)置了OnTouchListener,那么會(huì)在此處執(zhí)行OnTouchListener的onTouch方法
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//觸摸事件沒(méi)有被OnTouchListener處理,那么就會(huì)執(zhí)行View的onTouchEvent方法
if (!result && onTouchEvent(event)) {
//如果onTouchEvent返回了true,就表示觸摸事件被View處理了,result就被設(shè)置為了true
result = true;
}
}
......
return result;
}
從上面可以看出,首先會(huì)判斷有沒(méi)有設(shè)置onTouchListener,如果onTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不會(huì)被執(zhí)行,由此可見(jiàn)onTouchListener的優(yōu)先級(jí)是高于onTouchEvent,這樣做的好處是,方便在外界處理點(diǎn)擊事件。
下面看下onTouchEvent方法,當(dāng)view 是不可用的時(shí)候點(diǎn)擊事件的處理過(guò)程,在不可用的狀態(tài)下,View照樣會(huì)消耗事件,只不過(guò)不會(huì)觸發(fā)onClick方法。
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//首先判斷當(dāng)前View是不是DISABLED不可用狀態(tài)
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有代理會(huì)執(zhí)行這個(gè)方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
當(dāng)然如果設(shè)置有代理,還會(huì)執(zhí)行TouchDelegate的onTouchEvent方法,此處onTouchEvent的工作機(jī)制和onTouchListener的類(lèi)似。
onTouchEvent對(duì)點(diǎn)擊事件的具體處理如下:
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:
......
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
......
break;
}
return true;
}
如果view是可以點(diǎn)擊的,就會(huì)進(jìn)入switch語(yǔ)句中,當(dāng)ACTION_UP事件觸發(fā),就會(huì)執(zhí)行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;
}
如果我們?cè)O(shè)置了點(diǎn)擊事件就出執(zhí)行OnClickListener的onClick方法。點(diǎn)擊信息封裝在ListenerInfo中,當(dāng)我們?yōu)関iew設(shè)置點(diǎn)擊事件的時(shí)候就會(huì)調(diào)用,可以看下setOnClickListener方法和setOnLongClickListener方法的源碼:
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;
}
當(dāng)我們?cè)O(shè)置點(diǎn)擊事件的時(shí)候,會(huì)通過(guò)setClickable和setLongClickable方法將view設(shè)置成可點(diǎn)擊的。
本文學(xué)習(xí)到此,如果文章哪里有錯(cuò)誤,歡迎指出,謝謝!