Android觸摸事件傳遞分析與實(shí)踐

設(shè)計(jì)UI時(shí)附鸽,親愛(ài)的交互設(shè)計(jì)師們總會(huì)有一些天馬行空的想法弦撩,大多數(shù)情況下原生的控件已不能支持這些“看似簡(jiǎn)單”的交互邏輯,需要繼承ListView节视、ViewPager晦墙、ScrollView甚至直接繼承View來(lái)自定義一些特性來(lái)支撐。在處理觸摸事件時(shí)肴茄,無(wú)可避免的需要重寫onInterceptTouchEventonTouchEvent這兩個(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ù)以下順序被接收:

    1. 你將在這里接收到ACTION_DOWN
    2. 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
    3. 如果你在此方法中返回false贱鼻,那么本次觸摸事件中所有后續(xù)的TouchEvent都會(huì)先傳遞到這里,然后傳遞到目標(biāo)ViewonTouchEvent
    4. 如果你在此方法中返回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ì)傳遞到父ViewonInterceptTouchEvent而直接傳遞到此處庸论。

2.分發(fā)

ViewGroup.dispatchTouchEvent(MotionEvent ev)方法是觸摸事件在視圖結(jié)構(gòu)中傳遞邏輯的主導(dǎo)者。該方法最初定義在View中(會(huì)調(diào)用onTouchEvent并返回是否消費(fèi))棒呛,在ViewGroup中被重寫聂示。TouchEvent傳入ViewGroupdispatchTouchEvent首先被調(diào)用以負(fù)責(zé)觸摸事件在自身與子View之間的分發(fā)處理邏輯,并且通過(guò)返回值通知父View是否消費(fèi)了TouchEvent条霜。onInterceptTouchEventonTouchEvent都由其直接或間接被調(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;
}

流程示意圖

分發(fā)過(guò)程示意圖
分發(fā)過(guò)程示意圖

概括來(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.傳遞

至此為止,本文主要在橫向地分析TouchEventViewGroup中的分發(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傳遞至下一層近弟。底層ViewdispatchTouchEvent將直接調(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)注onInterceptTouchEventonTouchEvent的實(shí)現(xiàn)與返回值來(lái)影響觸摸事件的傳遞,從而滿足自身的需求沪摄。在這樣的前提下躯嫉,以上流程圖可以簡(jiǎn)化為下圖的形式(實(shí)現(xiàn)僅表示TouchEvent的傳遞方向)。

    情景一傳遞簡(jiǎn)化圖
    情景一傳遞簡(jiǎn)化圖

  • 情景二
    在情景一的前提下杨拐,如果不出其他幺蛾子的話祈餐,此次觸摸事件中后續(xù)的TouchEvent都會(huì)以相同的路徑向下傳遞。但是如果對(duì)于其中某一個(gè)TouchEvent哄陶,Level 1的ViewGrouponInterceptTouchEvent返回了true帆阳,那么根據(jù)上一節(jié)的分析,ViewGroup首先會(huì)沿原路徑向下傳遞一個(gè)ACTION_CANCEL屋吨,并且之后所有的TouchEvent都將會(huì)直接傳遞到其自身onTouchEvent中處理蜒谤,因?yàn)榇藭r(shí)該ViewGroup自身已成為本次觸摸事件的新“主人”。

    簡(jiǎn)化流程圖

    情景二傳遞簡(jiǎn)化圖
    情景二傳遞簡(jiǎn)化圖

  • 情景三
    在情景一的基礎(chǔ)上至扰,ACTION_DOWN時(shí)鳍徽,如果底層ViewonTouchEvent中返回了false,那么dispatchTouchEvent就會(huì)返回給上層ViewGroupfalse來(lái)表示其并不能處理本次觸摸事件敢课,那么上層ViewGroup便會(huì)調(diào)用自身的onTouchEvent并通過(guò)dispatchTouchEvent將返回值向上傳遞阶祭,直到找到觸摸事件的”主人“绷杜。

    流程示意圖

    情景三傳遞示意圖
    情景三傳遞示意圖

    簡(jiǎn)化流程圖

    情景三傳遞簡(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ǎn)化圖

  • 情景五
    將情景一與情景二結(jié)合一下晌姚,ACTION_DOWN時(shí)鹃两,如果某一層ViewGrouponInterceptTouchEvent時(shí)返回true,那么TouchEvent將直接傳遞到其自身onTouchEvent舀凛,之后根據(jù)其返回值依照上面所述邏輯繼續(xù)傳遞俊扳。

    流程示意圖

    情景五傳遞示意圖
    情景五傳遞示意圖

    簡(jiǎn)化流程圖

    情景五傳遞簡(jiǎn)化圖
    情景五傳遞簡(jiǎn)化圖

  • 情景六
    有些情況下,比如ListViewScrollView處理滑動(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):從上到下依次為ScrollViewViewPager腌紧,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
    
    1. 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)入到了ListViewonTouchEvent中并返回了true菠赚,此時(shí)ListView成為了整個(gè)觸摸事件的“主人”。
    1. 上一節(jié)情景四所述傳遞過(guò)程郑藏,ACTION_MOVE通過(guò)最短路徑進(jìn)入“主人”ListViewonTouchEvent中衡查,并且不經(jīng)過(guò)ListViewonInterceptTouchEvent
    1. 同2译秦。
    1. 由于此時(shí)手指已經(jīng)在屏幕豎直方向劃過(guò)一定距離峡捡,最頂層的ScrollView認(rèn)定這是一次上下滾動(dòng)的事件,在ListView調(diào)用requestDisallowInterceptTouchEvent獨(dú)占事件之前搶先一步在onInterceptTouchEvent中返回true攔截TouchEvent筑悴,成為了這次觸摸事件的新“主人”们拙,此時(shí)在下層的ViewPagerListView中收到了一個(gè)ACTION_CANCEL
    1. 之后所有的TouchEvent便直接進(jìn)入ScrollViewonTouchEvent阁吝,直到最后的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
    
    1. ACTION_DOWN,本次觸摸事件的開(kāi)始甲馋,此時(shí)為上一節(jié)情景三所述傳遞過(guò)程埂奈,ScrollViewViewPager定躏,ListView相繼在onInterceptTouchEvent返回true账磺,使觸摸事件一直能傳遞到最底層。此時(shí)ACTION_DOWN傳遞到ListView的子View時(shí)痊远,子View不需要處理觸摸事件垮抗,從而在onTouchEvent中返回了false,從而ACTION_DOWN返回到上一層進(jìn)入到了ListViewonTouchEvent中并返回了true碧聪,此時(shí)ListView成為了整個(gè)觸摸事件的“主人”冒版。
    1. 上一節(jié)情景四所述傳遞過(guò)程,ACTION_MOVE通過(guò)最短路徑進(jìn)入“主人”ListViewonTouchEvent中逞姿,并且不經(jīng)過(guò)ListViewonInterceptTouchEvent辞嗡。
    1. 同2。需要注意的是滞造,由于ScrollView不再能攔截事件欲间,手指劃過(guò)一定距離后,ListView認(rèn)定這是一次上下滾動(dòng)的事件断部,不希望之后的TouchEvent被父視圖攔截猎贴,所以在此時(shí)調(diào)用了requestDisallowInterceptTouchEvent
    1. 父視圖不再能攔截TouchEvent蝴光,所有TouchEvent直接進(jìn)入ListViewonTouchEvent中她渴,直到最后的ACTION_UP

