Android触摸事件传递分析与实践

设计UI时,亲爱的交互设计师们总会有一些天马行空的想法,大多数情况下原生的控件已不能支持这些“看似简单”的交互逻辑,需要继承ListViewViewPagerScrollView甚至直接继承View来自定义一些特性来支撑。在处理触摸事件时,无可避免的需要重写onInterceptTouchEventonTouchEvent这两个方法。本文将从源码的角度,从这两个棘手的函数为切入点,对触摸事件在View中的传递逻辑进行梳理。

1.概述

本文中只简单的考虑单指触摸事件。一次触摸事件通常有一系列TouchEvent组成,这一系列TouchEvent通常由一个ACTION_DOWN开始,并且由一个ACTION_UP/ACTION_CANCEL结束。这一系列TouchEvent都会自上而下传入视图结构,上层View根据自身需求决定是由自身来处理该事件,或者将其传入下一层视图处理。通常而言ViewGroup.onInterceptTouchEvent决定了父View是否拦截该触摸事件,而View.onTouchEvent中则实现了其自身如何处理该触摸事件。

  • ViewGroup.onInterceptTouchEvent

    public boolean onInterceptTouchEvent(MotionEvent ev);
    

    API 24对该方法的官方说明:

    实现该方法以拦截所有的屏幕触摸事件,从而使你能够监控触摸事件分发给子View的过程并且随时拦截。
    使用该方法时需小心谨慎,因为该方法与View.onTouchEvent的交互相当复杂,并且要正确的实现这两个方法。TouchEvent将会根据以下顺序被接收:

    1. 你将在这里接收到ACTION_DOWN
    2. ACTION_DOWN要么由一个子View来处理,要么由你自身的onTouchEvent来处理。后者意味着你应该实现onTouchEvent并返回true,你才能收到后续的TouchEvent(而不是由你的父View来处理);并且,当你在onTouchEvent中返回true时,你将不会在onInterceptTouchEvent中接收到后续的TouchEvent,但是仍然会正常的传递到onTouchEvent
    3. 如果你在此方法中返回false,那么本次触摸事件中所有后续的TouchEvent都会先传递到这里,然后传递到目标ViewonTouchEvent
    4. 如果你在此方法中返回true,本次触摸事件中所有后续的TouchEvent都不会再传递到此方法。原本的目标View将会接收到一个同样的TouchEvent(但是action为ACTION_CANCEL),之后的TouchEvent会传递到你自身的TouchEvent并且不再出现在此处

    onInterceptTouchEvent定义在ViewGroup中,intercept一词为拦截的意思。简而言之,该方法的用意为决定是否拦截该TouchEvent,如果该方法返回true表示拦截此TouchEvent,否则会向下传递到子View中。在ViewGroup中该方法直接返回true,继承于ViewGroup的控件根据自身需求自己实现。

  • View.onTouchEvent

    public boolean onTouchEvent(MotionEvent event)
    

    onTouchEvent定义在View中,该方法中实现了View处理触摸事件的真正过程,当TouchEvent传入视图并且决定由自身处理的时候,便会将其传入onTouchEvent。返回值true表示该TouchEvent被已被消费,相当于告诉别人“我是这次触摸事件的主人,我将会处理本次触摸事件”;返回false则表示未被消费,TouchEvent将会继续被传递寻找新的“主人”。在该方法中requestDisallowInterceptTouchEvent有会被调用,用以禁止父View拦截此次触摸事件中后续的TouchEvent,之后所有的TouchEvent将不会传递到父ViewonInterceptTouchEvent而直接传递到此处。

2.分发

ViewGroup.dispatchTouchEvent(MotionEvent ev)方法是触摸事件在视图结构中传递逻辑的主导者。该方法最初定义在View中(会调用onTouchEvent并返回是否消费),在ViewGroup中被重写。TouchEvent传入ViewGroupdispatchTouchEvent首先被调用以负责触摸事件在自身与子View之间的分发处理逻辑,并且通过返回值通知父View是否消费了TouchEventonInterceptTouchEventonTouchEvent都由其直接或间接被调用,多层视图结构通过一层层向下调用dispatchTouchEvent寻找触摸事件的“主人”。本节主要对以注释的形式对该方法源码进行分析以初步了解TouchEvent在视图结构中的分发过程。

//源码基于API Level 23,即Android 6.0
//省略了一些代码,着重分析单指触摸事件的传递过程。

