設(shè)計(jì)UI時(shí)附鸽,親愛(ài)的交互設(shè)計(jì)師們總會(huì)有一些天馬行空的想法弦撩,大多數(shù)情況下原生的控件已不能支持這些“看似簡(jiǎn)單”的交互邏輯,需要繼承ListView
节视、ViewPager
晦墙、ScrollView
甚至直接繼承View來(lái)自定義一些特性來(lái)支撐。在處理觸摸事件時(shí)肴茄,無(wú)可避免的需要重寫onInterceptTouchEvent
與onTouchEvent
這兩個(gè)方法晌畅。本文將從源碼的角度,從這兩個(gè)棘手的函數(shù)為切入點(diǎn)寡痰,對(duì)觸摸事件在View
中的傳遞邏輯進(jìn)行梳理抗楔。
1.概述
本文中只簡(jiǎn)單的考慮單指觸摸事件棋凳。一次觸摸事件通常有一系列TouchEvent
組成,這一系列TouchEvent
通常由一個(gè)ACTION_DOWN
開(kāi)始连躏,并且由一個(gè)ACTION_UP/ACTION_CANCEL
結(jié)束剩岳。這一系列TouchEvent
都會(huì)自上而下傳入視圖結(jié)構(gòu),上層View
根據(jù)自身需求決定是由自身來(lái)處理該事件入热,或者將其傳入下一層視圖處理拍棕。通常而言ViewGroup.onInterceptTouchEvent
決定了父View
是否攔截該觸摸事件,而View.onTouchEvent
中則實(shí)現(xiàn)了其自身如何處理該觸摸事件勺良。
-
ViewGroup.onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev);
API 24對(duì)該方法的官方說(shuō)明:
實(shí)現(xiàn)該方法以攔截所有的屏幕觸摸事件绰播,從而使你能夠監(jiān)控觸摸事件分發(fā)給子View的過(guò)程并且隨時(shí)攔截。
使用該方法時(shí)需小心謹(jǐn)慎尚困,因?yàn)樵摲椒ㄅcView.onTouchEvent
的交互相當(dāng)復(fù)雜蠢箩,并且要正確的實(shí)現(xiàn)這兩個(gè)方法。TouchEvent
將會(huì)根據(jù)以下順序被接收:- 你將在這里接收到
ACTION_DOWN
-
ACTION_DOWN
要么由一個(gè)子View來(lái)處理事甜,要么由你自身的onTouchEvent
來(lái)處理谬泌。后者意味著你應(yīng)該實(shí)現(xiàn)onTouchEvent
并返回true
,你才能收到后續(xù)的TouchEvent
(而不是由你的父View
來(lái)處理)逻谦;并且掌实,當(dāng)你在onTouchEvent
中返回true
時(shí),你將不會(huì)在onInterceptTouchEvent
中接收到后續(xù)的TouchEvent
邦马,但是仍然會(huì)正常的傳遞到onTouchEvent
中 - 如果你在此方法中返回
false
贱鼻,那么本次觸摸事件中所有后續(xù)的TouchEvent
都會(huì)先傳遞到這里,然后傳遞到目標(biāo)View
的onTouchEvent
中 - 如果你在此方法中返回
true
勇婴,本次觸摸事件中所有后續(xù)的TouchEvent
都不會(huì)再傳遞到此方法。原本的目標(biāo)View
將會(huì)接收到一個(gè)同樣的TouchEvent
(但是action為ACTION_CANCEL
)嘱腥,之后的TouchEvent
會(huì)傳遞到你自身的TouchEvent
并且不再出現(xiàn)在此處
onInterceptTouchEvent
定義在ViewGroup
中耕渴,intercept一詞為攔截的意思。簡(jiǎn)而言之齿兔,該方法的用意為決定是否攔截該TouchEvent
橱脸,如果該方法返回true
表示攔截此TouchEvent
,否則會(huì)向下傳遞到子View
中分苇。在ViewGroup
中該方法直接返回true
添诉,繼承于ViewGroup
的控件根據(jù)自身需求自己實(shí)現(xiàn)。 - 你將在這里接收到
-
View.onTouchEvent
public boolean onTouchEvent(MotionEvent event)
onTouchEvent
定義在View
中医寿,該方法中實(shí)現(xiàn)了View
處理觸摸事件的真正過(guò)程栏赴,當(dāng)TouchEvent
傳入視圖并且決定由自身處理的時(shí)候,便會(huì)將其傳入onTouchEvent
靖秩。返回值true
表示該TouchEvent
被已被消費(fèi)须眷,相當(dāng)于告訴別人“我是這次觸摸事件的主人竖瘾,我將會(huì)處理本次觸摸事件”;返回false
則表示未被消費(fèi)花颗,TouchEvent
將會(huì)繼續(xù)被傳遞尋找新的“主人”捕传。在該方法中requestDisallowInterceptTouchEvent
有會(huì)被調(diào)用,用以禁止父View
攔截此次觸摸事件中后續(xù)的TouchEvent
扩劝,之后所有的TouchEvent
將不會(huì)傳遞到父View
的onInterceptTouchEvent
而直接傳遞到此處庸论。
2.分發(fā)
ViewGroup.dispatchTouchEvent(MotionEvent ev)
方法是觸摸事件在視圖結(jié)構(gòu)中傳遞邏輯的主導(dǎo)者。該方法最初定義在View
中(會(huì)調(diào)用onTouchEvent
并返回是否消費(fèi))棒呛,在ViewGroup
中被重寫聂示。TouchEvent
傳入ViewGroup
后dispatchTouchEvent
首先被調(diào)用以負(fù)責(zé)觸摸事件在自身與子View
之間的分發(fā)處理邏輯,并且通過(guò)返回值通知父View
是否消費(fèi)了TouchEvent
条霜。onInterceptTouchEvent
與onTouchEvent
都由其直接或間接被調(diào)用催什,多層視圖結(jié)構(gòu)通過(guò)一層層向下調(diào)用dispatchTouchEvent
尋找觸摸事件的“主人”。本節(jié)主要對(duì)以注釋的形式對(duì)該方法源碼進(jìn)行分析以初步了解TouchEvent在視圖結(jié)構(gòu)中的分發(fā)過(guò)程宰睡。
//源碼基于API Level 23蒲凶,即Android 6.0
//省略了一些代碼,著重分析單指觸摸事件的傳遞過(guò)程拆内。
//返回值為此view及子view是否handle該MotionEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
......
//如果是DOWN旋圆,作為觸摸事件的開(kāi)始,初始化
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//如果event為ACTION_DOWN麸恍,或者已知有子view能handle此次事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//正常的話灵巧,調(diào)用onInterceptTouchEvent來(lái)決定是否攔截該event
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
//如果有FLAG_DISALLOW_INTERCEPT標(biāo)記,則不攔截event
//一般當(dāng)子view處理了事件抹沪,而不希望父容器截?cái)鄷r(shí)刻肄,會(huì)通過(guò)調(diào)用requestDisallowInterceptTouchEvent來(lái)給父容器設(shè)置該標(biāo)記
intercepted = false;
}
} else {
//ACTION_DOWN為一次觸摸事件的開(kāi)始,ACTION_DOWN傳遞給子view之后融欧,若有子view能handle敏弃,那么該子view即設(shè)置為touchTarget
//如果event不為ACTION_DOWN,那么它是ACTION_DOWN之后一連串event之一噪馏,此時(shí)若沒(méi)有目標(biāo)touchTarget麦到,說(shuō)明并沒(méi)有子view能handle此次事件(或者上一個(gè)TouchEvent被攔截導(dǎo)致touchTarget被清空),故直接打斷交由自身處理
intercepted = true;
}
......
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
......
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//沒(méi)有取消也沒(méi)有攔截欠肾,并且為ACTION_DOWN瓶颠,嘗試找到一個(gè)能handle該事件的子view
......
for(child in this ViewGroup){
//遍歷所有子view
......
//跳過(guò) 無(wú)法接收事件 與 不在觸摸位置 的子view
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
......
//此處dispatchTransformedTouchEvent的作用為,將event的坐標(biāo)轉(zhuǎn)換成該子view的坐標(biāo)后刺桃,調(diào)用子view的dispatchTouchEvent
//返回值為該子view是否handle該event
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
......
//如果子view能夠handle該event粹淋,則將該子view設(shè)置為touchTarget,并設(shè)置標(biāo)記表示找到了target
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
if (mFirstTouchTarget == null) {
//到這里touchTarget為null有以下幾種情況:
//1.某次觸摸事件最初的ACTION_DOWN被攔截或者沒(méi)有目標(biāo)handle,致使此次事件所有的event都會(huì)走到這里;
//2.某次觸摸事件最初的ACTION_DOWN被目標(biāo)handle廓啊,而中途被攔截欢搜,此時(shí)touchTarget不會(huì)null,但是會(huì)在下面的代碼中被清空谴轮,從而使之后的event走到這里;
//注意此時(shí)調(diào)用dispatchTransformedTouchEvent的第三個(gè)參數(shù)child為null
//在dispatchTransformedTouchEvent中可以看到child==null時(shí)會(huì)調(diào)用到super.dispatchTouchEvent炒瘟,也就是View.dispatchTouchEvent,從而調(diào)用到onTouchEvent
//也就是說(shuō)第步,將此ViewGroup試做一個(gè)普通的View疮装,由其自身來(lái)處理該事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//走到這里說(shuō)明touchTarget!=null
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
//循環(huán)遍歷所有的touchTarget,通常單指觸摸事件只有一個(gè)touchTarget
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//如果該event已經(jīng)在上面尋找target的代碼中已經(jīng)分發(fā)給該view過(guò)了,則直接將handled置為true粘都,然后跳過(guò)
handled = true;
} else {
//走到這里廓推,說(shuō)明可定不是ACTION_DOWN了
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//如果cancelChild為false,那么將TouchEvent的坐標(biāo)轉(zhuǎn)換后傳遞給子View
//如果intercepted為true說(shuō)明上面決定要攔截該event翩隧,那么cancelChild為true樊展,將會(huì)傳遞一個(gè)同樣的但是為ACTION_CANCEL的touchEvent給子View
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
//子View是否消費(fèi)TouchEvent決定了handled的值
}
if (cancelChild) {
//如果cancelChild,那么循環(huán)清空所有的touchTarget,接下來(lái)的所有TouchEvent都將有自身的onTouchEvent來(lái)處理
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
......
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
}
......
//返回是否此View是否消費(fèi)此TouchEvent
return handled;
}
流程示意圖
概括來(lái)講堆生,ACTION_DOWN
的分發(fā)過(guò)程對(duì)于整個(gè)觸摸事件來(lái)講是相當(dāng)重要的专缠,而dispatchTouchEvent
就是為ACTION_DOWN
尋找“主人”的一個(gè)過(guò)程,如果找到了則返回true
淑仆。ViewGroup.onInterceptTouchEvent
在分發(fā)ACTION_DOWN
時(shí)涝婉,如果intercepted = false
,便會(huì)向下傳遞尋找有沒(méi)有子視圖能做這次事件的“主人”蔗怠。如果intercepted = true
墩弯,或者在子視圖中沒(méi)有找到“主人”,那么就將其本身視為一個(gè)普通的View
來(lái)調(diào)用onTouchEvent
來(lái)處理寞射。如果有子視圖或者其自身能handled渔工,那么就向上返回true
表示“爸爸,我找到它的主人了”桥温。
ACTION_MOVE
進(jìn)入dispatchTouchEvent
時(shí)引矩,如果之前在子視圖中找到了“主人”就直接將其傳遞至目標(biāo),否則就將其本身視為一個(gè)普通的View
來(lái)調(diào)用onTouchEvent
來(lái)處理策治。如果intercepted = true
則給之前的“主人”傳遞一個(gè)ACTION_CANCEL
脓魏,同時(shí)清空目標(biāo)兰吟,那么之后進(jìn)入的TouchEvent
將會(huì)被自身來(lái)處理通惫。
3.傳遞
至此為止,本文主要在橫向地分析TouchEvent
在ViewGroup
中的分發(fā)過(guò)程混蔼,而在開(kāi)發(fā)過(guò)程中履腋,通常我們更多需要關(guān)注的是TouchEvent
在視圖層次中縱向的傳遞過(guò)程。基于以上對(duì)于TouchEvent
分發(fā)過(guò)程的分析遵湖,可以很清晰地整理出縱向傳遞的邏輯(本節(jié)的分析過(guò)程基于一個(gè)四層的視圖結(jié)構(gòu)悔政,上方三層為ViewGroup
,最底層為普通的View
):
-
情景一
對(duì)于初始的ACTION_DOWN
延旧,通常情況下ViewGroup
并不能馬上去攔截谋国,因?yàn)橐坏r截,就意味著該ViewGroup
下的任何子視圖都不會(huì)收到任何觸摸事件迁沫。在這樣的前提下芦瘾,TouchEvent
傳入某一層ViewGroup
后,dispatchTouchEvent
通過(guò)調(diào)用onInterceptTouchEvent
(返回false
)得知無(wú)需攔截集畅,那么便會(huì)通過(guò)調(diào)用下一層視圖的dispatchTouchEvent
來(lái)講TouchEvent
傳遞至下一層近弟。底層View
的dispatchTouchEvent
將直接調(diào)用onTouchEvent
(返回true),于是dispatchTouchEvent
通過(guò)一層層向上返回true
表示找到了本次觸摸事件的目標(biāo)挺智。流程示意圖
此情景可以用下圖簡(jiǎn)單的描述(以下圖中祷愉,實(shí)現(xiàn)表示方法調(diào)用遗增,虛線表示方法返回值侣签,標(biāo)號(hào)表示發(fā)生時(shí)序)。
情景一傳遞示意圖簡(jiǎn)化流程圖
大部分情況摔吏,我們自定義控件時(shí)無(wú)需關(guān)心dispatchTouchEvent
的實(shí)現(xiàn)沐扳,也不用關(guān)心方法之間的調(diào)用關(guān)系泥从,而只需要關(guān)注onInterceptTouchEvent
與onTouchEvent
的實(shí)現(xiàn)與返回值來(lái)影響觸摸事件的傳遞,從而滿足自身的需求沪摄。在這樣的前提下躯嫉,以上流程圖可以簡(jiǎn)化為下圖的形式(實(shí)現(xiàn)僅表示TouchEvent
的傳遞方向)。
情景一傳遞簡(jiǎn)化圖 -
情景二
在情景一的前提下杨拐,如果不出其他幺蛾子的話祈餐,此次觸摸事件中后續(xù)的TouchEvent
都會(huì)以相同的路徑向下傳遞。但是如果對(duì)于其中某一個(gè)TouchEvent
哄陶,Level 1的ViewGroup
在onInterceptTouchEvent
返回了true
帆阳,那么根據(jù)上一節(jié)的分析,ViewGroup
首先會(huì)沿原路徑向下傳遞一個(gè)ACTION_CANCEL
屋吨,并且之后所有的TouchEvent
都將會(huì)直接傳遞到其自身onTouchEvent
中處理蜒谤,因?yàn)榇藭r(shí)該ViewGroup
自身已成為本次觸摸事件的新“主人”。簡(jiǎn)化流程圖
情景二傳遞簡(jiǎn)化圖 -
情景三
在情景一的基礎(chǔ)上至扰,ACTION_DOWN
時(shí)鳍徽,如果底層View
在onTouchEvent
中返回了false,那么dispatchTouchEvent
就會(huì)返回給上層ViewGroup
值false
來(lái)表示其并不能處理本次觸摸事件敢课,那么上層ViewGroup
便會(huì)調(diào)用自身的onTouchEvent
并通過(guò)dispatchTouchEvent
將返回值向上傳遞阶祭,直到找到觸摸事件的”主人“绷杜。流程示意圖
情景三傳遞示意圖簡(jiǎn)化流程圖
情景三傳遞簡(jiǎn)化圖 -
情景四
在情景三的前提下,觸摸事件的后續(xù)TouchEvent
將會(huì)沿最短路徑直接傳遞給目標(biāo)濒募,而不再按照ACTION_DOWN
時(shí)的路徑走到最底層鞭盟。需要注意的是,由于TouchEvent
由Level 2的ViewGroup
自身來(lái)處理而不是子視圖瑰剃,此時(shí)應(yīng)將其視為一個(gè)普通的View
齿诉,TouchEvent
將直接進(jìn)入其onTouchEvent
而不再先進(jìn)入onInterceptTouchEvent
。簡(jiǎn)化流程圖
情景四傳遞簡(jiǎn)化圖 -
情景五
將情景一與情景二結(jié)合一下晌姚,ACTION_DOWN
時(shí)鹃两,如果某一層ViewGroup
在onInterceptTouchEvent
時(shí)返回true,那么TouchEvent
將直接傳遞到其自身onTouchEvent
舀凛,之后根據(jù)其返回值依照上面所述邏輯繼續(xù)傳遞俊扳。流程示意圖
情景五傳遞示意圖簡(jiǎn)化流程圖
情景五傳遞簡(jiǎn)化圖 -
情景六
有些情況下,比如ListView
與ScrollView
處理滑動(dòng)事件時(shí)猛遍,當(dāng)其希望對(duì)整個(gè)觸摸事件完全掌控而不希望父視圖攔截時(shí)馋记,會(huì)通過(guò)調(diào)用requestDisallowInterceptTouchEvent循環(huán)通知各層父視圖不要攔截之后的TouchEvent
,這時(shí)之后的所有TouchEvent
將不再傳遞到所有父視圖的onInterceptTouchEvent
而直接傳遞到該View
懊烤。流程示意圖
情景六傳遞示意圖
4.樣例
本節(jié)以兩個(gè)具體樣例來(lái)協(xié)助理解上述縱向傳遞過(guò)程梯醒。
-
樣例一
考慮這樣的一個(gè)三層視圖結(jié)構(gòu):從上到下依次為ScrollView
,ViewPager
腌紧,ListView
茸习。如果不做任何處理,那么手指在屏幕上下滑動(dòng)將會(huì)是以下的一個(gè)處理過(guò)程:1. 10-25 19:43:37.984 ScrollView onInterceptTouchEvent : Action Down x : 839.0 y : 1340.0 10-25 19:43:37.984 ViewPager onInterceptTouchEvent : Action Down x : 839.0 y : 996.0 10-25 19:43:37.984 ListView onInterceptTouchEvent : Action Down x : 839.0 y : 996.0 10-25 19:43:37.984 ListView onTouchEvent : Action Down x : 839.0 y : 996.0 2. 10-25 19:43:37.994 ScrollView onInterceptTouchEvent : Action Move x : 839.0 y : 1340.0 10-25 19:43:37.994 ViewPager onInterceptTouchEvent : Action Move x : 839.0 y : 996.0 10-25 19:43:37.994 ListView onTouchEvent : Action Move x : 839.0 y : 996.0 ... ... 3. 10-25 19:43:38.164 ScrollView onInterceptTouchEvent : Action Move x : 845.0 y : 1277.5642 10-25 19:43:38.164 ViewPager onInterceptTouchEvent : Action Move x : 845.0 y : 933.5642 10-25 19:43:38.164 ListView onTouchEvent : Action Move x : 845.0 y : 933.5642 4. 10-25 19:43:38.184 ScrollView onInterceptTouchEvent : Action Move x : 846.0 y : 1265.3169 10-25 19:43:38.184 ViewPager onInterceptTouchEvent : Action Up/Cancel x : 846.0 y : 1265.3169 10-25 19:43:38.184 ListView onTouchEvent : Action Up/Cancel x : 846.0 y : 1265.3169 5. 10-25 19:43:38.214 ScrollView onTouchEvent : Action Move x : 847.0 y : 1237.8169 6. 10-25 19:43:38.234 ScrollView onTouchEvent : Action Move x : 848.0 y : 1227.139 ... ... 7. 10-25 19:43:38.484 ScrollView onTouchEvent : Action Move x : 860.8562 y : 1062.2943 8. 10-25 19:43:38.484 ScrollView onTouchEvent : Action Up/Cancel x : 859.43677 y : 1065.0692
-
ACTION_DOWN
壁肋,本次觸摸事件的開(kāi)始号胚,此時(shí)為上一節(jié)情景三所述傳遞過(guò)程,ScrollView
浸遗,ViewPager
猫胁,ListView
相繼在onInterceptTouchEvent
返回true,使觸摸事件一直能傳遞到最底層跛锌。此時(shí)ACTION_DOWN
傳遞到ListView
的子View
時(shí)弃秆,子View
不需要處理觸摸事件,從而在onTouchEvent
中返回了false髓帽,從而ACTION_DOWN
返回到上一層進(jìn)入到了ListView
的onTouchEvent
中并返回了true菠赚,此時(shí)ListView
成為了整個(gè)觸摸事件的“主人”。
- 上一節(jié)情景四所述傳遞過(guò)程郑藏,
ACTION_MOVE
通過(guò)最短路徑進(jìn)入“主人”ListView
的onTouchEvent
中衡查,并且不經(jīng)過(guò)ListView
的onInterceptTouchEvent
。
- 同2译秦。
- 由于此時(shí)手指已經(jīng)在屏幕豎直方向劃過(guò)一定距離峡捡,最頂層的
ScrollView
認(rèn)定這是一次上下滾動(dòng)的事件,在ListView
調(diào)用requestDisallowInterceptTouchEvent
獨(dú)占事件之前搶先一步在onInterceptTouchEvent
中返回true攔截TouchEvent
筑悴,成為了這次觸摸事件的新“主人”们拙,此時(shí)在下層的ViewPager
與ListView
中收到了一個(gè)ACTION_CANCEL
。
- 之后所有的
TouchEvent
便直接進(jìn)入ScrollView
的onTouchEvent
阁吝,直到最后的ACTION_UP
砚婆。
-
-
樣例二
本例基于樣例一的模型,但是對(duì)ScrollView
進(jìn)行處理突勇,使其永遠(yuǎn)在onInterceptTouchEvent
中返回false
装盯。1. 10-25 19:43:38.484 ScrollView onInterceptTouchEvent : Action Down x : 859.43677 y : 1065.0692 10-25 19:43:38.484 ViewPager onInterceptTouchEvent : Action Down x : 859.43677 y : 921.0692 10-25 19:43:38.484 ListView onInterceptTouchEvent : Action Down x : 859.43677 y : 921.0692 10-25 19:43:38.484 ListView onTouchEvent : Action Down x : 859.43677 y : 921.0692 2. 10-25 19:43:38.484 ScrollView onInterceptTouchEvent : Action Move x : 859.43677 y : 1062.2943 10-25 19:43:38.484 ViewPager onInterceptTouchEvent : Action Move x : 859.43677 y : 918.2943 10-25 19:43:38.484 ListView onTouchEvent : Action Move x : 859.43677 y : 918.2943 ... ... 3. 10-25 19:43:38.564 ScrollView onInterceptTouchEvent : Action Move x : 867.7982 y : 985.2108 10-25 19:43:38.564 ViewPager onInterceptTouchEvent : Action Move x : 867.7982 y : 841.2108 10-25 19:43:38.564 ListView onTouchEvent : Action Move x : 867.7982 y : 841.2108 4. 10-25 19:43:38.584 ListView onTouchEvent : Action Move x : 869.28864 y : 823.2477 5. 10-25 19:43:38.594 ListView onTouchEvent : Action Move x : 873.9039 y : 805.7499 ... ... 6. 10-25 19:43:40.334 ListView onTouchEvent : Action Move x : 826.0 y : 1562.0 7. 10-25 19:43:40.334 ListView onTouchEvent : Action Up/Cancel x : 826.0 y : 1562.0
-
ACTION_DOWN
,本次觸摸事件的開(kāi)始甲馋,此時(shí)為上一節(jié)情景三所述傳遞過(guò)程埂奈,ScrollView
,ViewPager
定躏,ListView
相繼在onInterceptTouchEvent
返回true账磺,使觸摸事件一直能傳遞到最底層。此時(shí)ACTION_DOWN
傳遞到ListView
的子View
時(shí)痊远,子View
不需要處理觸摸事件垮抗,從而在onTouchEvent
中返回了false,從而ACTION_DOWN
返回到上一層進(jìn)入到了ListView
的onTouchEvent
中并返回了true碧聪,此時(shí)ListView
成為了整個(gè)觸摸事件的“主人”冒版。
- 上一節(jié)情景四所述傳遞過(guò)程,
ACTION_MOVE
通過(guò)最短路徑進(jìn)入“主人”ListView
的onTouchEvent
中逞姿,并且不經(jīng)過(guò)ListView
的onInterceptTouchEvent
辞嗡。
- 同2。需要注意的是滞造,由于
ScrollView
不再能攔截事件欲间,手指劃過(guò)一定距離后,ListView
認(rèn)定這是一次上下滾動(dòng)的事件断部,不希望之后的TouchEvent
被父視圖攔截猎贴,所以在此時(shí)調(diào)用了requestDisallowInterceptTouchEvent
。
- 父視圖不再能攔截
TouchEvent
蝴光,所有TouchEvent
直接進(jìn)入ListView
的onTouchEvent
中她渴,直到最后的ACTION_UP
。
-
5.實(shí)踐
考慮這樣的一個(gè)三層的視圖(忽略了一些無(wú)關(guān)緊要的層次):
ScrollView
中含有一個(gè)TextView
與ViewPager
蔑祟,其中ViewPager
的高度與ScrollView
的高度一致趁耗,而在ViewPager
的某一頁(yè)為一個(gè)同等大小的ListView
,通過(guò)在onMeasure
中作一些必要的處理從而將整個(gè)視圖完整的顯示之后疆虚,會(huì)發(fā)現(xiàn)ListView
完全無(wú)法滾動(dòng)苛败。而這個(gè)視圖結(jié)構(gòu)應(yīng)該挺常見(jiàn)满葛,交互的需求應(yīng)該更常見(jiàn):手指向上滑動(dòng)時(shí),先滾動(dòng)ScrollView
罢屈,滾動(dòng)到底后再滾動(dòng)ListView
嘀韧;手指向下滑動(dòng)時(shí),先滾動(dòng)ListView
缠捌,滾動(dòng)到底后再滾動(dòng)ScrollView
锄贷。
首先對(duì)于這個(gè)需求,相信大家會(huì)首先想到API 21推出的NestedScroll
曼月。在學(xué)習(xí)了Android
觸摸事件傳遞之后谊却,決定從onInterceptTouchEvent
與onTouchEvent
這兩個(gè)方法做做手腳,來(lái)實(shí)現(xiàn)這一需求哑芹。我的思路分為兩步:
對(duì)
onInterceptTouchEvent
做手腳炎辨。手指向上滑動(dòng)時(shí),當(dāng)ScrollView
滑動(dòng)到邊界時(shí)聪姿,onInterceptTouchEvent
返回false
蹦魔,將事件交由ListView
處理,使ListView
能夠滑動(dòng)咳燕;手指向下滑動(dòng)時(shí)勿决,如果ListView
能夠滑動(dòng),就在onInterceptTouchEvent
中返回false
招盲,讓ListView
優(yōu)先滑動(dòng)低缩。這樣下來(lái),雖然還無(wú)法在一次手指滑動(dòng)過(guò)程中切換ScrollView
與ListView
的滑動(dòng)曹货,但是已經(jīng)能夠用兩次手指滑動(dòng)來(lái)切換了咆繁。對(duì)
onTouchEvent
做手腳。手指向上滑動(dòng)時(shí)顶籽,當(dāng)ScrollView
滑動(dòng)到邊界時(shí)玩般,首先分發(fā)一個(gè)ACTION_CANCEL
表示此次觸摸事件已結(jié)束,同時(shí)馬上再分發(fā)一個(gè)ACTION_DOWN
表示新一次觸摸事件開(kāi)始礼饱,這時(shí)通過(guò)上一步onInterceptTouchEvent
中做的手腳就將滑動(dòng)切換到了ListView
坏为,為了達(dá)到目的不擇手段地強(qiáng)行將一次觸摸事件拆分為兩個(gè);手指向下滑動(dòng)時(shí)镊绪,當(dāng)ListView
滑動(dòng)到邊界時(shí)匀伏,通知最頂層的ScrollView
分發(fā)兩個(gè)新事件來(lái)進(jìn)行強(qiáng)拆。
自定義ScrollView
//記錄觸摸起始位置的Y坐標(biāo)
private float downY;
//是否有子視圖正在被拖動(dòng)的標(biāo)記
private boolean isChildBeingDragged;
private int touchSlop;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//初始化
downY = ev.getY();
isChildBeingDragged = false;
touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
break;
case MotionEvent.ACTION_MOVE:
if(!isChildBeingDragged){
//如果沒(méi)有子視圖正在被拖動(dòng)
float deltY = ev.getY() - downY;
if(Math.abs(deltY) > touchSlop && callback != null && callback.canChildScroll(0-(int) deltY)){
//滑動(dòng)距離已經(jīng)可判定為上下滑動(dòng)事件蝴韭,并且通過(guò)回調(diào)得知子視圖在該方向上能夠滑動(dòng)
if((deltY < 0 && !canScrollVertically(0-(int) deltY))
|| (deltY > 0)){
//deltY < 0 為手指向上滑動(dòng)够颠,此時(shí)自身已不能向上滑動(dòng),則不攔截交由子視圖處理
//deltY > 0 為手指向下滑動(dòng)榄鉴,子視圖還能向下滑動(dòng)履磨,則優(yōu)先交由子視圖滑動(dòng)
isChildBeingDragged = true;
return false;
}
}
//其他情況則正常處理
return super.onInterceptTouchEvent(ev);
}
//如果有子視圖正在被拖動(dòng)蛉抓,則不攔截事件
return false;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
isChildBeingDragged = false;
break;
}
return super.onInterceptTouchEvent(ev);
}
//記錄上一次TouchEvent的Y坐標(biāo)
private float lastY;
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if(lastY == -1) {
lastY = ev.getY();
break;
}
float deltY = ev.getY()-lastY;
float scrollY = computeVerticalScrollOffset();
float scrollRange = computeVerticalScrollRange() - computeVerticalScrollExtent();
if(deltY < 0 && scrollY <= scrollRange && scrollY-deltY > scrollRange){
//如果手指向上滑動(dòng),并且算上當(dāng)前deltY之后已超出最大可滑動(dòng)距離
//在最大滑動(dòng)距離對(duì)應(yīng)處分發(fā)一個(gè)ACTION_UP
ev.setLocation(ev.getX(), lastY - scrollRange + getScrollY());
super.onTouchEvent(ev);
ev.setAction(MotionEvent.ACTION_UP);
dispatchTouchEvent(ev);
//在相同位置分發(fā)一個(gè)ACTION_DOWN
ev.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(ev);
//加上剩余的距離后分發(fā)一個(gè)ACTION_MOVE
ev.setAction(MotionEvent.ACTION_MOVE);
ev.offsetLocation(0, deltY + scrollRange - scrollY);
dispatchTouchEvent(ev);
return true;
}
lastY = ev.getY();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
lastY = -1;
break;
default:
}
return super.onTouchEvent(ev);
}
自定義ListView
//此處不添加注釋了剃诅,道理與上面相當(dāng)
private float downY;
private int touchSlop;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
downY = ev.getY();
touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return super.onInterceptTouchEvent(ev);
}
private float lastX;
private float lastY;
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if(lastX == -1 || lastY == -1) {
lastY = ev.getY();
break;
}
float deltY = ev.getY()-lastY;
float scrollY = computeVerticalScrollOffset();
if(ev.getY() - downY > touchSlop && callback != null && deltY > 0 && scrollY >= 0 && scrollY - deltY < 0){
ev.setLocation(ev.getX(), lastY + scrollY);
super.onTouchEvent(ev);
ev.setAction(MotionEvent.ACTION_UP);
ev.offsetLocation(0, callback.getParentExtraHeight()); //這里注意需要通知最上層的視圖來(lái)分發(fā)TouchEvent巷送,而不是自己分發(fā)
callback.notifyParentDispatchTouchEvent(ev);
ev.setAction(MotionEvent.ACTION_DOWN);
callback.notifyParentDispatchTouchEvent(ev);
ev.setAction(MotionEvent.ACTION_MOVE);
ev.offsetLocation(0, deltY - scrollY);
callback.notifyParentDispatchTouchEvent(ev);
return true;
}
lastY = ev.getY();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
lastY = -1;
break;
}
return super.onTouchEvent(ev);
}