1.前言
事件分發這個東西嘛,大家一直都在講,但總有人覺得吃不透。為什么呢?因為事件分發是多維的,有好多條思維分岔路口,而文章基本上只能用一維的方式從左到右,從上到下進行表達,所以基本不可能讓普通智力的人從入門到精通。我們所要做的,就是踏踏實實打開源碼,自己多琢磨,多整理。才能徹底理解這些多維的知識點。
下面內容請配合源碼食用!不然基本上索然無味!
2.Touch與Click的前生今世
首先,我們先來做點前戲,搞清楚setOnTouchListener
、setOnClickListener
以及onTouchEvent
之間的關系。
2.1 setOnTouchListener
因為這一系列操作都是針對View的,所以我們直接看其源碼,精準定位到dispatchTouchEvent()
方法。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
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;
}
這段代碼非常簡單,直接將一切都暴露了出來。
result
變量十分關鍵,它是用來控制Touch與Click執行流程的。最開始result
為false,如果我們通過setOnTouchListener()
為某個View設置了touch監聽,并且在監聽的onTouch()
方法中返回true,那么result
變量就會被賦值為true,此時dispatchTouchEvent()
執行完畢,就不會執行接下來View本身的onTouchEvent()
方法。
2.2 onTouchEvent
相反,如果我們沒有為View設置touch監聽,或者設置了touch監聽但是在監聽的onTouch()
方法中返回false,那么result
依舊為false,就會執行View本身的onTouchEvent()
方法。我們來看看onTouchEvent()
做了什么,由于只是熱身運動,所以只貼出了與其有關的部分代碼。
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!post(mPerformClick)) {
performClick();
}
}
}
...
}
可以看到,在View本身的onTouchEvent()
方法中,先去判斷了該View是否可以被點擊,接著判斷觸摸事件的類型,如果是ACTION_UP
類型,則執行performClick()
方法。
2.3 setOnClickListener
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;
}
顯而易見,如果通過setOnClickListener
為當前View設置了Click監聽,此時就會去執行監聽中onClick()
方法。
2.4 小結
到此為止,前戲就算結束了。我們總結下,setOnTouchListener
與setOnClickListener
是程序員可以設置的,而onTouchEvent
是View本身的方法,在onTouchEvent
中會去執行setOnClickListener
中設置的OnClick
方法。而在View的dispatchTouchEvent()
中,首先會去判斷是否設置了OnTouchListener
并且其OnTouch
方法返回為true,如果是,則不會執行View本身的onTouchEvent
方法,如果不是,則會執行onTouchEvent
進而執行OnClick
方法。
我個人是這樣記住他們的關系的:Touch是觸摸,Click是點擊,從邏輯上來說,觸摸包含了點擊。所以如果設置了觸摸的監聽,那么其必定包含點擊,于是點擊的監聽也就沒什么必要了。
3.事件分發
3.1 事件分發的開始
下面進入正題,在使用安卓手機時,我們用手指觸摸了屏幕,物理設備就會一層層將觸摸事件傳遞出來,這是底層的活兒,我們暫且不去了解。屬于Android開發的故事,從Activity的dispatchTouchEvent()
方法開始。請注意,這是Activity的dispatchTouchEvent()
,不要和View的dispatchTouchEvent()
混淆起來。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
分析一波,首先判斷觸摸事件,如果是ACTION_DOWN
,則調用onUserInteraction()
,這是一個空方法,專門用來讓用戶重寫的,可以用于在事件發生前做一些操作。
接著getWindow().superDispatchTouchEvent(ev)
就比較重要了。一路跟來的同學肯定知道Activity中的Window就是PhoneWindw,不知道的傳送門在這里。我們直接看其superDispatchTouchEvent
方法。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
顯而易見,這里調用了DecorView中的superDispatchTouchEvent
方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView是PhoneWindow的內部類,我們去看看他的父類是誰。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
可見,FrameLayout 是DecorView的父類,所以就會調用FrameLayout的dispatchTouchEvent
方法,遺憾的是,FrameLayout并沒有這個方法,所以還要去找FrameLayout 的父類ViewGroup。ViewGroup中的dispatchTouchEvent
是本篇最大的高潮,我們下一節專門來講。在此,我們回到Activity的dispatchTouchEvent
方法中
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
看最后一行代碼——年輕的程序員啊,請記住,只要外層ViewGroup的dispatchTouchEvent
返回為true,那么就代表事件被消耗了,此時連Activity中的onTouchEvent
都不會被執行!
3.2 ViewGroup.dispatchTouchEvent
3.2.1 事件重置
我們從上往下,慢慢分析ViewGroup的dispatchTouchEvent
方法。
Android是支持殘障人士使用的,AccessibilityService能夠模擬觸摸事件,而現在我們通常用它來搶紅包,其實現原理就和下面這段代碼息息相關,先挖個坑。
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
下一行代碼對handled賦值為false,這里單獨拿出來就說明這個參數很重要,從名字可以看出這個值代表了事件是否被處理,后面還會多次遇到。
boolean handled = false;
接著判斷事件是否是安全的,如果OJBK,則通過事件掩碼獲取actionMasked,這里提一嘴,MotionEvent.ACTION_MASK
可以翻譯成事件掩碼,主要作用是在多點觸摸時分辨觸摸事件。
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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();
}
...
繼續分析,如果事件是ACTION_DOWN
,則調用cancelAndClearTouchTargets(ev)
和resetTouchState()
兩兄弟。先看前面一個方法。
/**
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
顧名思義,cancelAndClearTouchTargets
是用來重新初始化TouchTarget的,畢竟Down事件是一次用戶觸摸的開始,所以在開始之前都要把之前的TouchTarget都清除掉。那么TouchTarget又是個啥玩意兒呢?
/* Describes a touched view and the ids of the pointers that it has captured.
*
* This code assumes that pointer ids are always in the range 0..31 such that
* it can use a bitfield to track which pointer ids are present.
* As it happens, the lower layers of the input dispatch pipeline also use the
* same trick so the assumption should be safe here...
*/
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
...
// The next target in the target list.
public TouchTarget next;
從注釋上可以看出,TouchTarget形容了觸摸的點。它是一個單向鏈表,最大長度為32,也就是說,Android最多允許32個觸摸點同時進行操作,算一下,起碼2個人把手腳都放在同一個屏幕上才能(先不考慮能不能放得下)把設備弄成傻逼。
resetTouchState()
的作用是清除標志位,就不仔細看了。我們稍微總結一下這部分功能,ViewGroup的dispatchTouchEvent
會判斷觸摸事件類型,如果當前為DOWN事件,則會將所有狀態都初始化,開始新的一輪事件處理。
3.2.2 事件攔截
下面繼續分析dispatchTouchEvent
。結束了事件重置之后,這里定義了一個intercepted變量,顯而易見,這是用來做事件攔截的。
// 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;
}
在if判斷中,只要當前觸摸事件為DOWN或者存在TouchTarget就會繼續執行intercepted判斷。這里有一個與運算mGroupFlags & FLAG_DISALLOW_INTERCEPT
(兩位同時為“1”,結果才為“1”,否則為0)。來想想我們是如何請求父控件不要攔截觸摸事件的?沒錯,就是getParent().requestDisallowInterceptTouchEvent(true)
:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
當參數disallowIntercept為true時,會執行mGroupFlags |= FLAG_DISALLOW_INTERCEPT
運算(參加運算的兩個對象只要有一個為1,其值為1),由于是getParent
,所以此時的mGroupFlags
就是ViewGroup中的mGroupFlags
,下面的運算屬于計算機基礎,X|A&A=A,所以最終mGroupFlags
的結果就是FLAG_DISALLOW_INTERCEPT
。我們看源碼發現FLAG_DISALLOW_INTERCEPT
的值為0x80000不等于0,因此如果子View執行了getParent().requestDisallowInterceptTouchEvent(true)
這個方法,父View中的intercepted
參數就會被賦值為false。
知道了這樣一個流程后,我們再把思維分叉開來,回到之前的代碼中
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
默認情況下,disallowIntercept結果是false,此時就會執行onInterceptTouchEvent(ev)
并將其返回值賦值給intercepted,而onInterceptTouchEvent
一般是會由程序員來重寫的。
好了,現在你已經搞清楚intercepted這個標志位是怎么被賦值為true或者false的,不過這只是個標志位,并沒有執行什么攔截的操作,接下來我們回到ViewGroup的dispatchTouchEvent
方法中,去看看具體的攔截操作是怎么執行的。
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
...這里有很多很多代碼...
此時又獲取了canceled 標志位,顧名思義用來判斷事件是否被取消,這位兄弟一般都為true,并不是什么重點。重點在于if判斷,這里先討論intercepted
為true的情況,此時就不會執行if中的一大段代碼,直接跳到下面的流程中:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
由于之前的重置操作會將mFirstTouchTarget 設置為Null,所以此時會執行dispatchTransformedTouchEvent()
方法,這是事件分發中最重要的方法,請注意第三個參數為null:
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
我們截取了方法中最關鍵的部分,child就是調用方法時傳入的第三個參數,當child==null
時,會執行super.dispatchTouchEvent(transformedEvent)
并將返回結果賦值給handled,我們此時是在ViewGroup中,其父類是View,所以我們要去考察View的dispatchTouchEvent方法:
什么?
你居然還在等著看View的dispatchTouchEvent源碼?
文章的第二部分是白看的嗎?
前戲是白做的嗎?
快回去重新讀一遍!
請注意!雖然最后代碼跑到了View中,但這個View是ViewGroup的父類!也就是說最終執行的Touch或Click方法依然是外層ViewGroup中重寫的Touch或Click方法!請區分ViewGroup、View、父View與子View的區別~
我知道有人還是懵逼的,我們總結下。導致intercepted
為true的原因有兩個,一是父View重寫了onInterceptTouchEvent
方法并返回true,二是子View沒有請求getParent().requestDisallowInterceptTouchEvent(true)
方法。而當intercepted
為true時,父View就會執行攔截操作,在源碼中的表現就是dispatchTransformedTouchEvent()
的第三個參數為null,從而執行super.dispatchTouchEvent(transformedEvent)
方法,這個方法最終會調用在父View中重寫的Touch或Click方法。
別放松,還沒完呢。看View中的這段代碼:
if (!result && onTouchEvent(event)) {
result = true;
}
如果父View中onTouchEvent
返回false,那么result的結果就是false(結合文章第二段看更加清晰喲)。此時再回到Activity的dispatchTouchEvent
方法中:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
由于result為false,所以getWindow().superDispatchTouchEvent(ev)
也為false,此時就仍然會執行Activity的onTouchEvent(ev)
方法!因此,縱使父View攔截了事件,只要他的onTouchEvent
返回false,Activity中的onTouchEvent(ev)
方法依舊會得到執行!
OK,到此為止事件攔截就算講完了。道友們且好好消化,下面繼續發車!
3.2.3 事件分發
在前面事件攔截的分析中,我們假設intercepted
為true,所以就會跳過下面代碼中的if判斷
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
...這里有很多很多代碼...
而跳過的一大段代碼恰恰是實現事件分發的代碼。默認情況下,intercepted
都為false,因此if判斷中的代碼基本都會執行,現在讓我們一起來看看事件分發是如何實現的。
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
在事件分發的開始,又出現了Accessibility
相關的字段,這是android可以實現自動化測試的原因之一,這里先加深一波印象。
下面還是條件判斷,由于此時仍然是DOWN事件,自然而然就進去了。
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...省略幾行代碼...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
...省略下面代碼...
接著判斷當前控件是否含有子控件,如果包含子View,則通過buildTouchDispatchChildList()
對所有子View進行重排序,這個方法也是挺有意思的,它會回調buildOrderedChildList()
:
ArrayList<View> buildOrderedChildList() {
...省略...
for (int i = 0; i < childrenCount; i++) {
// add next child (in child order) to end of list
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View nextChild = mChildren[childIndex];
final float currentZ = nextChild.getZ();
// insert ahead of any Views with greater Z
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
為什么要重排序呢?因為View是一層層添加到Window上的,在事件分發的時候,如果某一觸摸點下面有多層子View,自然應該是最外層的子View先接收到事件。遺憾的是,在View的添加過程中,并不是先添加到父View中的子View就一定在最外層,因此我們就有必要通過每個子View的Z軸數值對他們進行重排序。
重排序之后,就按照排好的順序一個個拿到子View,并通過if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null))
這句代碼來判斷子View是否可以接收當前的觸摸事件。
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...省略Accessibility相關代碼...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
先看前一個方法,顯而易見接收觸摸事件有兩個條件,一是可見,二是不在執行動畫。
private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
接著看下一個方法,關在在于transformPointToViewLocal
會加上偏移值,而child.pointInView(point[0], point[1])
會判斷該child是否可以接收到(x,y)點的觸摸事件
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
綜合上述兩處,我們總結View能夠接收觸摸事件的條件一共有四個:
1.可見
2.不在執行動畫
3.可點擊
4.觸摸點在View內
在之前的分析中,如果遍歷到的子View不能接收事件,就直接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;
}
getTouchTarget()
也是重點方法:
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
當觸摸事件為DOWN時,mFirstTouchTarget會被重置為null,因此此時getTouchTarget
返回null,什么都沒有發生,代碼繼續向下執行。那么為什么又說這是重點方法呢,因為當觸摸事件為MOVE時,mFirstTouchTarget不為null,此時就會直接break出當前循環。我們知道MOVE是十分頻繁的調用,所以這里相當于是做了一層性能優化。具體是怎么優化的,我們在下個篇章還會介紹。
拓展完畢回到主線上,代碼再次進入條件判斷
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
dispatchTransformedTouchEvent()
在之前出現過,當父View攔截事件時,該方法被調用,第三個參數為null,并最終調用了父View的Touch或Click方法。而此時,第三個參數不再為null,取而代之的是可以接收觸摸事件的子View,我們重新來看dispatchTransformedTouchEvent()
中最關鍵的代碼:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
當child不為空時,先進行觸摸點的偏移計算,接著執行handled = child.dispatchTouchEvent(event)
。如果child是ViewGroup,就相當于重新執行上面的一大波步驟;如果child是View,則類似于父View攔截事件的過程,會執行View本身的Touch或Click方法。
3.2.4 事件回調
好了好了,事件分發下去的過程終于梳理完了,我們接著看分發結果的回調。
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;
}
如果子View的dispatchTouchEvent()
返回true,就會進入判斷體并執行最重要的一行代碼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;
}
之前說過TouchTarget表示觸摸目標,其本質是一個單向鏈表。顯而易見,addTouchTarget()
的作用就是為mFirstTouchTarget 賦值,初始化這個鏈表。
現在mFirstTouchTarget 不為Null了,我們來看最后一段源碼:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
...
}
}
當子View返回true時,子View就消耗了這個事件,執行else中的代碼,handled被賦值為true;而當子View不消耗事件返回false時,執行if中的代碼,dispatchTransformedTouchEvent
一共出現了3次,大家應該很熟悉了,當第三個參數為null時,會調用父View的Touch或Click方法,就這樣一層一層的回調上去,整個過程是一個很完美的遞歸。
3.3 MOVE事件
在前面的文章中,我們基本是以DOWN事件為例進行分析的,如果你熟練理解了上面所講的內容,那么請換上這輛快車,繼續來看看MOVE事件是怎樣的玩法。
3.3.1 MOVE事件攔截
MOVE事件也能夠被攔截的原因就在于這個if判斷。
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;
}
}
由于DOWN事件會為mFirstTouchTarget賦值,因此MOVE時mFirstTouchTarget!=null,該攔截的繼續攔截。
3.3.2 MOVE事件分發
現在回憶一下DOWN事件分發的那一大堆步驟,什么子View重排序啊、遍歷啊、判斷能否接收觸摸事件啊等等等等,其過程十分復雜,因為DOWN是點一下就完事了,所以可以這么整,而MOVE的調用非常頻繁,要也這樣操作,用戶界面絕對會被卡死。
所以我們才會用到TouchTarget,觸發Down事件后,TouchTarget被賦值,其目標就是可以接收觸摸事件的子View,因此在MOVE事件中,我們可以直接跳過前面的一大段代碼,直接從mFirstTouchTarget獲取需要的子View:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
這段代碼不久前出現過,只是我省略了else中的部分代碼,因為之前在說DOWN事件,與MOVE無關。現在我們來看完整的流程,當mFirstTouchTarget不為Null時,最關鍵的是這段:
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
MOVE事件就是在這里進行分發與回調的!
4.總結
如果讀到最后,你有一種什么都聯系起來了,豁然開朗的感覺,那就點個贊唄!如果讀完心想這文章寫的什么[嗶]東西,請務必擺上一份源碼再讀一次!
而如果你真的什么都懂,底下留言!我賠錢!