Android Touch事件分發(fā)深入了解
本文帶著大家深入學(xué)習(xí)觸摸事件的分發(fā),具體內(nèi)容如下
1. 觸摸動作及事件序列
(1)觸摸事件的動作
觸摸動作一共有三種:ACTION_DOWN、ACTION_MOVE、ACTION_UP。當(dāng)用戶手指接觸屏幕時,便產(chǎn)生一個動作為ACTION_DOWN的觸摸事件,此時若用戶的手指立即離開屏幕,會產(chǎn)生一個動作為ACTION_UP的觸摸事件;若用戶手指接觸屏幕后繼續(xù)滑動,當(dāng)滑動距離超過了系統(tǒng)中預(yù)定義的距離常數(shù),則產(chǎn)生一個動作為ACTION_MOVE的觸摸事件,系統(tǒng)中預(yù)定義的用來判斷用戶手指在屏幕上的滑動是否是一個ACTION_MOVE動作的這個距離常量叫做TouchSlop,可通過ViewConfiguration.get(getContext()).getScaledTouchSlop()獲取。
(2)事件序列
當(dāng)用戶的手指接觸屏幕,在屏幕上滑動,又離開屏幕,這個過程會產(chǎn)生一系列觸摸事件:ACTION_DOWN-->若干個ACTION_MOVE-->ACTION_UP。這一系列觸摸事件即為一個事件序列。
2. 觸摸事件的分發(fā)
(1)概述
當(dāng)產(chǎn)生了一個觸摸時間后,系統(tǒng)要負(fù)責(zé)把這個觸摸事件給一個View(TargetView)來處理,touch事件傳遞到TargetView的過程即為touch事件的分發(fā)。
觸摸事件的分發(fā)順序:Activity-->頂級View-->頂級View的子View-->. . .-->Target View
觸摸事件的響應(yīng)順序:TargetView --> TargetView的父容器 --> . . . -->頂級View -->Activity
(2)toush事件分發(fā)的具體過程
a. Activity對touch事件的分發(fā)
當(dāng)用戶手指接觸屏幕時,便產(chǎn)生了一個touch事件,封裝了touch事件的MotionEvent最先被傳遞給當(dāng)前Activity,Activity的dispatchTouchEvent方法負(fù)責(zé)touch事件的分發(fā)。分發(fā)touch事件的實際工作由當(dāng)前Activity的Window完成,而Window會將touch事件傳遞給DecorView(當(dāng)前用戶界面頂級View)。Activity的dispatchTouchEvent方法代碼如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
根據(jù)以上代碼可以知道,touch事件會交由Window的superDispatchTouchEvent進(jìn)行分發(fā),若這個方法返回true,意味touch事件的分發(fā)過程結(jié)束,返回false則說明經(jīng)過層層分發(fā),沒有子View對這個事件進(jìn)行處理,即所有子View的onTouchEvent方法都返回false(即這個touch事件沒有被“消耗”)。這時會調(diào)用Activity的onTouchEvent方法來處理這個touch事件。
在Window的superDispatchTouchEvent方法中,首先會把touch事件分發(fā)給DecorView,因為它是當(dāng)前用戶界面的頂級View。Window的superDispatchTouchEvent方法如下:
public abstract boolean superDispatchTouchEvent(MotionEvent ev);
是個抽象方法,這個方法由Window的實現(xiàn)類PhoneWindow實現(xiàn),PhoneWindow的superDispatchTouchEvent方法的代碼如下:
public boolean superDispatchTouchEvent(MotionEvent ev) {
return mDecor.superDispatchTouchEvent(event);
}
由以上代碼可得,PhoneWindow的superDispatchTouchEvent方法實際上是通過DecorView的superDispatchTouchEvent方法來完成自己的工作,也就是說,當(dāng)前Activity的Window直接將這個touch事件傳遞給了DecorView。也就是說,目前touch事件已經(jīng)經(jīng)過了如下的分發(fā):Activity-->Window-->DecorView。
b. 頂級View對touch事件的分發(fā)
經(jīng)過Activity與Window的分發(fā),現(xiàn)在touch事件已經(jīng)被傳遞到了DecorView的dispatchTouchEvent方法中。DecorView本質(zhì)上是一個ViewGroup(更具體的說是FrameLayout),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();
}
第一階段的主要工作有倆:一是在第6行的resetTouchState方法中完成了對FLAG_DISALLOW_INTERCEPT標(biāo)記的重置;二是第5行的cancelAndClearTouchTargets方法會清除當(dāng)前MotionEvent的touch target。關(guān)于FLAG_DISALLOW_INTERCEPT標(biāo)記和touch target,在下文會有相關(guān)說明。
第二階段的主要工作是決定當(dāng)前ViewGroup是否攔截本次的touch事件,主要代碼如下:
//Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWM || 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.
intercept =true;
}
由以上代碼我們可以知道,當(dāng)一個touch事件被傳遞到ViewGroup時,會先判斷這個touch事件的動作是否是ACTION_DOWN,如果這個事件是ACTION_DOWN或者mFirstTouchTarget不為null,就會根據(jù)FLAG_DISALLOW_INTERCEPT標(biāo)記決定是否攔截這個touch事件。那么mFirstTouchTarget是什么呢?當(dāng)touch事件被ViewGroup的子View成功處理時,mFirstTouchTarget就會被賦值為成功處理touch事件的View,也就是上面提高的touch target。
總結(jié)一下上述代碼的流程:在子View不干預(yù)ViewGroup的攔截的情況下(上述代碼中的disallowIntercept為false),若當(dāng)前事件為ACTION_DOWN或者mFirstTouchTarget不為空,則會調(diào)用ViewGroup的onInterceptTouchEvent方法來決定最終是否攔截此事件;否則(沒有TargetView并且此事件不是ACTION_DOWN),當(dāng)前ViewGroup就攔截下此事件。 一旦ViewGroup攔截了某次touch事件,那么mFirstTouchTarget就不會被賦值,因此當(dāng)再有ACTION_MOVE或是ACTION_UP傳遞到該ViewGroup時,mTouchTarget就為null,所以上述代碼第3行的條件就為false,ViewGroup會攔截下來。由此可得到的結(jié)論是:一旦ViewGroup攔截了某次事件,則同一事件序列中的剩余事件也會它默認(rèn)被攔截而不會再詢問是否攔截(即不會再調(diào)用onInterceptTouchEvent)。
這里存在一種特殊情形,就是子View通過requestDisallowInterceptTouchEvent方法設(shè)置父容器的FLAG_DISALLOW_INTERCEPT為true,這個標(biāo)記指示是否不允許父容器攔截,為true表示不允許。這樣做能夠禁止父容器攔截除ACTION_DOWN以外的所有touch事件。之所以不能夠攔截ACTION_DOWN事件,是因為每當(dāng)ACTION_DOWN事件到來時,都會重置FLAG_DISALLOW_INTERCEPT這個標(biāo)記位為默認(rèn)值(false),所以每當(dāng)開始一個新touch事件序列(即到來一個ACTION_DOWN動作),都會通過調(diào)用onInterceptTouchEven詢問ViewGroup是否攔截此事件。當(dāng)ACTION_DOWN事件到來時,重置標(biāo)記位的工作是在上面的第一階段完成的。
接下來,會進(jìn)入第三階段的工作:
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 不是ACTION_CANCEL并且不攔截
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 若當(dāng)前事件為ACTION_DOWN則去尋找這次事件新出現(xiàn)的touch target
final int actionIndex = ev.getActionIndex(); // always 0 for down
...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 根據(jù)觸摸的坐標(biāo)尋找能夠接收這個事件的touch target
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final View[] children = mChildren;
// 遍歷所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = i;
final View child = children[childIndex];
// 尋找可接收這個事件并且touch事件坐標(biāo)在其區(qū)域內(nèi)的子View
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child); // 找到了符合條件的子View,賦值給newTouchTarget
if (newTouchTarget != null) {
//Child is already receiving touch within its bounds.
//Give it the new pointer in addition to ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 把ACTION_DOWN事件傳遞給子組件進(jìn)行處理
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();
//把mFirstTouchTarget賦值為newTouchTarget,此子View成為新的touch事件的起點
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
當(dāng)ViewGroup不攔截本次事件,則touch事件會分發(fā)給它的子View進(jìn)行處理,相關(guān)代碼從第21行開始:遍歷所有ViewGroup的子View,尋找能夠處理此touch事件的子View,若一個子View不在播放動畫并且touch事件坐標(biāo)位于其區(qū)域內(nèi),則該子View能夠處理此touch事件,并且會把該子View賦值給newTouchTarget。
若當(dāng)前遍歷到的子View能夠處理此touch事件,就會進(jìn)入第38行的dispatchTransformedTouchEvent方法,該方法實際上調(diào)用了子View的dispatchTouchEvent方法。dispatchTransformedTouchEvent方法中相關(guān)的代碼如下:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
若dispatchTransformedTouchEvent方法傳入的child參數(shù)不為null,則會調(diào)用child(即處理touch事件的子View)的dispatchTouchEvent方法。若該子View的dispatchTouchEvent方法返回true,則dispatchTransformedTouchEvent方法也會返回true,則表示成功找到了一個處理該事件的touch target,會在第55行把newTouchTarget賦值給mFirstTouchTarget(這一賦值過程是在addTouchTarget方法內(nèi)部完成的),并跳出對子View遍歷的循環(huán)。若子View的dispatchTouchEvent方法返回false,ViewGroup就會把事件分發(fā)給下一個子View。
若遍歷了所有子View后,touch事件都沒被處理(該ViewGroup沒有子View或是所有子View的dispatchTouchEvent返回false),ViewGroup會自己處理touch事件,相關(guān)代碼如下:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
由以上代碼可知,ViewGroup自己處理touch事件時,會調(diào)用dispatchTransformedTouchEvent方法,傳入的child參數(shù)為null。根據(jù)上文的分析,傳入的chid為null時,會調(diào)用super.dispatchTouchEvent方法,即調(diào)用View類的dispatchTouchEvent方法。
c. View對touch事件的處理
View的dispatchTouchEvent方法的主要代碼如下:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
. . .
if (onFilterTouchEventForSecurity(event)) {
//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對touch事件的處理過程如下:由于View不包含子元素,所以它只能自己處理事件。它首先會判斷是否設(shè)置了OnTouchListener,若設(shè)置了,會調(diào)用onTouch方法,若onTouch方法返回true(表示該touch事件已經(jīng)被消耗),則不會再調(diào)用onTouchEvent方法;若onTouch方法返回false或沒有設(shè)置OnTouchListener,則會調(diào)用onTouchEvent方法,onTouchEvent對touch事件進(jìn)行具體處理的相關(guān)代碼如下:
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) {
. . .
if (!mHasPerformedLongPress) {
//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) {
mPerformClck = new PeformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
. . .
}
break;
}
. . .
return true;
}
由以上代碼可知,只要View的CLICKABLE屬性和LONG_CLICKABLE屬性有一個為true(View的CLICKABLE屬性和具體View有關(guān),LONG_CLICKABLE屬性默認(rèn)為false,setOnClikListener和setOnLongClickListener會分別自動將以上倆屬性設(shè)為true),那么這個View就會消耗這個touch事件,即使這個View處于DISABLED狀態(tài)。若當(dāng)前事件是ACTION_UP,還會調(diào)用performClick方法,該View若設(shè)置了OnClickListener,則performClick方法會在其內(nèi)部調(diào)用onClick方法。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);
return result;
}
以上是我學(xué)習(xí)Android中觸摸事件分發(fā)后的簡單總結(jié),很多地方敘述的還不夠清晰準(zhǔn)確
- Android事件分發(fā)機(jī)制的詳解
- Android View事件分發(fā)和消費源碼簡單理解
- Android View的事件分發(fā)機(jī)制
- 談?wù)剬ndroid View事件分發(fā)機(jī)制的理解
- Android事件分發(fā)機(jī)制(下) View的事件處理
- Android事件分發(fā)機(jī)制(上) ViewGroup的事件分發(fā)
- Android View事件分發(fā)機(jī)制詳解
- Android View 事件分發(fā)機(jī)制詳解
- Android 事件分發(fā)詳解及示例代碼
- android事件分發(fā)機(jī)制的實現(xiàn)原理
相關(guān)文章
Android Jetpack系列之App Startup使用詳解
這篇文章主要為大家介紹了Android Jetpack系列之App Startup使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Android進(jìn)程通信之Messenger和AIDL使用詳解
本篇文章主要介紹了Android進(jìn)程通信之Messenger和AIDL使用詳解,具有一定的參考價值,有興趣的可以了解一下。2017-01-01
Android 使用AsyncTask實現(xiàn)多任務(wù)多線程斷點續(xù)傳下載
這篇文章主要介紹了Android 使用AsyncTask實現(xiàn)多任務(wù)多線程斷點續(xù)傳下載的相關(guān)資料,需要的朋友可以參考下2018-05-05
Android使用socket創(chuàng)建簡單TCP連接的方法
這篇文章主要介紹了Android使用socket創(chuàng)建簡單TCP連接的方法,結(jié)合實例形式詳細(xì)分析了Android使用socket創(chuàng)建TCP連接的具體步驟與實現(xiàn)技巧,需要的朋友可以參考下2016-04-04