5.實(shí)踐

考慮這樣的一個(gè)三層的視圖(忽略了一些無(wú)關(guān)緊要的層次):

screenshot
screenshot

ScrollView中含有一個(gè)TextViewViewPager蔑祟,其中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觸摸事件傳遞之后谊却,決定從onInterceptTouchEventonTouchEvent這兩個(gè)方法做做手腳,來(lái)實(shí)現(xiàn)這一需求哑芹。我的思路分為兩步:

  1. 對(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ò)程中切換ScrollViewListView的滑動(dòng)曹货,但是已經(jīng)能夠用兩次手指滑動(dòng)來(lái)切換了咆繁。

  2. 對(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);
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市综苔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌位岔,老刑警劉巖如筛,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異抒抬,居然都是意外死亡杨刨,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門擦剑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)妖胀,“玉大人,你說(shuō)我怎么就攤上這事惠勒∽眨” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵纠屋,是天一觀的道長(zhǎng)涂臣。 經(jīng)常有香客問(wèn)我,道長(zhǎng)售担,這世上最難降的妖魔是什么赁遗? 我笑而不...
    開(kāi)封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮族铆,結(jié)果婚禮上岩四,老公的妹妹穿的比我還像新娘。我一直安慰自己哥攘,他們只是感情好剖煌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著逝淹,像睡著了一般末捣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上创橄,一...
    開(kāi)封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天箩做,我揣著相機(jī)與錄音,去河邊找鬼妥畏。 笑死邦邦,一個(gè)胖子當(dāng)著我的面吹牛安吁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播燃辖,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鬼店,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了黔龟?” 一聲冷哼從身側(cè)響起妇智,我...
    開(kāi)封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎氏身,沒(méi)想到半個(gè)月后巍棱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛋欣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年航徙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陷虎。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡到踏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出尚猿,到底是詐尸還是另有隱情窝稿,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布凿掂,位于F島的核電站讹躯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏缠劝。R本人自食惡果不足惜潮梯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望惨恭。 院中可真熱鬧秉馏,春花似錦、人聲如沸脱羡。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)锉罐。三九已至帆竹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間脓规,已是汗流浹背栽连。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人秒紧。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓绢陌,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親熔恢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子脐湾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容