前言
在如今這個(gè)拼顏值的社會(huì),app不光要運(yùn)行流暢,更要用優(yōu)美的界面來(lái)吸引用戶,有時(shí)候總感覺(jué)官方提供的控件遠(yuǎn)遠(yuǎn)不能滿足我們的需求,我們需要自己動(dòng)手去自定義一些view。
說(shuō)到自定義view,相信很多人都比較頭疼了,當(dāng)然我也不擅長(zhǎng)這個(gè)。最讓我頭疼的應(yīng)該就算是view的事件分發(fā)了(說(shuō)了這么多廢話,終于進(jìn)入正題了),廢話不多說(shuō),本文會(huì)對(duì)view的事件分發(fā)機(jī)制做一個(gè)詳細(xì)的說(shuō)明。
當(dāng)我們點(diǎn)擊了一個(gè)按鈕,系統(tǒng)內(nèi)部到底發(fā)生了什么
一個(gè)完整的點(diǎn)擊事件是有多個(gè)MotionEvent事件構(gòu)成的,當(dāng)手指按下屏幕,會(huì)伴隨著一個(gè)ACTION_DOWN事件;手指在屏幕上滑動(dòng),會(huì)伴隨一個(gè)或多個(gè)ACTION_MOVE事件;手指抬起則會(huì)產(chǎn)生一個(gè)ACTION_UP事件,從ACTION_DOWN到ACTION_UP成為一個(gè)事件序列。
Activity作為四大組件之一,我們與app的交互都要依賴與它,點(diǎn)擊事件自然是最先傳遞到Activity的dispatchTouchEvent中(talk is cheap,上代碼)
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity中具體的分發(fā)工作交給了Window,如果返回true,整個(gè)事件循環(huán)就結(jié)束了,返回false則代表沒(méi)人處理,所有的onTouchEvent都返回false,那么activity的onTouchEvent會(huì)被調(diào)用。
那么看一下Window中是如何分發(fā)事件的
public abstract boolean superDispatchTouchEvent(MotionEvent event);
Window中的分發(fā)事件的方法是一個(gè)抽象方法,我們就要找到哪里實(shí)現(xiàn)了這個(gè)方法。而這個(gè)方法的實(shí)現(xiàn)是在PhoneWindow中,PhoneWindow是Window的唯一實(shí)現(xiàn),看PhoneWindow中的代碼
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
代碼比較清晰,PhoneWindow將事件又傳遞給了DectorView,這里先看一下Activity,Window和Decorview之間的關(guān)系吧