//返回值为此view及子view是否handle该MotionEvent
public boolean dispatchTouchEvent(MotionEvent ev) {

    ......

    //如果是DOWN,作为触摸事件的开始,初始化
    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) {
            //正常的话,调用onInterceptTouchEvent来决定是否拦截该event
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); 
        } else {
            //如果有FLAG_DISALLOW_INTERCEPT标记,则不拦截event
            //一般当子view处理了事件,而不希望父容器截断时,会通过调用requestDisallowInterceptTouchEvent来给父容器设置该标记
            intercepted = false;
        }
    } else {
        //ACTION_DOWN为一次触摸事件的开始,ACTION_DOWN传递给子view之后,若有子view能handle,那么该子view即设置为touchTarget
        //如果event不为ACTION_DOWN,那么它是ACTION_DOWN之后一连串event之一,此时若没有目标touchTarget,说明并没有子view能handle此次事件(或者上一个TouchEvent被拦截导致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) {
                        
            //没有取消也没有拦截,并且为ACTION_DOWN,尝试找到一个能handle该事件的子view

            ......

            for(child in this ViewGroup){
                //遍历所有子view

                ......

                //跳过 无法接收事件 与 不在触摸位置 的子view
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
                            
                ......

                //此处dispatchTransformedTouchEvent的作用为,将event的坐标转换成该子view的坐标后,调用子view的dispatchTouchEvent
                //返回值为该子view是否handle该event
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

                    ......

                    //如果子view能够handle该event,则将该子view设置为touchTarget,并设置标记表示找到了target
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
            }
        }
    }
            
    if (mFirstTouchTarget == null) {
        //到这里touchTarget为null有以下几种情况:
        //1.某次触摸事件最初的ACTION_DOWN被拦截或者没有目标handle,致使此次事件所有的event都会走到这里;
        //2.某次触摸事件最初的ACTION_DOWN被目标handle,而中途被拦截,此时touchTarget不会null,但是会在下面的代码中被清空,从而使之后的event走到这里;

        //注意此时调用dispatchTransformedTouchEvent的第三个参数child为null
        //在dispatchTransformedTouchEvent中可以看到child==null时会调用到super.dispatchTouchEvent,也就是View.dispatchTouchEvent,从而调用到onTouchEvent
        //也就是说,将此ViewGroup试做一个普通的View,由其自身来处理该事件
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        //走到这里说明touchTarget!=null
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            //循环遍历所有的touchTarget,通常单指触摸事件只有一个touchTarget
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                //如果该event已经在上面寻找target的代码中已经分发给该view过了,则直接将handled置为true,然后跳过
                handled = true;
            } else {
                //走到这里,说明可定不是ACTION_DOWN了
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                //如果cancelChild为false,那么将TouchEvent的坐标转换后传递给子View
                //如果intercepted为true说明上面决定要拦截该event,那么cancelChild为true,将会传递一个同样的但是为ACTION_CANCEL的touchEvent给子View
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                    //子View是否消费TouchEvent决定了handled的值
                }
                if (cancelChild) {
                    //如果cancelChild,那么循环清空所有的touchTarget,接下来的所有TouchEvent都将有自身的onTouchEvent来处理
                    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是否消费此TouchEvent
    return handled;
}

流程示意图

分发过程示意图
分发过程示意图

概括来讲,ACTION_DOWN的分发过程对于整个触摸事件来讲是相当重要的,而dispatchTouchEvent就是为ACTION_DOWN寻找“主人”的一个过程,如果找到了则返回trueViewGroup.onInterceptTouchEvent在分发ACTION_DOWN时,如果intercepted = false,便会向下传递寻找有没有子视图能做这次事件的“主人”。如果intercepted = true,或者在子视图中没有找到“主人”,那么就将其本身视为一个普通的View来调用onTouchEvent来处理。如果有子视图或者其自身能handled,那么就向上返回true表示“爸爸,我找到它的主人了”。

ACTION_MOVE进入dispatchTouchEvent时,如果之前在子视图中找到了“主人”就直接将其传递至目标,否则就将其本身视为一个普通的View来调用onTouchEvent来处理。如果intercepted = true则给之前的“主人”传递一个ACTION_CANCEL,同时清空目标,那么之后进入的TouchEvent将会被自身来处理。

3.传递

