设计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`将不会传递到父`View`的`onInterceptTouchEvent`而直接传递到此处。

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来决定是否拦截该eventintercepted = 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......//跳过 无法接收事件 与 不在触摸位置 的子viewif (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}......//此处dispatchTransformedTouchEvent的作用为,将event的坐标转换成该子view的坐标后,调用子view的dispatchTouchEvent//返回值为该子view是否handle该eventif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {......//如果子view能够handle该event,则将该子view设置为touchTarget,并设置标记表示找到了targetnewTouchTarget = 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!=nullTouchTarget predecessor = null;TouchTarget target = mFirstTouchTarget;while (target != null) {//循环遍历所有的touchTarget,通常单指触摸事件只有一个touchTargetfinal 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给子Viewif (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是否消费此TouchEventreturn handled;
}

3.分析

通过上面对源码的分析,本节主要详细梳理一次常规的单指触摸事件(以ACTION_DOWN开始,并以ACTION_UP结束,中间全为ACTION_MOVE)在一个ViewGroup中的分发处理过程。

  • ACTION_DOWN

    1. 作为触摸事件的开始,初始化,进入下一步
    2. 如果有子View调用requestDisallowInterceptTouchEvent,则interceptedfalse,进入第3步;否则进入下一步
    3. 调用onInterceptTouchEvent,返回值赋予intercepted,进入下一步
    4. 如果interceptedfalse,进入下一步;否则进入第5步;
    5. 遍历所有子View找到在触摸位置的View,将坐标转换后调用其dispatchTouchEvent,将第一个返回true的子View作为触摸事件的Target,进入下一步
    6. 如果Target为空,进入下一步;否则进入第7步
    7. 没有Target,则将TouchEvent交由自身的onTouchEvent处理,返回值赋予handled,进入第8步
    8. Target,则将handled置为true,以通知父视图“我将处理这此触摸事件”,进入下一步
    9. 返回handled
  • ACTION_MOVE/ACTION_UP/ACTION_CANCEL

    1. 如果Target为空,intercepted置为true,进入第4步;否则进入下一步
    2. 如果有子View调用requestDisallowInterceptTouchEvent,则interceptedfalse,进入第4步;否则进入下一步
    3. 调用onInterceptTouchEvent,返回值赋予intercepted,进入下一步
    4. 如果Target为空,进入下一步;否则进入第6步
    5. 没有Target,调用自身onTouchEvent,返回值赋予handled
    6. Target。如果interceptedfalse,进入下一步;否则进入第8步
    7. 调用TargetdispatchTouchEvent,返回值赋予handled,进入第9步
    8. 调用TargetdispatchTouchEvent并传入一个ACTION_CANCELhandled置为true,清空Target,进入下一步
    9. 返回handled

流程示意图

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

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

4.传递

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

  • 情景一

    对于初始的`ACTION_DOWN`,通常情况下`ViewGroup`并不能马上去拦截,因为一旦拦截,就意味着该`ViewGroup`下的任何子视图都不会收到任何触摸事件。在这样的前提下,`TouchEvent`传入某一层`ViewGroup`后,`dispatchTouchEvent`通过调用`onInterceptTouchEvent`(返回`false`)得知无需拦截,那么便会通过调用下一层视图的`dispatchTouchEvent`来讲`TouchEvent`传递至下一层。底层`View`的`dispatchTouchEvent`将直接调用`onTouchEvent`(返回true),于是`dispatchTouchEvent`通过一层层向上返回`true`表示找到了本次触摸事件的目标。  *流程示意图*
    此情景可以用下图简单的描述(以下图中,实现表示方法调用,虚线表示方法返回值,标号表示发生时序)。
    ![情景一传递示意图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/a52b1dbe431adc221b923b69cb97c552)*简化流程图*
    大部分情况,我们自定义控件时无需关心`dispatchTouchEvent`的实现,也不用关心方法之间的调用关系,而只需要关注`onInterceptTouchEvent`与`onTouchEvent`的实现与返回值来影响触摸事件的传递,从而满足自身的需求。在这样的前提下,以上流程图可以简化为下图的形式(实现仅表示`TouchEvent`的传递方向)。
    ![情景一传递简化图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/a1cbae1213e0a2d3c1c20a9dbe7b7eac)
    
  • 情景二

    在情景一的前提下,如果不出其他幺蛾子的话,此次触摸事件中后续的`TouchEvent`都会以相同的路径向下传递。但是如果对于其中某一个`TouchEvent`,Level 1的`ViewGroup`在`onInterceptTouchEvent`返回了`true`,那么根据上一节的分析,`ViewGroup`首先会沿原路径向下传递一个`ACTION_CANCEL`,并且之后所有的`TouchEvent`都将会直接传递到其自身`onTouchEvent`中处理,因为此时该`ViewGroup`自身已成为本次触摸事件的新“主人”。  *简化流程图*
    ![情景二传递简化图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/3d0dd1cf60c73b89154f61249f21a133)
    
  • 情景三

    在情景一的基础上,`ACTION_DOWN`时,如果底层`View`在`onTouchEvent`中返回了false,那么`dispatchTouchEvent`就会返回给上层`ViewGroup`值`false`来表示其并不能处理本次触摸事件,那么上层`ViewGroup`便会调用自身的`onTouchEvent`并通过`dispatchTouchEvent`将返回值向上传递,直到找到触摸事件的”主人“。*流程示意图*
    ![情景三传递示意图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/2af05c5a630dfd00bab8bb6339facdce)  *简化流程图*
    ![情景三传递简化图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/3fa1d6eca8e990337d1cdb9fadaf49be)
    
  • 情景四

    在情景三的前提下,触摸事件的后续`TouchEvent`将会沿最短路径直接传递给目标,而不再按照`ACTION_DOWN`时的路径走到最底层。需要注意的是,由于`TouchEvent`由Level 2的`ViewGroup`自身来处理而不是子视图,此时应将其视为一个普通的`View`,`TouchEvent`将直接进入其`onTouchEvent`而不再先进入`onInterceptTouchEvent`。   *简化流程图*
    ![情景四传递简化图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/a9e5c45a5ff531fce6651a1988199686)
    
  • 情景五

    将情景一与情景二结合一下,`ACTION_DOWN`时,如果某一层`ViewGroup`在`onInterceptTouchEvent`时返回true,那么`TouchEvent`将直接传递到其自身`onTouchEvent`,之后根据其返回值依照上面所述逻辑继续传递。  *流程示意图*
    ![情景五传递示意图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/199641ee91105fa7bc2e3480db8901a6)  *简化流程图*
    ![情景五传递简化图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/f8c8bada84cff586c62b1e5687cc992f)
    
  • 情景六

    有些情况下,比如`ListView`与`ScrollView`处理滑动事件时,当其希望对整个触摸事件完全掌控而不希望父视图拦截时,会通过调用requestDisallowInterceptTouchEvent循环通知各层父视图不要拦截之后的`TouchEvent`,这时之后的所有`TouchEvent`将不再传递到所有父视图的`onInterceptTouchEvent`而直接传递到该`View`。*流程示意图*
    ![情景六传递示意图](http://ata2-img.cn-hangzhou.img-pub.aliyun-inc.com/3d45b68dd5858c1dca6a0ad4f3011b5c)
    

5.样例

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

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

    1.  10-25 19:43:37.984  ScrollView  onInterceptTouchEvent :  Action Down    x : 839.0 y : 1340.010-25 19:43:37.984  ViewPager   onInterceptTouchEvent :  Action Down    x : 839.0 y : 996.010-25 19:43:37.984  ListView    onInterceptTouchEvent :  Action Down    x : 839.0 y : 996.010-25 19:43:37.984  ListView    onTouchEvent          :  Action Down    x : 839.0 y : 996.02.  10-25 19:43:37.994  ScrollView  onInterceptTouchEvent :  Action Move    x : 839.0 y : 1340.010-25 19:43:37.994  ViewPager   onInterceptTouchEvent :  Action Move    x : 839.0 y : 996.010-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.564210-25 19:43:38.164  ViewPager   onInterceptTouchEvent :  Action Move    x : 845.0 y : 933.564210-25 19:43:38.164  ListView    onTouchEvent          :  Action Move    x : 845.0 y : 933.56424.  10-25 19:43:38.184  ScrollView  onInterceptTouchEvent :  Action Move    x : 846.0 y : 1265.316910-25 19:43:38.184  ViewPager   onInterceptTouchEvent :  Action Up/Cancel   x : 846.0 y : 1265.316910-25 19:43:38.184  ListView    onTouchEvent          :  Action Up/Cancel   x : 846.0 y : 1265.31695.  10-25 19:43:38.214  ScrollView  onTouchEvent          :  Action Move    x : 847.0 y : 1237.81696.  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.29438.  10-25 19:43:38.484  ScrollView  onTouchEvent          :  Action Up/Cancel   x : 859.43677 y : 1065.0692
>1. `ACTION_DOWN`,本次触摸事件的开始,此时为上一节情景三所述传递过程,`ScrollView`,`ViewPager`,`ListView`相继在`onInterceptTouchEvent`返回true,使触摸事件一直能传递到最底层。此时`ACTION_DOWN`传递到`ListView`的子`View`时,子`View`不需要处理触摸事件,从而在`onTouchEvent`中返回了false,从而`ACTION_DOWN`返回到上一层进入到了`ListView`的`onTouchEvent`中并返回了true,此时`ListView`成为了整个触摸事件的“主人”。>2. 上一节情景四所述传递过程,`ACTION_MOVE`通过最短路径进入“主人”`ListView`的`onTouchEvent`中,并且不经过`ListView`的`onInterceptTouchEvent`。>3. 同2。>4. 由于此时手指已经在屏幕竖直方向划过一定距离,最顶层的`ScrollView`认定这是一次上下滚动的事件,在`ListView`调用`requestDisallowInterceptTouchEvent`独占事件之前抢先一步在`onInterceptTouchEvent`中返回true拦截`TouchEvent`,成为了这次触摸事件的新“主人”,此时在下层的`ViewPager`与`ListView`中收到了一个`ACTION_CANCEL`。>5. 之后所有的`TouchEvent`便直接进入`ScrollView`的`onTouchEvent`,直到最后的`ACTION_UP`。
  • 样例二
    本例基于样例一的模型,但是对ScrollView进行处理,使其永远在onInterceptTouchEvent中返回false

    1.  10-25 19:43:38.484  ScrollView  onInterceptTouchEvent :  Action Down    x : 859.43677 y : 1065.069210-25 19:43:38.484  ViewPager   onInterceptTouchEvent :  Action Down    x : 859.43677 y : 921.069210-25 19:43:38.484  ListView    onInterceptTouchEvent :  Action Down    x : 859.43677 y : 921.069210-25 19:43:38.484  ListView    onTouchEvent          :  Action Down    x : 859.43677 y : 921.06922.  10-25 19:43:38.484  ScrollView  onInterceptTouchEvent :  Action Move    x : 859.43677 y : 1062.294310-25 19:43:38.484  ViewPager   onInterceptTouchEvent :  Action Move    x : 859.43677 y : 918.294310-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.210810-25 19:43:38.564  ViewPager   onInterceptTouchEvent :  Action Move    x : 867.7982 y : 841.210810-25 19:43:38.564  ListView    onTouchEvent          :  Action Move    x : 867.7982 y : 841.21084.  10-25 19:43:38.584  ListView    onTouchEvent          :  Action Move    x : 869.28864 y : 823.24775.  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.07.  10-25 19:43:40.334  ListView    onTouchEvent          :  Action Up/Cancel   x : 826.0 y : 1562.0
>1. `ACTION_DOWN`,本次触摸事件的开始,此时为上一节情景三所述传递过程,`ScrollView`,`ViewPager`,`ListView`相继在`onInterceptTouchEvent`返回true,使触摸事件一直能传递到最底层。此时`ACTION_DOWN`传递到`ListView`的子`View`时,子`View`不需要处理触摸事件,从而在`onTouchEvent`中返回了false,从而`ACTION_DOWN`返回到上一层进入到了`ListView`的`onTouchEvent`中并返回了true,此时`ListView`成为了整个触摸事件的“主人”。>2. 上一节情景四所述传递过程,`ACTION_MOVE`通过最短路径进入“主人”`ListView`的`onTouchEvent`中,并且不经过`ListView`的`onInterceptTouchEvent`。>3. 同2。需要注意的是,由于`ScrollView`不再能拦截事件,手指划过一定距离后,`ListView`认定这是一次上下滚动的事件,不希望之后的`TouchEvent`被父视图拦截,所以在此时调用了`requestDisallowInterceptTouchEvent`。>4. 父视图不再能拦截`TouchEvent`,所有`TouchEvent`直接进入`ListView`的`onTouchEvent`中,直到最后的`ACTION_UP`。

6.实践

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

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;@Overridepublic 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;@Overridepublic 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_UPev.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_MOVEev.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;@Overridepublic 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;@Overridepublic 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);}

Android触摸事件传递分析与实践相关推荐

  1. android方向触摸事件,Android触摸事件传递机制,这一篇就够了

    整个触摸事件牵涉到的是,Activity,View,ViewGroup三者的传递机制. 这个触摸事件就是从外层往内层一层层的传递. 整个传递机制,分为3个步骤:分发,拦截,和消费. 1. 触摸事件的类 ...

  2. Android之Android触摸事件传递机制

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/53431274 本文出自:[顾林海的博客] ##前言 关于Android ...

  3. Android 触摸事件传递机制

    android系统中的每个View的子类都具有下面三个和TouchEvent处理密切相关的方法: 1)public boolean dispatchTouchEvent(MotionEvent ev) ...

  4. Android笔记:触摸事件的分析与总结----多点触控

       其他相关博文:    Android笔记:触摸事件的分析与总结----MotionEvent对象    Android笔记:触摸事件的分析与总结----TouchEvent处理机制     An ...

  5. Android面试必问之触摸事件传递机制

    Android面试必问之触摸事件传递机制 一.Activity的构成 二.触摸事件的类型 三.事件传递的三个阶段 Activity对点击事件的分发过程 五.View的事件分发机制 六.点击事件分发的传 ...

  6. android触摸事件分发,Android 事件分发机制

    Android 事件分发机制一直让人头痛,之前也是面向 GitHub 编程得过且过.今天下定决心了解一下,以便后面自己定制 View 效果.Android 触摸事件有三个基本类型:ACTION_DOW ...

  7. 为了讲清楚Android触摸事件,我“拆了部手机”

    Android 是一个有用户界面(GUI)的操作系统,在它诞生之初,就是为带有触摸屏的手持设备准备的.作为提供给用户最重要的交互方式之一,了解触摸系统是怎么工作的,对于实际的项目开发有着非常大的帮助. ...

  8. Android Touch事件传递机制 二:单纯的(伪生命周期) 这个清楚一点

    转载于:http://blog.csdn.net/yuanzeyao/article/details/38025165 在前一篇文章中,我主要讲解了Android源码中的Touch事件的传递过程,现在 ...

  9. Android Touch事件传递机制 二:单纯的(伪生命周期)

    转载于:http://blog.csdn.net/yuanzeyao/article/details/38025165 在前一篇文章中,我主要讲解了Android源码中的Touch事件的传递过程,现在 ...

最新文章

  1. 聊聊数据库中的那些锁
  2. fir.im 全名 Fly It Remotely ,是一个为移动开发者服务,FIR一个免费的App托管平台
  3. 树状数组(单点+区间的所有操作)
  4. 27.用zxing生成二维码
  5. 信息学奥赛一本通(1078:求分数序列和)
  6. Python风格总结:数据类型
  7. 镜像资源的使用:100倍速度提升不是梦!
  8. 基于C51单片机的锂电池容量检测仪电压电流检测 原理图PCB程序设计
  9. Excel怎么求和?5大常用的Excel求和公式
  10. 小红书数据分析工具丨借助数据教你分分钟锁定优质达人
  11. tensorflow离线安装
  12. 记一次流量分析实战——安恒科技(八月ctf)
  13. linux v4l2架构分析之v4l2_ctrl_handler初始化及添加v4l2_ctrl的过程分析
  14. TXSQL:云计算时代数据库核弹头——云+未来峰会开发者专场回顾 1
  15. Xmanager远程桌面linux,Windows下通过Xmanager远程桌面控制Linux
  16. POJ-3426-0-1背包Charm Bracelet
  17. references column 'xxx' which is not in SELECT list
  18. 2021-10-06 求1到100以内的质数
  19. 2003 文件服务器权限设置,win2003服务器设置文件访问权限
  20. 火山android端 4.7.0 版本,火山小视频 v4.7.0 安卓版

热门文章

  1. 1.8 深入理解Surface系统
  2. Synchronization 同步
  3. jquery回弹_jQuery实现移动端下拉展现新的内容回弹动画
  4. 如何让python进程常驻内存_常驻内存程序--python+rrd监控cpu
  5. 在Windows Media Player 12中创建自定义播放列表
  6. c++ 实现时间片轮转调度算法
  7. Linux统计文件个数
  8. nginx设置图片跨域访问
  9. 屌丝程序猿对一个产品的思考
  10. 惠普c7000服务器装系统,C7000刀片系统安装实施方案.docx