每個(gè)Activity都對(duì)應(yīng)一個(gè)Window,這個(gè)Window窗口的唯一實(shí)例就是PhoneWindow,PhoneWindow對(duì)應(yīng)的布局則是DecorView,它繼承自FrameLayout,是一個(gè)ViewGroup,DecorView里面又分為兩部分,actionBar和contentView,而contentView就是我們?cè)贏ctivity中setContentView設(shè)置的布局。
通過(guò)這個(gè)圖,我們也了解了他們之間的關(guān)系,下面繼續(xù)說(shuō)事件的分發(fā)傳遞。
此時(shí)事件已經(jīng)傳遞給DecorView,它其實(shí)就是一個(gè)ViewGroup,
public class DecorView extends FrameLayout implements ... {
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}
從這里開(kāi)始,事件就傳遞到了頂級(jí)View,而我們最關(guān)注的點(diǎn)就是從頂級(jí)View開(kāi)始到各個(gè)子view之間的事件是如何分發(fā)的。一般頂級(jí)View都是ViewGroup,會(huì)調(diào)用ViewGroup的dispatchTouchEvent方法,這里只看一下關(guān)鍵代碼
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
//分兩種情況攔截事件,事件類型為ACTION_DOWN或者mFirstTouchTarget != null
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 {
intercepted = true;
}
當(dāng)事件為ACTION_DOWN時(shí),首先會(huì)重設(shè)一些狀態(tài),resetTouchState()方法會(huì)將mFirstTouchTarget置為空,F(xiàn)LAG_DISALLOW_INTERCEPT重置,
ViewGroup是否攔截事件分為兩種情況,當(dāng)事件為ACTION_DOWN或mFirstTouchTarget != null時(shí),這個(gè)mFirstTouchTarget是什么?mFirstTouchTarget是判斷是否有子view消費(fèi)了當(dāng)前事件,若消費(fèi)則會(huì)通過(guò)newTouchTarget = addTouchTarget(child, idBitsToAssign)對(duì)mFirstTouchTarget賦值,具體會(huì)在下面說(shuō)明
接下來(lái)看一下FLAG_DISALLOW_INTERCEPT這個(gè)參數(shù),FLAG_DISALLOW_INTERCEPT可以通過(guò)requestDisallowInterceptTouchEvent方法設(shè)置,一般用于子view,如果子view設(shè)置了FLAG_DISALLOW_INTERCEPT,ViewGroup將無(wú)法攔截除ACTION_DOWN以外的事件。
為什么是除了ACTION_DOWN以外的事件?上面說(shuō)了,ACTION_DOWN會(huì)重置FLAG_DISALLOW_INTERCEPT,導(dǎo)致子view設(shè)置的這個(gè)標(biāo)記位無(wú)效,ViewGroup總是調(diào)用onInterceptedTouchEvent詢問(wèn)是否攔截事件。