至此为止,本文主要在横向地分析TouchEventViewGroup中的分发过程,而在开发过程中,通常我们更多需要关注的是TouchEvent在视图层次中纵向的传递过程。基于以上对于TouchEvent分发过程的分析,可以很清晰地整理出纵向传递的逻辑(本节的分析过程基于一个四层的视图结构,上方三层为ViewGroup,最底层为普通的View):

  • 情景一
    对于初始的ACTION_DOWN,通常情况下ViewGroup并不能马上去拦截,因为一旦拦截,就意味着该ViewGroup下的任何子视图都不会收到任何触摸事件。在这样的前提下,TouchEvent传入某一层ViewGroup后,dispatchTouchEvent通过调用onInterceptTouchEvent(返回false)得知无需拦截,那么便会通过调用下一层视图的dispatchTouchEvent来讲TouchEvent传递至下一层。底层ViewdispatchTouchEvent将直接调用onTouchEvent(返回true),于是dispatchTouchEvent通过一层层向上返回true表示找到了本次触摸事件的目标。

    流程示意图
    此情景可以用下图简单的描述(以下图中,实现表示方法调用,虚线表示方法返回值,标号表示发生时序)。

    情景一传递示意图
    情景一传递示意图

    简化流程图
    大部分情况,我们自定义控件时无需关心dispatchTouchEvent的实现,也不用关心方法之间的调用关系,而只需要关注onInterceptTouchEventonTouchEvent的实现与返回值来影响触摸事件的传递,从而满足自身的需求。在这样的前提下,以上流程图可以简化为下图的形式(实现仅表示TouchEvent的传递方向)。

    情景一传递简化图
    情景一传递简化图

  • 情景二
    在情景一的前提下,如果不出其他幺蛾子的话,此次触摸事件中后续的TouchEvent都会以相同的路径向下传递。但是如果对于其中某一个TouchEvent,Level 1的ViewGrouponInterceptTouchEvent返回了true,那么根据上一节的分析,ViewGroup首先会沿原路径向下传递一个ACTION_CANCEL,并且之后所有的TouchEvent都将会直接传递到其自身onTouchEvent中处理,因为此时该ViewGroup自身已成为本次触摸事件的新“主人”。

    简化流程图

    情景二传递简化图
    情景二传递简化图

  • 情景三
    在情景一的基础上,ACTION_DOWN时,如果底层ViewonTouchEvent中返回了false,那么dispatchTouchEvent就会返回给上层ViewGroupfalse来表示其并不能处理本次触摸事件,那么上层ViewGroup便会调用自身的onTouchEvent并通过dispatchTouchEvent将返回值向上传递,直到找到触摸事件的”主人“。

    流程示意图

    情景三传递示意图
    情景三传递示意图

    简化流程图

    情景三传递简化图
    情景三传递简化图

  • 情景四
    在情景三的前提下,触摸事件的后续TouchEvent将会沿最短路径直接传递给目标,而不再按照ACTION_DOWN时的路径走到最底层。需要注意的是,由于TouchEvent由Level 2的ViewGroup自身来处理而不是子视图,此时应将其视为一个普通的ViewTouchEvent将直接进入其onTouchEvent而不再先进入onInterceptTouchEvent

    简化流程图

    情景四传递简化图
    情景四传递简化图

  • 情景五
    将情景一与情景二结合一下,ACTION_DOWN时,如果某一层ViewGrouponInterceptTouchEvent时返回true,那么TouchEvent将直接传递到其自身onTouchEvent,之后根据其返回值依照上面所述逻辑继续传递。

    流程示意图

    情景五传递示意图
    情景五传递示意图

    简化流程图

    情景五传递简化图
    情景五传递简化图

  • 情景六
    有些情况下,比如ListViewScrollView处理滑动事件时,当其希望对整个触摸事件完全掌控而不希望父视图拦截时,会通过调用requestDisallowInterceptTouchEvent循环通知各层父视图不要拦截之后的TouchEvent,这时之后的所有TouchEvent将不再传递到所有父视图的onInterceptTouchEvent而直接传递到该View

    流程示意图

    情景六传递示意图
    情景六传递示意图

4.样例

本节以两个具体样例来协助理解上述纵向传递过程。

  • 样例一
    考虑这样的一个三层视图结构:从上到下依次为ScrollViewViewPagerListView。如果不做任何处理,那么手指在屏幕上下滑动将会是以下的一个处理过程:

    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,本次触摸事件的开始,此时为上一节情景三所述传递过程,ScrollViewViewPagerListView相继在onInterceptTouchEvent返回true,使触摸事件一直能传递到最底层。此时ACTION_DOWN传递到ListView的子View时,子View不需要处理触摸事件,从而在onTouchEvent中返回了false,从而ACTION_DOWN返回到上一层进入到了ListViewonTouchEvent中并返回了true,此时ListView成为了整个触摸事件的“主人”。
    1. 上一节情景四所述传递过程,ACTION_MOVE通过最短路径进入“主人”ListViewonTouchEvent中,并且不经过ListViewonInterceptTouchEvent
    1. 同2。
    1. 由于此时手指已经在屏幕竖直方向划过一定距离,最顶层的ScrollView认定这是一次上下滚动的事件,在ListView调用requestDisallowInterceptTouchEvent独占事件之前抢先一步在onInterceptTouchEvent中返回true拦截TouchEvent,成为了这次触摸事件的新“主人”,此时在下层的ViewPagerListView中收到了一个ACTION_CANCEL
    1. 之后所有的TouchEvent便直接进入ScrollViewonTouchEvent,直到最后的ACTION_UP
  • 样例二
    本例基于样例一的模型,但是对ScrollView进行处理,使其永远在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,本次触摸事件的开始,此时为上一节情景三所述传递过程,ScrollViewViewPagerListView相继在onInterceptTouchEvent返回true,使触摸事件一直能传递到最底层。此时ACTION_DOWN传递到ListView的子View时,子View不需要处理触摸事件,从而在onTouchEvent中返回了false,从而ACTION_DOWN返回到上一层进入到了ListViewonTouchEvent中并返回了true,此时ListView成为了整个触摸事件的“主人”。
    1. 上一节情景四所述传递过程,ACTION_MOVE通过最短路径进入“主人”ListViewonTouchEvent中,并且不经过ListViewonInterceptTouchEvent
    1. 同2。需要注意的是,由于ScrollView不再能拦截事件,手指划过一定距离后,ListView认定这是一次上下滚动的事件,不希望之后的TouchEvent被父视图拦截,所以在此时调用了requestDisallowInterceptTouchEvent
    1. 父视图不再能拦截TouchEvent,所有TouchEvent直接进入ListViewonTouchEvent中,直到最后的ACTION_UP

