本篇文章將從源碼的角度解析事件分發機制的詳細內容。關于上篇文章的那些情況迥異的分發處理過程,是如何在源碼中實現的?本篇文章將逐一揭曉。
一、分發機制中三個方法的關系
上篇文章關于dispatchTouchEvent()方法,onInterceptTouchEvent()方法和onTouchEvent ()方法的流程進行了梳理。那么在源碼實現中,三者之間的關系具體是什么樣的?用一段偽代碼來介紹。
/ 點擊事件產生后,會直接調用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {
//代表是否消耗事件
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
//如果onInterceptTouchEvent()返回true則代表當前View攔截了點擊事件
//則該點擊事件則會交給當前View進行處理
//即調用onTouchEvent ()方法去處理點擊事件
consume = onTouchEvent (ev) ;
} else {
//如果onInterceptTouchEvent()返回false則代表當前View不攔截點擊事件
//則該點擊事件則會繼續傳遞給它的子元素
//子元素的dispatchTouchEvent()就會被調用,重復上述過程
//直到點擊事件被最終處理為止
consume = child.dispatchTouchEvent (ev) ;
}
return consume;
}
上述偽代碼清楚地描述了,事件分發從Activity->ViewGroup->View過程中,三大方法之間的調用關系。
二、Activity中的分發機制
上篇文章,我們介紹到,分發機制是從Activity開始的,當觸摸屏幕時,Activity先感受到,并調用dispatchTouchEvent()方法進行分發。下面就來具體了解這一方法的內容。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//第一次按下操作時,用戶希望能與設備進行交互,可通過實現該方法
onUserInteraction();
}
//獲取當前Activity的頂層窗口是PhoneWindow,執行其superDispatchTouchEvent()方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//當沒有任何view處理時,交由activity的onTouchEvent處理
return onTouchEvent(ev);
}
可以看到,當Activity向下分發事件,最終沒有任何控件進行處理后,將會交給Activity的onTouchEvent()方法處理。
繼續看superDispatchTouchEvent()
方法
public boolean superDispatchTouchEvent(KeyEvent event) {
return mDecor.superDispatcTouchEvent(event);
}
PhoneWindow的最頂View是DecorView,再交由DecorView處理。而DecorView的父類的父類是ViewGroup,接著調用 ViewGroup.dispatchTouchEvent()方法。
所以Activity中的分發機制簡述為:若不重寫該方法,則調用根ViewGroup的dispatchTouchEvent()方法,進行分發,如果事件沒有任何控件進行處理,則最后返回給Activity的onTouchEvent()方法進行處理。若重寫該方法,不論返回值是false/true,都不會向下進行事件分發,也就是事件停止分發,已經在Activity中消費了。
三、ViewGroup中的分發機制
從Acivity中向下分發,就到達了ViewGroup中的dispatchTouchEvent()方法,下面來具體看看這個方法。
該方法比較復雜,篇幅有限,就截取幾個重要的邏輯片段進行介紹,來解析整個分發流程。
// 發生ACTION_DOWN事件或者已經發生過ACTION_DOWN,并且將mFirstTouchTarget賦值,才進入此區域,主要功能是攔截器
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
//disallowIntercept:是否禁用事件攔截的功能(默認是false),即不禁用
//可以在子View通過調用requestDisallowInterceptTouchEvent方法對這個值進行修改,不讓該View攔截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//默認情況下會進入該方法
if (!disallowIntercept) {
//調用攔截方法
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 當沒有觸摸targets,且不是down事件時,開始持續攔截觸摸。
intercepted = true;
}
這一段的內容主要是為判斷是否攔截。如果當前事件的MotionEvent.ACTION_DOWN,則進入判斷,調用ViewGroup onInterceptTouchEvent()方法的值,判斷是否攔截。如果mFirstTouchTarget != null,即已經發生過MotionEvent.ACTION_DOWN,并且該事件已經有ViewGroup的子View進行處理了,那么也進入判斷,調用ViewGroup onInterceptTouchEvent()方法的值,判斷是否攔截。如果不是以上兩種情況,即已經是MOVE或UP事件了,并且之間的事件沒有對象進行處理,則設置成true,開始攔截接下來的所有事件。這也就解釋了如果子View的onTouchEvent()方法返回false,那么接下來的一些列事件都不會交給他處理。其實這并不是onTouchEvent()方法傲嬌,而是onInterceptTouchEvent()方法沒給他機會,直接攔截了,不給子View機會。如果VieGroup的onInterceptTouchEvent()第一次執行為true,則mFirstTouchTarget = null,則也會使得接下來不會調用onInterceptTouchEvent(),直接將攔截設置為true。
當ViewGroup不攔截事件的時候,事件會向下分發交由它的子View或ViewGroup進行處理。
/* 從最底層的父視圖開始遍歷,
** 找尋newTouchTarget,即上面的mFirstTouchTarget
** 如果已經存在找尋newTouchTarget,說明正在接收觸摸事件,則跳出循環。
*/
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// 如果當前視圖無法獲取用戶焦點,則跳過本次循環
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//如果view不可見,或者觸摸的坐標點不在view的范圍內,則跳過本次循環
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
// 已經開始接收觸摸事件,并退出整個循環。
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
//重置取消或抬起標志位
//如果觸摸位置在child的區域內,則把事件分發給子View或ViewGroup
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 獲取TouchDown的時間點
mLastTouchDownTime = ev.getDownTime();
// 獲取TouchDown的Index
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
//獲取TouchDown的x,y坐標
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//添加TouchTarget,則mFirstTouchTarget != null。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示以及分發給NewTouchTarget
alreadyDispatchedToNewTouchTarget = true;
break;
}
dispatchTransformedTouchEvent()
方法實際就是調用子元素的dispatchTouchEvent()
方法。
其中dispatchTransformedTouchEvent()
方法的重要邏輯如下:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
由于其中傳遞的child不為空,所以就會調用子元素的dispatchTouchEvent()。
如果子元素的dispatchTouchEvent()方法返回true,那么mFirstTouchTarget就會被賦值,同時跳出for循環。
//添加TouchTarget,則mFirstTouchTarget != null。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示以及分發給NewTouchTarget
alreadyDispatchedToNewTouchTarget = true;
其中在addTouchTarget(child, idBitsToAssign);
內部完成mFirstTouchTarget被賦值。
如果mFirstTouchTarget為空,將會讓ViewGroup默認攔截所有操作。
如果遍歷所有子View或ViewGroup,都沒有消費事件。ViewGroup會自己處理事件。
所以ViewGroup中的分發機制簡述為:若子View或ViewGroup不處理MotionEvent.ACTION_DOWN事件,那么接下來的一些列事件都交由ViewGroup處理。若ViewGroup的onInterceptTouchEvent()執行為true,則接下來的所有事件都默認由該ViewGroup執行。若子View或ViewGroup處理MotionEvent.ACTION_DOWN事件,則接下來的事件處理交給誰要看onInterceptTouchEvent()的返回值,如果返回true,則第一個MOVE事件,會變成CANCEL事件,繼續交由原來的子View或ViewGroup處理,接下來的一些列事件都交由ViewGroup執行。如果返回false,則繼續交給原來的子View或ViewGroup處理。
四、View中的分發機制
View中的分發機制就比較簡單了。上面ViewGroup中已經開始調用View.dispatchTouchEvent()方法,下面來具體看一下。
public boolean dispatchTouchEvent(MotionEvent event) {
...
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
//在Down事件之前,如果存在滾動操作則停止。不存在則不進行操作
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
//第一個條件默認為true
//第二個條件mOnTouchListener 不為空
//第三個條件該條件是判斷當前點擊的控件是否enable
//第四個條件onTouch返回值是true
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true; //滿足上述四個條件,已經消費事件,則返回True
}
//如果OnTouch()返回false,或者沒滿足其他條件,沒有消費Touch事件則調用OnTouchEvent()
if (!result && onTouchEvent(event)) {
result = true; //onTouchEvent(event)返回true,已經消費事件,則返回True
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// 處理取消或抬起操作
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
針對上述四個條件的判斷,很多View默認是(mViewFlags & ENABLED_MASK) == ENABLED
,通過設置OnTouchListener中的onTouch返回true,那么onTouchEvent()方法就不會調用,表明OnTouchListener的優先級高于onTouchEvent。
//手動調用設置
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
如果OnTouchListener中的onTouch返回false,那么會調用onTouchEvent()方法。
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
// 當View狀態為DISABLED,如果可點擊或可長按,則返回True,即消費事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//當View狀態為ENABLED,如果可點擊或可長按,則返回True,即消費事件;
//與前面的的結合,可得出結論:只要view是可點擊或可長按,則消費該事件.
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress) {
//這是Tap操作,移除長按回調方法
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//調用View.OnClickListener
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
//獲取是否處于可滾動的視圖內
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
//當處于可滾動視圖內,則延遲TAP_TIMEOUT,再反饋按壓狀態,用來判斷用戶是否想要滾動。默認延時為100ms
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
//當不再滾動視圖內,則立刻反饋按壓狀態
setPressed(true, x, y);
checkForLongClick(0); //檢測是否是長按
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
if (!pointInView(x, y, mTouchSlop)) {
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
只要View的CLICKABLE和LONG_CLICKABLE有一個為true,那么它就會消耗這個事件。
如果View設置了OnClickLisenter,那么performClick方法內部就會調用onClick方法。
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
則表明onTouch()優先級高于onTouchEvent(),并高于onClick()。
所以View中的分發機制簡述為:默認情況下,如果View的CLICKABLE和LONG_CLICKABLE有一個為true(默認LONG_CLICKABLE為false,一般可以點擊的View中CLICKABLE為true。),則就會消費該事件,如果都為false,則不會消費該事件,dispatchTouchEvent()方法返回false,交由父控件的循環下一個子View進行同樣操作。如果重寫onTouchEvent()方法,返回false,dispatchTouchEvent()方法返回false,交由父控件的循環下一個子View進行同樣操作。如果重寫onTouchEvent()方法,返回true,則消費該事件。
以上就是源碼解析事件分發的內容。
參考文章:
Android事件分發機制完全解析,帶你從源碼的角度徹底理解(上)
Android事件分發機制完全解析,帶你從源碼的角度徹底理解(下)
Android事件分發機制