除去ACTION_DOWN事件, 若子view設(shè)置了FLAG_DISALLOW_INTERCEPT,intercept = false, 不攔截;否則調(diào)用onInterceptTouchEvent詢問(wèn)是否攔截事件, 默認(rèn)返回false,事件交由子view處理,接著看代碼,
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder);
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.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//子view是否可見(jiàn)或是否正在播放動(dòng)畫
if (!canViewReceivePointerEvents(child)
//點(diǎn)擊區(qū)域是否落在子view內(nèi)部
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//如果已經(jīng)有子view處理過(guò)事件序列中的一個(gè)事件,則newTouchTarget不為空,跳出當(dāng)前循環(huán)
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);
//調(diào)用child的diapatchTouchEvent方法
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,子view如果不可見(jiàn)并且沒(méi)有播放動(dòng)畫,并且點(diǎn)擊區(qū)域沒(méi)有落在子view區(qū)域內(nèi)部,繼續(xù)遍歷下一個(gè)子view;接下來(lái)這段代碼比較有意思,
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
分發(fā)事件時(shí)會(huì)先判斷newTouchTarget是否為空,那么這個(gè)newTouchTarget何時(shí)為空,何時(shí)不為空呢。還記得本文上面有提到addTouchTarget方法么,事實(shí)上mFirstTouchTarget也是在那個(gè)時(shí)候被賦值的,當(dāng)某個(gè)子view處理了ACTION_DOWN事件,mFirstTouchTarget被設(shè)置為當(dāng)前子view,當(dāng)這一個(gè)事件序列的其他事件分發(fā)時(shí),此時(shí)再去獲取newTouchTarget時(shí),newTouchTarget顯然不為空,直接跳出循環(huán),當(dāng)然首次處理時(shí),mFirstTouchTarget為空,newTouchTarget也為空。
接下來(lái)就要了分發(fā)過(guò)程中最重要的步驟了,放大招
通過(guò)將遍歷到的子view傳入dispatchTransformedTouchEvent方法中(此時(shí)child不為空),我們也看一下這個(gè)方法的核心源碼,
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
final boolean handled;
...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
方法中通過(guò)child是否為空分為兩種情況,當(dāng)child不為空時(shí),事件就交由子view去處理,這樣就完成了一輪事件的分發(fā),如果子view的dispatchTouchEvent返回true,終于在這里看到了addTouchTarget方法,是的,mFirstTouchTarget就是在這里賦值的,結(jié)束以后跳出循環(huán)。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果遍歷完以后mFirstTouchTarget仍然為空,說(shuō)明子view的dispatchTouchEvent返回了false或者ViewGroup內(nèi)沒(méi)有子元素,此時(shí)ViewGroup會(huì)自己處理這個(gè)點(diǎn)擊事件
if (mFirstTouchTarget == null) {
//傳入的child為null,說(shuō)明沒(méi)有子view去處理這個(gè)事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
child == null時(shí),handle = super.dispatchTouchEvent(event),到此,事件就轉(zhuǎn)到了View的dispatchTouchEvent方法中。
那么View是怎么分發(fā)事件的
既然到了View,還是要show一下源碼
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;
}
View的dispatchTouchEvent方法中就比ViewGroup的精簡(jiǎn)了很多,因?yàn)?strong>View是一個(gè)單獨(dú)的元素,它沒(méi)有子view的概念,省去了遍歷子view分發(fā)事件的步驟。從代碼可以看出View對(duì)事件的處理,首先判斷view有沒(méi)有設(shè)置OnTouchListener,如果設(shè)置了并且onTouch方法返回true,那么onTouchEvent方法就不會(huì)被調(diào)用了,若返回false則調(diào)用OnTouchEvent方法。接著我們看一下onTouchEvent方法
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//clickable和long_clickable只要有一個(gè)為true,就會(huì)消費(fèi)點(diǎn)擊事件
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
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.
// 此處說(shuō)明了只要clickable為true,不可用狀態(tài)也會(huì)消費(fèi)點(diǎn)擊事件
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
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)) {
//ACTION_UP事件觸發(fā)performClick()
performClickInternal();
}
}
}
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;
...
}
return true;
}
return false;
}
通過(guò)View的OnTouchEvent方法不難看出,當(dāng)一個(gè)View的clickable或longClickable為true時(shí),即使它處于不可用的狀態(tài),也依然會(huì)消費(fèi)點(diǎn)擊事件返回true;在ACTION_UP事件發(fā)生時(shí),會(huì)觸發(fā)performClick()方法,看一下這個(gè)方法的代碼
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;
}
...
return result;
}
如果View設(shè)置了OnClickLisener,就會(huì)調(diào)用它的onClick方法,到這里,View的事件分發(fā)就結(jié)束了,好像還差點(diǎn)什么。
結(jié)論
1.一個(gè)事件序列以一個(gè)ACTION_DOWN開(kāi)始,中間可能伴隨著多個(gè)ACTION_MOVE,最后以一個(gè)ACTION_UP結(jié)束
2.一個(gè)事件序列只能被一個(gè)view攔截并消費(fèi)掉,若一個(gè)View攔截了ACTION_DOWN,則必定會(huì)設(shè)置mFirstTouchTarget,其它事件會(huì)通過(guò)通過(guò)getTouchTarget.child獲取到子view并交由它處理
3.ViewGroup默認(rèn)不攔截任何事件
4.View沒(méi)有onInterceptedTouchEvent方法,View是單獨(dú)元素,一旦有點(diǎn)擊事件傳遞給它,就會(huì)調(diào)用它的onTouchEvent方法
5.View 的onTouchEvent方法默認(rèn)會(huì)消耗掉事件(返回true),除非它是不可點(diǎn)擊的
6.View的enable屬性不影響onTouchEvent的返回值,只要clickable和longclickable有一個(gè)為true,它的onTouchEvent就返回true
7.onClick發(fā)生的前提是當(dāng)前view是可點(diǎn)擊的,通過(guò)setOnClickListener或setOnLongClickListener會(huì)默認(rèn)將clickable或longclickable置為true
8.OnTouchListener, onTouchEvent, onClickListener之前的優(yōu)先級(jí) OnTouchListener > onTouchEvent > onClickListener