此文接于上一篇完全理解Android TouchEvent事件分發(fā)機(jī)制(一)
可以看出來,事件一旦被某一層消費(fèi)掉伪货,其它層就不會(huì)再消費(fèi)了
到這里其實(shí)對事件分發(fā)的機(jī)制就有個(gè)大概了解,知道里面的原理是怎么回事。
下面就讓我們來去梳理一下這個(gè)事件分發(fā)所走的邏輯替蛉。
我們仔細(xì)思考一下盗似,為什么有的事件有UP有的沒有俺亮?
為什么Up和Down的順序不同呢?
為什么要按照這個(gè)順序執(zhí)行呢罩润?
這個(gè)例子主要是為了說明分發(fā)玖翅、攔截、消費(fèi)的流程
以例一為例割以,在每個(gè) View 都不攔截 down 事件的情況下金度,down 事件是這樣傳遞的
super.dispatchTouchEvent方法,上面我們介紹過严沥,
這個(gè)方法內(nèi)部實(shí)際上是調(diào)用的onTouchEvent方法
所以最后的輸出日志順序就是從父到子依次調(diào)用分發(fā)和攔截猜极,然后從子到父依次調(diào)用消費(fèi)。
例二也是同理消玄,區(qū)別在于
當(dāng)Father拿到事件的時(shí)候跟伏,選擇了攔截下來不再詢問其他,
但是Father也沒消費(fèi),直接又還回給了Grandpa翩瓜,
Grandpa同樣也沒有消費(fèi)這個(gè)事件受扳。
所以最終的順序就是,從Grandpa到Father再返回Grandpa就結(jié)束了兔跌,沒有經(jīng)過LogImageView勘高。
例三的情況就不太一樣了
- 當(dāng)Grandpa->Father->LogImageView 傳遞到LogImageView時(shí),LogImageView不消費(fèi)又返回給了Father浮定,F(xiàn)ather在onTouchEvent消費(fèi)掉了事件相满。
- 然后反饋給Father說事件已經(jīng)消費(fèi)。桦卒,就等于parent.dispatchTouchEvent返回true給上一級(jí)的Grandpa立美,
Grandpa不會(huì)再調(diào)用grandpa.onTouchEvent方法。
從這里我們可以總結(jié)出來:
dispatchTouchEvent返回值的作用是用于標(biāo)志這個(gè)事件是否“消費(fèi)了”方灾,
無論是自己或者下面的子一級(jí)用掉了都算是消費(fèi)掉建蹄。
再如這個(gè)例子中,如果我們讓LogImageView消費(fèi)掉事件裕偿,
那么Father收到LogImageView的消息后洞慎,也會(huì)調(diào)用parent.dispatchTouchEvent返回true給Grandpa,
所以這個(gè)方法返回值的true是只要用掉就行嘿棘,無論自己還是下面某一級(jí)劲腿,
而非我把事件傳遞下去就是true了,下面沒人消費(fèi)最終其實(shí)還是返回false的鸟妙。
至此焦人,我們來總結(jié)一下這三個(gè)方法的大致作用:
- dispatchTouchEvent方法內(nèi)容里處理的是分發(fā)過程挥吵。可以理解為從Grandpa->Father->LogImageView一層層分發(fā)的動(dòng)作
dispatchTouchEvent的返回值則代表是否將事件分發(fā)出去用掉了花椭,自己用或者給某一層子級(jí)用都算分發(fā)成功忽匈。比如Father消費(fèi)了事件,或者他發(fā)出去給的LogImageView消費(fèi)了事件矿辽,這兩種情況下B的dispatchTouchEvent都會(huì)返回true給Grandpa
onInterceptTouchEvent會(huì)在第一輪從父到子的時(shí)候在分發(fā)時(shí)調(diào)用丹允,以它去決定是否攔截掉此事件不再向下分發(fā)。如果攔截下來袋倔,就會(huì)調(diào)用自己的onTouchEvent處理雕蔽;如果不攔截,則繼續(xù)向下傳遞
onTouchEvent代表消費(fèi)掉事件奕污。方法內(nèi)容是具體的事件處理方法萎羔,如何處理點(diǎn)擊滑動(dòng)等。
onTouchEvent的返回值則代表對上級(jí)的反饋碳默,通知這個(gè)東西我用掉啦,然后他的父級(jí)就會(huì)讓分發(fā)方法也返回true
下面我們來解釋為什么例一缘眶、二中沒有Up嘱根,而例三中有
一個(gè)Action_DOWN,一個(gè)ACTION_UP巷懈,若干個(gè)ACTION_MOVE该抒,才構(gòu)成完整的事件。
前倆例子里為什么沒有Up呢顶燕,很好理解凑保,
因?yàn)樗麄兌紱]有消費(fèi)事件啊,所以他們只有DOWN事件涌攻,因此只有Down欧引,沒Up。
例三做類比恳谎,F(xiàn)ather消費(fèi)掉了這個(gè)事件
流程依然是先從Grandpa開始分配
(grandpa.dispatchTouchEvent)
(grandpa.onInterceptTouchEvent 判斷是否攔截)Grandpa還是沒攔下來,繼續(xù)分發(fā)事件(grandpa不攔截,然后調(diào)用child.dispatchTouchEvent)
事件到了Father掉瞳,F(xiàn)ather進(jìn)行了消費(fèi)硕蛹。(parent沒有調(diào)用攔截方法)
Father調(diào)用onTouchEvent返回true消費(fèi)掉事件,完成了整個(gè)行為鸵膏。
【例四】
在Grandpa類的onInterceptTouchEvent中添加個(gè)判斷膊升,
如果動(dòng)作是UP就return true攔截掉,DOWN則不攔截和之前一樣
打印如下:
04-04 07:16:43.353 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup dispatchTouchEvent Event 0
04-04 07:16:43.355 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup onInterceptTouchEvent Event 0
04-04 07:16:43.355 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup dispatchTouchEvent Event 0
04-04 07:16:43.356 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup onInterceptTouchEvent Event 0
04-04 07:16:43.356 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: LogImageView dispatchTouchEvent Event 0
04-04 07:16:43.357 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: LogImageView onTouchEvent Event 0
04-04 07:16:43.357 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup onTouchEvent Event 0
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup dispatchTouchEvent Event 1
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup onInterceptTouchEvent Event 1
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup dispatchTouchEvent Event 3
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup onTouchEvent Event 3
前面Down行為和例三一樣谭企,后面就不同了
UP流程變了廓译,然后多了個(gè)CANCEL的動(dòng)作
這里我們可以理解為
GrandPa調(diào)用dispatchTouchEvent分發(fā)UP事件
GrandPa調(diào)用onInterceptTouchEvent返回true评肆,攔截UP,DOWN事件則是正常的傳遞
FatherView調(diào)用dispatchTouchEvent分發(fā)CANCEL動(dòng)作
FatherView使用CANCEL動(dòng)作調(diào)用onTouchEvent方法责循,結(jié)束
大概也就了解的差不多了糟港,還剩一個(gè)TouchTarget目標(biāo)的概念,
為什么例三中Up和Down流程不同院仿?
回頭去看完整源碼:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 1.每次起始動(dòng)作就重置之前的TouchTarget等參數(shù)
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 2.如果是起始動(dòng)作才攔截秸抚,或者已經(jīng)有人消費(fèi)掉了事件,再去判斷攔截
// 起始動(dòng)作是第一次向下分發(fā)的時(shí)候歹垫,每個(gè)view都可以決定是否攔截剥汤,然后進(jìn)一步判斷是否消費(fèi),很好理解
// 如果有人消費(fèi)掉了事件排惨,那么也攔截~ 就像例四中的情況吭敢,也可以再次判斷是否攔截的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 3.這里可以設(shè)置一個(gè)disallowIntercept標(biāo)志,如果是true暮芭,就是誰收到事件后都不準(zhǔn)攔截B雇铡!辕宏!
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 4.如果未攔截畜晰,只有Down動(dòng)作才去子一級(jí)去找目標(biāo)對象~
// 因?yàn)檎夷繕?biāo)這個(gè)操作只有Down中才會(huì)處理
if (actionMasked == MotionEvent.ACTION_DOWN ) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
newTouchTarget = getTouchTarget(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
if (mFirstTouchTarget == null) {
// 5.把自己當(dāng)做目標(biāo),去判斷自己的onTouchEvent是否消費(fèi)
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 6.如果有人消費(fèi)掉了事件瑞筐,找出他~
TouchTarget target = mFirstTouchTarget;
while (target != null) {
// 7.消費(fèi)對象信息其實(shí)是一個(gè)鏈?zhǔn)綄ο笃啾牵涊d著一個(gè)一個(gè)傳遞的人的信息,遍歷調(diào)用它c(diǎn)hild的分發(fā)方法
final TouchTarget next = target.next;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
target = next;
}
}
}
return handled;
}
dispatchTransformedTouchEvent方法聚假,內(nèi)部簡化代碼為:
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;
}
就是判斷如果沒child了(是ViewGroup但是沒子控件块蚌,或者自己就是View),
如果沒child膘格,就調(diào)用View的dispatchTouchEvent方法峭范,
實(shí)質(zhì)就是調(diào)用onTouchEvent判斷是否消費(fèi)掉事件
如果有child,就調(diào)用child的dispatchTouchEvent將事件一層層向下分發(fā)
例三四中的復(fù)雜情況
其中關(guān)鍵主要在于多了一個(gè)TouchTarget的處理.
向下傳遞的核心主要是在于dispatchTransformedTouchEvent方法
第一輪動(dòng)作的Down時(shí)闯袒,只要不攔截虎敦,就會(huì)在注釋4代碼處遍歷所有child調(diào)用該方法層層傳遞下去
而后續(xù)其他動(dòng)作時(shí),就會(huì)進(jìn)入注釋6代碼條件政敢,然后遍歷TouchTarget中的信息用該方法層層分發(fā)
注意不要誤解:
第一次Down的時(shí)候會(huì)for循環(huán)所有child
第二輪Up的時(shí)候也會(huì)while(target.next)的迭代循環(huán)挨個(gè)判斷,但是next是遍歷同級(jí)其徙,不是子級(jí)
dispatchTrancformTouchEvent(target.child)這里的.child才是向子一級(jí)一層一層分發(fā)傳遞的地方.
這個(gè)TouchTarget對象,主要保存的是傳遞路線信息喷户,它是一個(gè)鏈?zhǔn)浇Y(jié)構(gòu)
不過這個(gè)路線不是Grandpa->Father->LogImageView的一個(gè)單子唾那,而是每個(gè)ViewGroup都會(huì)保存一個(gè)向下的路線信息.
Cancel部分
dispatchTrancformTouchEvent中會(huì)判斷,如果cancel=true動(dòng)作,
則會(huì)把動(dòng)作改成ACTION_CANCEL一層一層的傳下去.
注意:
onTouch和onTouchEvent有什么區(qū)別闹获,又該如何使用期犬?
從源碼中可以看出,這兩個(gè)方法都是在View的dispatchTouchEvent中調(diào)用的避诽,onTouch優(yōu)先于onTouchEvent執(zhí)行龟虎。如果在onTouch方法中通過返回true將事件消費(fèi)掉,onTouchEvent將不會(huì)再執(zhí)行沙庐。
另外需要注意的是鲤妥,onTouch能夠得到執(zhí)行需要兩個(gè)前提條件,第一mOnTouchListener的值不能為空拱雏,第二當(dāng)前點(diǎn)擊的控件必須是enable的棉安。因此如果你有一個(gè)控件是非enable的,那么給它注冊onTouch事件將永遠(yuǎn)得不到執(zhí)行铸抑。對于這一類控件贡耽,如果我們想要監(jiān)聽它的touch事件,就必須通過在該控件中重寫onTouchEvent方法來實(shí)現(xiàn)鹊汛。
到這里蒲赂,我們的事件傳遞就全部講解完成了,下面讓我們看看他的實(shí)際用途吧刁憋。
用途:
滑動(dòng)事件沖突的解決方法:
外部攔截法
外部攔截法是指在有點(diǎn)擊事件時(shí)都要經(jīng)過父容器凳宙,那么在父容器時(shí)如果需要攔截就攔截自己處理,不需要?jiǎng)t傳遞給下一層進(jìn)行處理职祷,
下面看個(gè)例子
首先定義一個(gè)水平滑動(dòng)的HorizontalScrollView,看主要代碼
主要的攔截是需要重寫onInterceptTouchEvent
此示例是借鑒于lylodlig的一篇文章届囚,在此感謝有梆。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//down事件不攔截,否則無法傳給子元素
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
//水平滑動(dòng)則攔截
if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//不攔截意系,否則子元素?zé)o法收到
intercepted = false;
break;
}
//因?yàn)楫?dāng)ViewGroup中的子View可能消耗了down事件泥耀,在onTouchEvent無法獲取,
// 無法對mLastX賦初值蛔添,所以在這里賦值一次
mLastX = x;
mLastY = y;
mLastYIntercept = y;
mLastXIntercept = x;
return intercepted;
}
在down事件不需要攔截痰催,返回false,否則的話子view無法收到事件迎瞧,將全部會(huì)由父容器處理夸溶,這不是希望的;up事件也要返回false凶硅,否則最后子view收不到
看看move事件缝裁,當(dāng)水平滑動(dòng)距離大于豎直距離時(shí),代表水平滑動(dòng)足绅,返回true捷绑,由父類來進(jìn)行處理韩脑,否則交由子view處理。這里move事件就是主要的攔截條件判斷粹污,如果你遇到的不是水平和豎直的條件這么簡單段多,就可以在這里進(jìn)行改變,比如壮吩,ScrollView嵌套了ListView进苍,條件就變成,當(dāng)ListView滑動(dòng)到底部或頂部時(shí)粥航,返回true琅捏,交由父類滑動(dòng)處理,否則自身ListView滑動(dòng)递雀。
在onTouchEvent中主要是做的滑動(dòng)切換的處理
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (getScrollX() < 0) {
scrollTo(0, 0);
}
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocityTracker = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocityTracker) > 50) {//速度大于50則滑動(dòng)到下一個(gè)
mChildIndex = xVelocityTracker > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWith / 2) / mChildWith;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWith - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastY = y;
mLastX = x;
return true;
}
在這個(gè)嵌套一個(gè)普通的ListView柄延,這樣就可以解決水平和豎直滑動(dòng)沖突的問題了。
<com.shanlovana.rcimageview.HorizontalScrollViewEx
android:layout_width="match_parent"
android:layout_height="200dp">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_bright"
android:text="2" />
<Button
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_green_dark"
android:text="3" />
</com.shanlovana.rcimageview.HorizontalScrollView>
內(nèi)部攔截法
內(nèi)部攔截法是父容器不攔截任何事件缀程,所有事件都傳遞給子view搜吧,如果需要就直接消耗掉,不需要再傳給父容器處理
下面重寫一個(gè)ListView杨凑,只需要重寫一個(gè)dispatchTouchEvent方法就OK
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//子View的所有父ViewGroup都會(huì)跳過onInterceptTouchEvent的回調(diào)
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {//水平滑動(dòng)滤奈,使得父類可以執(zhí)行onInterceptTouchEvent
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
在down事件調(diào)用getParent().requestDisallowInterceptTouchEvent(true),這句代碼的意思是使這個(gè)view的父容器都會(huì)跳過onInterceptTouchEvent撩满,在move中判斷如果是水平滑動(dòng)就由父容器去處理蜒程,父容器只需要把之前的onInterceptTouchEvent改為下面那樣,其他不變.
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
//如果是非down事件伺帘,說明子View并沒有攔截父類的onInterceptTouchEvent
//說明該事件交由父類處理昭躺,所以不需要再傳遞給子類,返回true
return true;
}
}
解決這些問題的基本的思想就是這些伪嫁,希望可以幫助到您领炫。