1.前言
事件分發(fā)這個(gè)東西嘛,大家一直都在講,但總有人覺得吃不透。為什么呢?因?yàn)槭录职l(fā)是多維的,有好多條思維分岔路口,而文章基本上只能用一維的方式從左到右,從上到下進(jìn)行表達(dá),所以基本不可能讓普通智力的人從入門到精通。我們所要做的,就是踏踏實(shí)實(shí)打開源碼,自己多琢磨,多整理。才能徹底理解這些多維的知識(shí)點(diǎn)。
下面內(nèi)容請(qǐng)配合源碼食用!不然基本上索然無(wú)味!
2.Touch與Click的前生今世
首先,我們先來(lái)做點(diǎn)前戲,搞清楚setOnTouchListener、setOnClickListener以及onTouchEvent之間的關(guān)系。
2.1 setOnTouchListener
因?yàn)檫@一系列操作都是針對(duì)View的,所以我們直接看其源碼,精準(zhǔn)定位到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;
}
這段代碼非常簡(jiǎn)單,直接將一切都暴露了出來(lái)。
result變量十分關(guān)鍵,它是用來(lái)控制Touch與Click執(zhí)行流程的。最開始result為false,如果我們通過(guò)setOnTouchListener()為某個(gè)View設(shè)置了touch監(jiān)聽,并且在監(jiān)聽的onTouch()方法中返回true,那么result變量就會(huì)被賦值為true,此時(shí)dispatchTouchEvent()執(zhí)行完畢,就不會(huì)執(zhí)行接下來(lái)View本身的onTouchEvent()方法。
2.2 onTouchEvent
相反,如果我們沒有為View設(shè)置touch監(jiān)聽,或者設(shè)置了touch監(jiān)聽但是在監(jiān)聽的onTouch()方法中返回false,那么result依舊為false,就會(huì)執(zhí)行View本身的onTouchEvent()方法。我們來(lái)看看onTouchEvent()做了什么,由于只是熱身運(yùn)動(dòng),所以只貼出了與其有關(guān)的部分代碼。
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是否可以被點(diǎn)擊,接著判斷觸摸事件的類型,如果是ACTION_UP類型,則執(zhí)行performClick()方法。
2.3 setOnClickListener
performClick()這個(gè)方法比較短,直接展示出來(lái)。
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;
}
顯而易見,如果通過(guò)setOnClickListener為當(dāng)前View設(shè)置了Click監(jiān)聽,此時(shí)就會(huì)去執(zhí)行監(jiān)聽中onClick()方法。
2.4 小結(jié)
到此為止,前戲就算結(jié)束了。我們總結(jié)下,setOnTouchListener與setOnClickListener是程序員可以設(shè)置的,而onTouchEvent是View本身的方法,在onTouchEvent中會(huì)去執(zhí)行setOnClickListener中設(shè)置的OnClick方法。而在View的dispatchTouchEvent()中,首先會(huì)去判斷是否設(shè)置了OnTouchListener并且其OnTouch方法返回為true,如果是,則不會(huì)執(zhí)行View本身的onTouchEvent方法,如果不是,則會(huì)執(zhí)行onTouchEvent進(jìn)而執(zhí)行OnClick方法。
我個(gè)人是這樣記住他們的關(guān)系的:Touch是觸摸,Click是點(diǎn)擊,從邏輯上來(lái)說(shuō),觸摸包含了點(diǎn)擊。所以如果設(shè)置了觸摸的監(jiān)聽,那么其必定包含點(diǎn)擊,于是點(diǎn)擊的監(jiān)聽也就沒什么必要了。
3.事件分發(fā)
3.1 事件分發(fā)的開始
下面進(jìn)入正題,在使用安卓手機(jī)時(shí),我們用手指觸摸了屏幕,物理設(shè)備就會(huì)一層層將觸摸事件傳遞出來(lái),這是底層的活兒,我們暫且不去了解。屬于Android開發(fā)的故事,從Activity的dispatchTouchEvent()方法開始。請(qǐng)注意,這是Activity的dispatchTouchEvent(),不要和View的dispatchTouchEvent()混淆起來(lái)。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
分析一波,首先判斷觸摸事件,如果是ACTION_DOWN,則調(diào)用onUserInteraction(),這是一個(gè)空方法,專門用來(lái)讓用戶重寫的,可以用于在事件發(fā)生前做一些操作。
接著getWindow().superDispatchTouchEvent(ev)就比較重要了。一路跟來(lái)的同學(xué)肯定知道Activity中的Window就是PhoneWindw,不知道的傳送門在這里。我們直接看其superDispatchTouchEvent方法。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
顯而易見,這里調(diào)用了DecorView中的superDispatchTouchEvent方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView是PhoneWindow的內(nèi)部類,我們?nèi)タ纯此母割愂钦l(shuí)。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
可見,F(xiàn)rameLayout 是DecorView的父類,所以就會(huì)調(diào)用FrameLayout的dispatchTouchEvent方法,遺憾的是,F(xiàn)rameLayout并沒有這個(gè)方法,所以還要去找FrameLayout 的父類ViewGroup。ViewGroup中的dispatchTouchEvent是本篇最大的高潮,我們下一節(jié)專門來(lái)講。在此,我們回到Activity的dispatchTouchEvent方法中
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
看最后一行代碼——年輕的程序員啊,請(qǐng)記住,只要外層ViewGroup的dispatchTouchEvent返回為true,那么就代表事件被消耗了,此時(shí)連Activity中的onTouchEvent都不會(huì)被執(zhí)行!
3.2 ViewGroup.dispatchTouchEvent
3.2.1 事件重置
我們從上往下,慢慢分析ViewGroup的dispatchTouchEvent方法。
Android是支持殘障人士使用的,AccessibilityService能夠模擬觸摸事件,而現(xiàn)在我們通常用它來(lái)?yè)尲t包,其實(shí)現(xiàn)原理就和下面這段代碼息息相關(guān),先挖個(gè)坑。
// 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);
}
下一行代碼對(duì)handled賦值為false,這里單獨(dú)拿出來(lái)就說(shuō)明這個(gè)參數(shù)很重要,從名字可以看出這個(gè)值代表了事件是否被處理,后面還會(huì)多次遇到。
boolean handled = false;
接著判斷事件是否是安全的,如果OJBK,則通過(guò)事件掩碼獲取actionMasked,這里提一嘴,MotionEvent.ACTION_MASK可以翻譯成事件掩碼,主要作用是在多點(diǎn)觸摸時(shí)分辨觸摸事件。
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();
}
...
繼續(xù)分析,如果事件是ACTION_DOWN,則調(diào)用cancelAndClearTouchTargets(ev)和resetTouchState()兩兄弟。先看前面一個(gè)方法。
/**
* 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是用來(lái)重新初始化TouchTarget的,畢竟Down事件是一次用戶觸摸的開始,所以在開始之前都要把之前的TouchTarget都清除掉。那么TouchTarget又是個(gè)啥玩意兒呢?
/* 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形容了觸摸的點(diǎn)。它是一個(gè)單向鏈表,最大長(zhǎng)度為32,也就是說(shuō),Android最多允許32個(gè)觸摸點(diǎn)同時(shí)進(jìn)行操作,算一下,起碼2個(gè)人把手腳都放在同一個(gè)屏幕上才能(先不考慮能不能放得下)把設(shè)備弄成傻逼。
resetTouchState()的作用是清除標(biāo)志位,就不仔細(xì)看了。我們稍微總結(jié)一下這部分功能,ViewGroup的dispatchTouchEvent會(huì)判斷觸摸事件類型,如果當(dāng)前為DOWN事件,則會(huì)將所有狀態(tài)都初始化,開始新的一輪事件處理。
3.2.2 事件攔截
下面繼續(xù)分析dispatchTouchEvent。結(jié)束了事件重置之后,這里定義了一個(gè)intercepted變量,顯而易見,這是用來(lái)做事件攔截的。
// 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判斷中,只要當(dāng)前觸摸事件為DOWN或者存在TouchTarget就會(huì)繼續(xù)執(zhí)行intercepted判斷。這里有一個(gè)與運(yùn)算mGroupFlags & FLAG_DISALLOW_INTERCEPT(兩位同時(shí)為“1”,結(jié)果才為“1”,否則為0)。來(lái)想想我們是如何請(qǐng)求父控件不要攔截觸摸事件的?沒錯(cuò),就是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);
}
}
當(dāng)參數(shù)disallowIntercept為true時(shí),會(huì)執(zhí)行mGroupFlags |= FLAG_DISALLOW_INTERCEPT運(yùn)算(參加運(yùn)算的兩個(gè)對(duì)象只要有一個(gè)為1,其值為1),由于是getParent,所以此時(shí)的mGroupFlags 就是ViewGroup中的mGroupFlags ,下面的運(yùn)算屬于計(jì)算機(jī)基礎(chǔ),X|A&A=A,所以最終mGroupFlags的結(jié)果就是FLAG_DISALLOW_INTERCEPT。我們看源碼發(fā)現(xiàn)FLAG_DISALLOW_INTERCEPT的值為0x80000不等于0,因此如果子View執(zhí)行了getParent().requestDisallowInterceptTouchEvent(true)這個(gè)方法,父View中的intercepted參數(shù)就會(huì)被賦值為false。
知道了這樣一個(gè)流程后,我們?cè)侔阉季S分叉開來(lái),回到之前的代碼中
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
默認(rèn)情況下,disallowIntercept結(jié)果是false,此時(shí)就會(huì)執(zhí)行onInterceptTouchEvent(ev)并將其返回值賦值給intercepted,而onInterceptTouchEvent一般是會(huì)由程序員來(lái)重寫的。
好了,現(xiàn)在你已經(jīng)搞清楚intercepted這個(gè)標(biāo)志位是怎么被賦值為true或者false的,不過(guò)這只是個(gè)標(biāo)志位,并沒有執(zhí)行什么攔截的操作,接下來(lái)我們回到ViewGroup的dispatchTouchEvent方法中,去看看具體的攔截操作是怎么執(zhí)行的。
// 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) {
...這里有很多很多代碼...
此時(shí)又獲取了canceled 標(biāo)志位,顧名思義用來(lái)判斷事件是否被取消,這位兄弟一般都為true,并不是什么重點(diǎn)。重點(diǎn)在于if判斷,這里先討論intercepted為true的情況,此時(shí)就不會(huì)執(zhí)行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);
}
由于之前的重置操作會(huì)將mFirstTouchTarget 設(shè)置為Null,所以此時(shí)會(huì)執(zhí)行dispatchTransformedTouchEvent()方法,這是事件分發(fā)中最重要的方法,請(qǐng)注意第三個(gè)參數(shù)為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);
}
我們截取了方法中最關(guān)鍵的部分,child就是調(diào)用方法時(shí)傳入的第三個(gè)參數(shù),當(dāng)child==null時(shí),會(huì)執(zhí)行super.dispatchTouchEvent(transformedEvent)并將返回結(jié)果賦值給handled,我們此時(shí)是在ViewGroup中,其父類是View,所以我們要去考察View的dispatchTouchEvent方法:
什么?
你居然還在等著看View的dispatchTouchEvent源碼?
文章的第二部分是白看的嗎?
前戲是白做的嗎?
快回去重新讀一遍!
請(qǐng)注意!雖然最后代碼跑到了View中,但這個(gè)View是ViewGroup的父類!也就是說(shuō)最終執(zhí)行的Touch或Click方法依然是外層ViewGroup中重寫的Touch或Click方法!請(qǐng)區(qū)分ViewGroup、View、父View與子View的區(qū)別~
我知道有人還是懵逼的,我們總結(jié)下。導(dǎo)致intercepted為true的原因有兩個(gè),一是父View重寫了onInterceptTouchEvent方法并返回true,二是子View沒有請(qǐng)求getParent().requestDisallowInterceptTouchEvent(true)方法。而當(dāng)intercepted為true時(shí),父View就會(huì)執(zhí)行攔截操作,在源碼中的表現(xiàn)就是dispatchTransformedTouchEvent()的第三個(gè)參數(shù)為null,從而執(zhí)行super.dispatchTouchEvent(transformedEvent)方法,這個(gè)方法最終會(huì)調(diào)用在父View中重寫的Touch或Click方法。
別放松,還沒完呢??碫iew中的這段代碼:
if (!result && onTouchEvent(event)) {
result = true;
}
如果父View中onTouchEvent返回false,那么result的結(jié)果就是false(結(jié)合文章第二段看更加清晰喲)。此時(shí)再回到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,此時(shí)就仍然會(huì)執(zhí)行Activity的onTouchEvent(ev)方法!因此,縱使父View攔截了事件,只要他的onTouchEvent返回false,Activity中的onTouchEvent(ev)方法依舊會(huì)得到執(zhí)行!
OK,到此為止事件攔截就算講完了。道友們且好好消化,下面繼續(xù)發(fā)車!
3.2.3 事件分發(fā)
在前面事件攔截的分析中,我們假設(shè)intercepted為true,所以就會(huì)跳過(guò)下面代碼中的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) {
...這里有很多很多代碼...
而跳過(guò)的一大段代碼恰恰是實(shí)現(xiàn)事件分發(fā)的代碼。默認(rèn)情況下,intercepted都為false,因此if判斷中的代碼基本都會(huì)執(zhí)行,現(xiàn)在讓我們一起來(lái)看看事件分發(fā)是如何實(shí)現(xiàn)的。
// 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;
在事件分發(fā)的開始,又出現(xiàn)了Accessibility相關(guān)的字段,這是android可以實(shí)現(xiàn)自動(dòng)化測(cè)試的原因之一,這里先加深一波印象。
下面還是條件判斷,由于此時(shí)仍然是DOWN事件,自然而然就進(jìn)去了。
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();
...省略下面代碼...
接著判斷當(dāng)前控件是否含有子控件,如果包含子View,則通過(guò)buildTouchDispatchChildList()對(duì)所有子View進(jìn)行重排序,這個(gè)方法也是挺有意思的,它會(huì)回調(diào)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;
}
為什么要重排序呢?因?yàn)閂iew是一層層添加到Window上的,在事件分發(fā)的時(shí)候,如果某一觸摸點(diǎn)下面有多層子View,自然應(yīng)該是最外層的子View先接收到事件。遺憾的是,在View的添加過(guò)程中,并不是先添加到父View中的子View就一定在最外層,因此我們就有必要通過(guò)每個(gè)子View的Z軸數(shù)值對(duì)他們進(jìn)行重排序。
重排序之后,就按照排好的順序一個(gè)個(gè)拿到子View,并通過(guò)if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null))這句代碼來(lái)判斷子View是否可以接收當(dāng)前的觸摸事件。
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...省略Accessibility相關(guān)代碼...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
先看前一個(gè)方法,顯而易見接收觸摸事件有兩個(gè)條件,一是可見,二是不在執(zhí)行動(dòng)畫。
private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
接著看下一個(gè)方法,關(guān)在在于transformPointToViewLocal會(huì)加上偏移值,而child.pointInView(point[0], point[1])會(huì)判斷該child是否可以接收到(x,y)點(diǎn)的觸摸事件
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;
}
綜合上述兩處,我們總結(jié)View能夠接收觸摸事件的條件一共有四個(gè):
1.可見
2.不在執(zhí)行動(dòng)畫
3.可點(diǎn)擊
4.觸摸點(diǎn)在View內(nèi)
在之前的分析中,如果遍歷到的子View不能接收事件,就直接continue,反之則執(zhí)行下面的代碼:
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()也是重點(diǎn)方法:
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
當(dāng)觸摸事件為DOWN時(shí),mFirstTouchTarget會(huì)被重置為null,因此此時(shí)getTouchTarget返回null,什么都沒有發(fā)生,代碼繼續(xù)向下執(zhí)行。那么為什么又說(shuō)這是重點(diǎn)方法呢,因?yàn)楫?dāng)觸摸事件為MOVE時(shí),mFirstTouchTarget不為null,此時(shí)就會(huì)直接break出當(dāng)前循環(huán)。我們知道MOVE是十分頻繁的調(diào)用,所以這里相當(dāng)于是做了一層性能優(yōu)化。具體是怎么優(yōu)化的,我們?cè)谙聜€(gè)篇章還會(huì)介紹。
拓展完畢回到主線上,代碼再次進(jìn)入條件判斷
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
dispatchTransformedTouchEvent()在之前出現(xiàn)過(guò),當(dāng)父View攔截事件時(shí),該方法被調(diào)用,第三個(gè)參數(shù)為null,并最終調(diào)用了父View的Touch或Click方法。而此時(shí),第三個(gè)參數(shù)不再為null,取而代之的是可以接收觸摸事件的子View,我們重新來(lái)看dispatchTransformedTouchEvent()中最關(guān)鍵的代碼:
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;
當(dāng)child不為空時(shí),先進(jìn)行觸摸點(diǎn)的偏移計(jì)算,接著執(zhí)行handled = child.dispatchTouchEvent(event)。如果child是ViewGroup,就相當(dāng)于重新執(zhí)行上面的一大波步驟;如果child是View,則類似于父View攔截事件的過(guò)程,會(huì)執(zhí)行View本身的Touch或Click方法。
3.2.4 事件回調(diào)
好了好了,事件分發(fā)下去的過(guò)程終于梳理完了,我們接著看分發(fā)結(jié)果的回調(diào)。
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,就會(huì)進(jìn)入判斷體并執(zhí)行最重要的一行代碼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;
}
之前說(shuō)過(guò)TouchTarget表示觸摸目標(biāo),其本質(zhì)是一個(gè)單向鏈表。顯而易見,addTouchTarget()的作用就是為mFirstTouchTarget 賦值,初始化這個(gè)鏈表。
現(xiàn)在mFirstTouchTarget 不為Null了,我們來(lái)看最后一段源碼:
// 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 {
...
}
}
當(dāng)子View返回true時(shí),子View就消耗了這個(gè)事件,執(zhí)行else中的代碼,handled被賦值為true;而當(dāng)子View不消耗事件返回false時(shí),執(zhí)行if中的代碼,dispatchTransformedTouchEvent一共出現(xiàn)了3次,大家應(yīng)該很熟悉了,當(dāng)?shù)谌齻€(gè)參數(shù)為null時(shí),會(huì)調(diào)用父View的Touch或Click方法,就這樣一層一層的回調(diào)上去,整個(gè)過(guò)程是一個(gè)很完美的遞歸。
3.3 MOVE事件
在前面的文章中,我們基本是以DOWN事件為例進(jìn)行分析的,如果你熟練理解了上面所講的內(nèi)容,那么請(qǐng)換上這輛快車,繼續(xù)來(lái)看看MOVE事件是怎樣的玩法。
3.3.1 MOVE事件攔截
MOVE事件也能夠被攔截的原因就在于這個(gè)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事件會(huì)為mFirstTouchTarget賦值,因此MOVE時(shí)mFirstTouchTarget!=null,該攔截的繼續(xù)攔截。
3.3.2 MOVE事件分發(fā)
現(xiàn)在回憶一下DOWN事件分發(fā)的那一大堆步驟,什么子View重排序啊、遍歷啊、判斷能否接收觸摸事件啊等等等等,其過(guò)程十分復(fù)雜,因?yàn)镈OWN是點(diǎn)一下就完事了,所以可以這么整,而MOVE的調(diào)用非常頻繁,要也這樣操作,用戶界面絕對(duì)會(huì)被卡死。
所以我們才會(huì)用到TouchTarget,觸發(fā)Down事件后,TouchTarget被賦值,其目標(biāo)就是可以接收觸摸事件的子View,因此在MOVE事件中,我們可以直接跳過(guò)前面的一大段代碼,直接從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;
}
}
這段代碼不久前出現(xiàn)過(guò),只是我省略了else中的部分代碼,因?yàn)橹霸谡f(shuō)DOWN事件,與MOVE無(wú)關(guān)?,F(xiàn)在我們來(lái)看完整的流程,當(dāng)mFirstTouchTarget不為Null時(shí),最關(guān)鍵的是這段:
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
MOVE事件就是在這里進(jìn)行分發(fā)與回調(diào)的!
4.總結(jié)
如果讀到最后,你有一種什么都聯(lián)系起來(lái)了,豁然開朗的感覺,那就點(diǎn)個(gè)贊唄!如果讀完心想這文章寫的什么[嗶]東西,請(qǐng)務(wù)必?cái)[上一份源碼再讀一次!
而如果你真的什么都懂,底下留言!我賠錢!