5.实践

考虑这样的一个三层的视图(忽略了一些无关紧要的层次):

screenshot
screenshot

ScrollView中含有一个TextViewViewPager,其中ViewPager的高度与ScrollView的高度一致,而在ViewPager的某一页为一个同等大小的ListView,通过在onMeasure中作一些必要的处理从而将整个视图完整的显示之后,会发现ListView完全无法滚动。而这个视图结构应该挺常见,交互的需求应该更常见:手指向上滑动时,先滚动ScrollView,滚动到底后再滚动ListView;手指向下滑动时,先滚动ListView,滚动到底后再滚动ScrollView

首先对于这个需求,相信大家会首先想到API 21推出的NestedScroll。在学习了Android触摸事件传递之后,决定从onInterceptTouchEventonTouchEvent这两个方法做做手脚,来实现这一需求。我的思路分为两步:

  1. onInterceptTouchEvent做手脚。手指向上滑动时,当ScrollView滑动到边界时,onInterceptTouchEvent返回false,将事件交由ListView处理,使ListView能够滑动;手指向下滑动时,如果ListView能够滑动,就在onInterceptTouchEvent中返回false,让ListView优先滑动。这样下来,虽然还无法在一次手指滑动过程中切换ScrollViewListView的滑动,但是已经能够用两次手指滑动来切换了。

  2. onTouchEvent做手脚。手指向上滑动时,当ScrollView滑动到边界时,首先分发一个ACTION_CANCEL表示此次触摸事件已结束,同时马上再分发一个ACTION_DOWN表示新一次触摸事件开始,这时通过上一步onInterceptTouchEvent中做的手脚就将滑动切换到了ListView,为了达到目的不择手段地强行将一次触摸事件拆分为两个;手指向下滑动时,当ListView滑动到边界时,通知最顶层的ScrollView分发两个新事件来进行强拆。

自定义ScrollView

    //记录触摸起始位置的Y坐标
    private float downY;

    //是否有子视图正在被拖动的标记
    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){
                    //如果没有子视图正在被拖动

                    float deltY = ev.getY() - downY;
                    if(Math.abs(deltY) > touchSlop && callback != null && callback.canChildScroll(0-(int) deltY)){
                        //滑动距离已经可判定为上下滑动事件,并且通过回调得知子视图在该方向上能够滑动

                        if((deltY < 0 && !canScrollVertically(0-(int) deltY))
                                || (deltY > 0)){
                            //deltY < 0 为手指向上滑动,此时自身已不能向上滑动,则不拦截交由子视图处理
                            //deltY > 0 为手指向下滑动,子视图还能向下滑动,则优先交由子视图滑动

                            isChildBeingDragged = true;
                            return false;
                        }
                    }
                    //其他情况则正常处理
                    return super.onInterceptTouchEvent(ev);
                }

                //如果有子视图正在被拖动,则不拦截事件
                return false;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                isChildBeingDragged = false;
                break;
        }

        return super.onInterceptTouchEvent(ev);
    }

    //记录上一次TouchEvent的Y坐标
    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){
                    //如果手指向上滑动,并且算上当前deltY之后已超出最大可滑动距离

                    //在最大滑动距离对应处分发一个ACTION_UP
                    ev.setLocation(ev.getX(), lastY - scrollRange + getScrollY());
                    super.onTouchEvent(ev);
                    ev.setAction(MotionEvent.ACTION_UP);
                    dispatchTouchEvent(ev);

                    //在相同位置分发一个ACTION_DOWN  
                    ev.setAction(MotionEvent.ACTION_DOWN);
                    dispatchTouchEvent(ev);

                    //加上剩余的距离后分发一个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

    //此处不添加注释了,道理与上面相当

    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());  //这里注意需要通知最上层的视图来分发TouchEvent,而不是自己分发
                    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);
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容