设计UI时,亲爱的交互设计师们总会有一些天马行空的想法,大多数情况下原生的控件已不能支持这些“看似简单”的交互逻辑,需要继承ListView
、ViewPager
、ScrollView
甚至直接继承View来自定义一些特性来支撑。在处理触摸事件时,无可避免的需要重写onInterceptTouchEvent
与onTouchEvent
这两个方法。本文将从源码的角度,从这两个棘手的函数为切入点,对触摸事件在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
将会根据以下顺序被接收:- 你将在这里接收到
ACTION_DOWN
-
ACTION_DOWN
要么由一个子View来处理,要么由你自身的onTouchEvent
来处理。后者意味着你应该实现onTouchEvent
并返回true
,你才能收到后续的TouchEvent
(而不是由你的父View
来处理);并且,当你在onTouchEvent
中返回true
时,你将不会在onInterceptTouchEvent
中接收到后续的TouchEvent
,但是仍然会正常的传递到onTouchEvent
中 - 如果你在此方法中返回
false
,那么本次触摸事件中所有后续的TouchEvent
都会先传递到这里,然后传递到目标View
的onTouchEvent
中 - 如果你在此方法中返回
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
传入ViewGroup
后dispatchTouchEvent
首先被调用以负责触摸事件在自身与子View
之间的分发处理逻辑,并且通过返回值通知父View
是否消费了TouchEvent
。onInterceptTouchEvent
与onTouchEvent
都由其直接或间接被调用,多层视图结构通过一层层向下调用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
寻找“主人”的一个过程,如果找到了则返回true
。ViewGroup.onInterceptTouchEvent
在分发ACTION_DOWN
时,如果intercepted = false
,便会向下传递寻找有没有子视图能做这次事件的“主人”。如果intercepted = true
,或者在子视图中没有找到“主人”,那么就将其本身视为一个普通的View
来调用onTouchEvent
来处理。如果有子视图或者其自身能handled,那么就向上返回true
表示“爸爸,我找到它的主人了”。
ACTION_MOVE
进入dispatchTouchEvent
时,如果之前在子视图中找到了“主人”就直接将其传递至目标,否则就将其本身视为一个普通的View
来调用onTouchEvent
来处理。如果intercepted = true
则给之前的“主人”传递一个ACTION_CANCEL
,同时清空目标,那么之后进入的TouchEvent
将会被自身来处理。
3.传递
至此为止,本文主要在横向地分析TouchEvent
在ViewGroup
中的分发过程,而在开发过程中,通常我们更多需要关注的是TouchEvent
在视图层次中纵向的传递过程。基于以上对于TouchEvent
分发过程的分析,可以很清晰地整理出纵向传递的逻辑(本节的分析过程基于一个四层的视图结构,上方三层为ViewGroup
,最底层为普通的View
):
-
情景一
对于初始的ACTION_DOWN
,通常情况下ViewGroup
并不能马上去拦截,因为一旦拦截,就意味着该ViewGroup
下的任何子视图都不会收到任何触摸事件。在这样的前提下,TouchEvent
传入某一层ViewGroup
后,dispatchTouchEvent
通过调用onInterceptTouchEvent
(返回false
)得知无需拦截,那么便会通过调用下一层视图的dispatchTouchEvent
来讲TouchEvent
传递至下一层。底层View
的dispatchTouchEvent
将直接调用onTouchEvent
(返回true),于是dispatchTouchEvent
通过一层层向上返回true
表示找到了本次触摸事件的目标。流程示意图
此情景可以用下图简单的描述(以下图中,实现表示方法调用,虚线表示方法返回值,标号表示发生时序)。
简化流程图
大部分情况,我们自定义控件时无需关心dispatchTouchEvent
的实现,也不用关心方法之间的调用关系,而只需要关注onInterceptTouchEvent
与onTouchEvent
的实现与返回值来影响触摸事件的传递,从而满足自身的需求。在这样的前提下,以上流程图可以简化为下图的形式(实现仅表示TouchEvent
的传递方向)。
-
情景二
在情景一的前提下,如果不出其他幺蛾子的话,此次触摸事件中后续的TouchEvent
都会以相同的路径向下传递。但是如果对于其中某一个TouchEvent
,Level 1的ViewGroup
在onInterceptTouchEvent
返回了true
,那么根据上一节的分析,ViewGroup
首先会沿原路径向下传递一个ACTION_CANCEL
,并且之后所有的TouchEvent
都将会直接传递到其自身onTouchEvent
中处理,因为此时该ViewGroup
自身已成为本次触摸事件的新“主人”。简化流程图
-
情景三
在情景一的基础上,ACTION_DOWN
时,如果底层View
在onTouchEvent
中返回了false,那么dispatchTouchEvent
就会返回给上层ViewGroup
值false
来表示其并不能处理本次触摸事件,那么上层ViewGroup
便会调用自身的onTouchEvent
并通过dispatchTouchEvent
将返回值向上传递,直到找到触摸事件的”主人“。流程示意图
简化流程图
-
情景四
在情景三的前提下,触摸事件的后续TouchEvent
将会沿最短路径直接传递给目标,而不再按照ACTION_DOWN
时的路径走到最底层。需要注意的是,由于TouchEvent
由Level 2的ViewGroup
自身来处理而不是子视图,此时应将其视为一个普通的View
,TouchEvent
将直接进入其onTouchEvent
而不再先进入onInterceptTouchEvent
。简化流程图
-
情景五
将情景一与情景二结合一下,ACTION_DOWN
时,如果某一层ViewGroup
在onInterceptTouchEvent
时返回true,那么TouchEvent
将直接传递到其自身onTouchEvent
,之后根据其返回值依照上面所述逻辑继续传递。流程示意图
简化流程图
-
情景六
有些情况下,比如ListView
与ScrollView
处理滑动事件时,当其希望对整个触摸事件完全掌控而不希望父视图拦截时,会通过调用requestDisallowInterceptTouchEvent循环通知各层父视图不要拦截之后的TouchEvent
,这时之后的所有TouchEvent
将不再传递到所有父视图的onInterceptTouchEvent
而直接传递到该View
。流程示意图
4.样例
本节以两个具体样例来协助理解上述纵向传递过程。
-
样例一
考虑这样的一个三层视图结构:从上到下依次为ScrollView
,ViewPager
,ListView
。如果不做任何处理,那么手指在屏幕上下滑动将会是以下的一个处理过程: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
,本次触摸事件的开始,此时为上一节情景三所述传递过程,ScrollView
,ViewPager
,ListView
相继在onInterceptTouchEvent
返回true,使触摸事件一直能传递到最底层。此时ACTION_DOWN
传递到ListView
的子View
时,子View
不需要处理触摸事件,从而在onTouchEvent
中返回了false,从而ACTION_DOWN
返回到上一层进入到了ListView
的onTouchEvent
中并返回了true,此时ListView
成为了整个触摸事件的“主人”。
- 上一节情景四所述传递过程,
ACTION_MOVE
通过最短路径进入“主人”ListView
的onTouchEvent
中,并且不经过ListView
的onInterceptTouchEvent
。
- 同2。
- 由于此时手指已经在屏幕竖直方向划过一定距离,最顶层的
ScrollView
认定这是一次上下滚动的事件,在ListView
调用requestDisallowInterceptTouchEvent
独占事件之前抢先一步在onInterceptTouchEvent
中返回true拦截TouchEvent
,成为了这次触摸事件的新“主人”,此时在下层的ViewPager
与ListView
中收到了一个ACTION_CANCEL
。
- 之后所有的
TouchEvent
便直接进入ScrollView
的onTouchEvent
,直到最后的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
-
ACTION_DOWN
,本次触摸事件的开始,此时为上一节情景三所述传递过程,ScrollView
,ViewPager
,ListView
相继在onInterceptTouchEvent
返回true,使触摸事件一直能传递到最底层。此时ACTION_DOWN
传递到ListView
的子View
时,子View
不需要处理触摸事件,从而在onTouchEvent
中返回了false,从而ACTION_DOWN
返回到上一层进入到了ListView
的onTouchEvent
中并返回了true,此时ListView
成为了整个触摸事件的“主人”。
- 上一节情景四所述传递过程,
ACTION_MOVE
通过最短路径进入“主人”ListView
的onTouchEvent
中,并且不经过ListView
的onInterceptTouchEvent
。
- 同2。需要注意的是,由于
ScrollView
不再能拦截事件,手指划过一定距离后,ListView
认定这是一次上下滚动的事件,不希望之后的TouchEvent
被父视图拦截,所以在此时调用了requestDisallowInterceptTouchEvent
。
- 父视图不再能拦截
TouchEvent
,所有TouchEvent
直接进入ListView
的onTouchEvent
中,直到最后的ACTION_UP
。
-
5.实践
考虑这样的一个三层的视图(忽略了一些无关紧要的层次):
ScrollView
中含有一个TextView
与ViewPager
,其中ViewPager
的高度与ScrollView
的高度一致,而在ViewPager
的某一页为一个同等大小的ListView
,通过在onMeasure
中作一些必要的处理从而将整个视图完整的显示之后,会发现ListView
完全无法滚动。而这个视图结构应该挺常见,交互的需求应该更常见:手指向上滑动时,先滚动ScrollView
,滚动到底后再滚动ListView
;手指向下滑动时,先滚动ListView
,滚动到底后再滚动ScrollView
。
首先对于这个需求,相信大家会首先想到API 21推出的NestedScroll
。在学习了Android
触摸事件传递之后,决定从onInterceptTouchEvent
与onTouchEvent
这两个方法做做手脚,来实现这一需求。我的思路分为两步:
对
onInterceptTouchEvent
做手脚。手指向上滑动时,当ScrollView
滑动到边界时,onInterceptTouchEvent
返回false
,将事件交由ListView
处理,使ListView
能够滑动;手指向下滑动时,如果ListView
能够滑动,就在onInterceptTouchEvent
中返回false
,让ListView
优先滑动。这样下来,虽然还无法在一次手指滑动过程中切换ScrollView
与ListView
的滑动,但是已经能够用两次手指滑动来切换了。对
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);
}