说起Android滑动冲突,是个很常见的场景,比如SliddingMenu与ListView的嵌套,要解决滑动冲突,不得不提及到View的事件分发机制。
一、Touch事件传递规则分析
首先,我们要知道Touch事件是包装在MotionEvent对象中的,在手指与屏幕接触过程中产生一系列事件,典型的事件有以下三种:
ACTION_DOWN:手指刚接触屏幕的瞬间
ACTION_UP:手指刚离开屏幕的瞬间
ACTION_MOVE:手指在屏幕上滑动
如果return true,事件会由当前View的dispatchTouchEvent方法进行消费,同时事件会停止向下传递;
如果return false,事件分发分为两种情况:
如果当前 View 获取的事件直接来自 Activity,则会将事件返回给Activity的onTouchEvent进行消费;
如果当前 View 获取的事件来自外层父控件,则会将事件返回给父View的onTouchEvent进行消费。
如果return true,则表示拦截该事件,并将事件传递给当前View的onTouchEvent方法;
如果return false,则表示不拦截该事件,并将该事件交由子View的dispatchTouchEvent方法进行事件分发,重复上述过程;
如果return true,则表示响应并消费该事件;
如果return fasle,则表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true,如果到了最顶层View还是返回false,那么认为该事件不消耗,则在同一个事件系列中,当前View无法再次接收到事件,该事件会交由Activity的onTouchEvent进行处理;
如果return super.dispatchTouchEvent(ev),则表示不响应事件,结果与return false一样。
这里也顺便说一下,如果一个View同时监听了onTouch事件和onClick事件,则在onTouchEvent里面应该返回false,否则点击事件就无法监听到。后面会提到这一点。
(1)当一个点击事件产生后,它的传递过程遵循的规则如下:Activity->Window->View。顶级View接收到事件之后,就会按相应规则去分发事件。如果一个View的onTouchEvent方法返回false,那么将会交给父容器的onTouchEvent方法进行处理,逐级往上,如果所有的View都不处理该事件,则交由Activity的onTouchEvent进行处理,就跟工作中遇到了难题,逐级找领导解决一个道理,领导解决不了,再找上一级领导。
(2)正常情况下,一个事件序列只能被一个View拦截且消耗,某个View一旦进行事件拦截,那么这一个事件序列都只能交由他处理,并且onInterceptTouchEvent也不会被再次调用,因此,正常情况下一个事件是不能交给两个View来处理的,当然,特殊做法就是在View的onTouchEvent,处理完之后再返回false,强行交给其他View处理。
(3)如果某一个View开始处理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),则同一事件序列比如接下来进行ACTION_MOVE,则不会再交给该View处理,就像工作中做一件事情,你要嘛做完,要嘛你就不要做这件事了。
(4)在Android中,ViewGroup默认返回false,即不拦截任何事件。
(5)诸如TextView、ImageView这些不作为容器的View,一旦接受到事件,就调用onTouchEvent方法,它们本身没有onInterceptTouchEvent方法。正常情况下,它们都会消耗事件(返回true),除非它们是不可点击的(clickable和longClickable都为false),那么就会交由父容器的onTouchEvent处理。
(6)View的enable属性不影响onTouchEvent的默认返回值,只要它clickable或者longClickable为true,则onTouchEvent就会返回true。
(7)点击事件分发过程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent-->OnClickListener的onClick方法。也就是说,我们平时调用的setOnClickListener,优先级是最低的,所以,onTouchEvent或OnTouchListener的onTouch方法如果返回true,则不响应onClick方法...
二、滑动冲突处理过程分析
滑动冲突的场景常见于滑动嵌套,就是一个页面中可能有两个或两个以上的View同时可以滑动,那么就可能会导致只有其中的一个View能滑动。一个最简单的屏幕触摸动作触发了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE—>...->ACTION_MOVE->ACTION_UP。
滑动冲突场景主要有三种:
(1)一个页面中同时存在左右滑动和上下滑动。
让外部的View拦截滑动事件,判断滑动的特征,如果水平滑动距离>竖直滑动距离,则为水平滑动,反之为竖直滑动。假设内部View可以水平滑动,外部View可以竖直滑动,那么在外部View的onInterceptTouchEvent方法判断,如果触摸事件为水平滑动,则应该放行,也就是返回false,然后交给内部View来处理,那么内部子View就可以实现水平滑动。当然,还有一种方法就是外部View不拦截,交给内部View处理,如果内部View有需要就自己消耗掉,否则交给上一层,但是这样违反了事件分发机制,所以需配合requestDisallowInterceptTouchEvent(MotionEvent ev)进行处理,这里就不细说了,有兴趣的童鞋可以研究一下。
(2)同时存在两个竖直或水平滑动
这个主要还得根据具体的需求分析。最简单的加入是两个ScrollView嵌套,一般可以判断ACTION_DOWN在那个View上,就执行那个View的滑动事件。
(3)就是(1)和(2)同时存在的情况
实际上也得看具体业务需求找到突破点,但是处理方式本质上来说都是差不多的,就是要根据滑动策略,来干扰事件分发机制。
附上一段伪代码来理清一下思路:
一、Touch事件传递规则分析
首先,我们要知道Touch事件是包装在MotionEvent对象中的,在手指与屏幕接触过程中产生一系列事件,典型的事件有以下三种:
ACTION_DOWN:手指刚接触屏幕的瞬间
ACTION_UP:手指刚离开屏幕的瞬间
ACTION_MOVE:手指在屏幕上滑动
那么,Android中Touch事件是一个怎样的传递过程呢?
事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
Touch事件发生时Activity的dispatchTouchEvent(MotionEvent ev)方法会将事件传递给最外层View的dispatchTouchEvent(MotionEvent ev)方法,该方法对事件进行分发。分发逻辑如下:如果return true,事件会由当前View的dispatchTouchEvent方法进行消费,同时事件会停止向下传递;
如果return false,事件分发分为两种情况:
如果当前 View 获取的事件直接来自 Activity,则会将事件返回给Activity的onTouchEvent进行消费;
如果当前 View 获取的事件来自外层父控件,则会将事件返回给父View的onTouchEvent进行消费。
如果return super.dispatchTouchEvent(ev),事件会自动的分发给当前View的onInterceptTouchEvent方法。
事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
上面已经提到,如果在dispatchTouchEvent返回super.dispatchTouchEvent(ev),那么事件将会传递到onInterceptTouchEvent方法,该方法对事件进行拦截。拦截逻辑如下:如果return true,则表示拦截该事件,并将事件传递给当前View的onTouchEvent方法;
如果return false,则表示不拦截该事件,并将该事件交由子View的dispatchTouchEvent方法进行事件分发,重复上述过程;
如果return super.onInterceptTouchEvent(ev),默认表示拦截该事件,并将事件传递给当前View的onTouchEvent方法,和return true一样。
事件响应:public boolean onTouchEvent(MotionEvent ev)
上面已经提到,在dispatchTouchEvent(事件分发)返回super.dispatchTouchEvent(ev)并且onInterceptTouchEvent(事件拦截返回true或super.onInterceptTouchEvent(ev)的情况下,那么事件会传递到onTouchEvent方法,该方法对事件进行响应。响应逻辑如下:如果return true,则表示响应并消费该事件;
如果return fasle,则表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true,如果到了最顶层View还是返回false,那么认为该事件不消耗,则在同一个事件系列中,当前View无法再次接收到事件,该事件会交由Activity的onTouchEvent进行处理;
如果return super.dispatchTouchEvent(ev),则表示不响应事件,结果与return false一样。
这里也顺便说一下,如果一个View同时监听了onTouch事件和onClick事件,则在onTouchEvent里面应该返回false,否则点击事件就无法监听到。后面会提到这一点。
上述三个方法到底有什么区别与联系呢?我们通过一段伪代码来表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){ // 如果onInterceptTouchEvent返回true
consume = onTouchEvent(ev); // 则交由该View的onTouchEvent方法
} else {
consume = child. dispatchTouchEvent(ev); // 否则交由子View的dispatchTouchEvent事件进行分发
}
return consume; // 如果成功消费该事件,则返回true,然后停止传递,否则返回false
}
那么,接下来就总结一下事件的传递的规则。(1)当一个点击事件产生后,它的传递过程遵循的规则如下:Activity->Window->View。顶级View接收到事件之后,就会按相应规则去分发事件。如果一个View的onTouchEvent方法返回false,那么将会交给父容器的onTouchEvent方法进行处理,逐级往上,如果所有的View都不处理该事件,则交由Activity的onTouchEvent进行处理,就跟工作中遇到了难题,逐级找领导解决一个道理,领导解决不了,再找上一级领导。
(2)正常情况下,一个事件序列只能被一个View拦截且消耗,某个View一旦进行事件拦截,那么这一个事件序列都只能交由他处理,并且onInterceptTouchEvent也不会被再次调用,因此,正常情况下一个事件是不能交给两个View来处理的,当然,特殊做法就是在View的onTouchEvent,处理完之后再返回false,强行交给其他View处理。
(3)如果某一个View开始处理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),则同一事件序列比如接下来进行ACTION_MOVE,则不会再交给该View处理,就像工作中做一件事情,你要嘛做完,要嘛你就不要做这件事了。
(4)在Android中,ViewGroup默认返回false,即不拦截任何事件。
(5)诸如TextView、ImageView这些不作为容器的View,一旦接受到事件,就调用onTouchEvent方法,它们本身没有onInterceptTouchEvent方法。正常情况下,它们都会消耗事件(返回true),除非它们是不可点击的(clickable和longClickable都为false),那么就会交由父容器的onTouchEvent处理。
(6)View的enable属性不影响onTouchEvent的默认返回值,只要它clickable或者longClickable为true,则onTouchEvent就会返回true。
(7)点击事件分发过程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent-->OnClickListener的onClick方法。也就是说,我们平时调用的setOnClickListener,优先级是最低的,所以,onTouchEvent或OnTouchListener的onTouch方法如果返回true,则不响应onClick方法...
二、滑动冲突处理过程分析
滑动冲突的场景常见于滑动嵌套,就是一个页面中可能有两个或两个以上的View同时可以滑动,那么就可能会导致只有其中的一个View能滑动。一个最简单的屏幕触摸动作触发了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE—>...->ACTION_MOVE->ACTION_UP。
滑动冲突场景主要有三种:
(1)一个页面中同时存在左右滑动和上下滑动。
让外部的View拦截滑动事件,判断滑动的特征,如果水平滑动距离>竖直滑动距离,则为水平滑动,反之为竖直滑动。假设内部View可以水平滑动,外部View可以竖直滑动,那么在外部View的onInterceptTouchEvent方法判断,如果触摸事件为水平滑动,则应该放行,也就是返回false,然后交给内部View来处理,那么内部子View就可以实现水平滑动。当然,还有一种方法就是外部View不拦截,交给内部View处理,如果内部View有需要就自己消耗掉,否则交给上一层,但是这样违反了事件分发机制,所以需配合requestDisallowInterceptTouchEvent(MotionEvent ev)进行处理,这里就不细说了,有兴趣的童鞋可以研究一下。
(2)同时存在两个竖直或水平滑动
这个主要还得根据具体的需求分析。最简单的加入是两个ScrollView嵌套,一般可以判断ACTION_DOWN在那个View上,就执行那个View的滑动事件。
(3)就是(1)和(2)同时存在的情况
实际上也得看具体业务需求找到突破点,但是处理方式本质上来说都是差不多的,就是要根据滑动策略,来干扰事件分发机制。
附上一段伪代码来理清一下思路:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) { // 外部View拦截事件
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x; // 分别记录上次滑动坐标
mLastYIntercept = y;
return intercepted; // 看是否需要传递给内部View处理
}
参考:
Android开发艺术探索