事件分发与消费机制
参考郭霖博客:
http://blog.csdn.net/guolin_blog/article/details/9097463
http://blog.csdn.net/guolin_blog/article/details/9153747
标签(空格分隔): Android
onTouch与onClick的关系,调用时机###
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
onTouch是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你还可能会有多次ACTION_MOVE的执行,如果你手抖了一下)。因此事件传递的顺序是先经过onTouch,再传递到onClick。
【要注意的是onTouch与onTouchEvent都可以监控ACTION_DOWN、ACTION_MOVE、ACTION_UP等手势,而且是持续监听,即每个ACTION都会进入这两个方法进行监听】
结论:onTouch方法是有返回值的,这里我们返回的是false,那么onClick()还会执行,如果我们尝试把onTouch方法里的返回值改成true,那么onClick()就不会执行
应用场景:为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?
滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。
滚动事件也像点击事件一样,跟onTouch的返回值有关???
【任何控件本身是没有dispatchTouchEvent方法的,是从view类继承的】
首先你需要知道一点,只要你触摸到了任何一个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法【该dispatchTouchEvent方法是继承于ViewGroup】,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。所有view控件的dispatchTouchEvent方法都是继承于view,示意图如下:
单个子View时
此处输入图片的描述
布局嵌套时
此处输入图片的描述
子View的dispatchTouchEvent方法的源码:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
//条件一:mOnTouchListener正是在setOnTouchListener方法里赋值的,也就是说只要我们给控件注册了touch事件,mOnTouchListener就一定被赋值了。
//条件二:(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的
//条件三:mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。
让这三个条件全部成立,从而dispatchTouchEvent方法直接返回true
而且onClick的调用肯定是在onTouchEvent(event)方法中的,所以当在onTouch方法里返回了true【而且既然可以调用onTouch方法,那么就一定满足该控件注册了touch事件,而且是可以点击的】,就会让dispatchTouchEvent方法直接返回true,所以不会走return onTouchEvent(event),当然就不会调用到onClick方法了
1. onTouch和onTouchEvent有什么区别,又该如何使用?
从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
因为onTouchEvent()
方法的代码有点长,所以就不列出来了,在onTouchEvent()可以判断ACTION_DOWN、ACTION_MOVE、ACTION_UP等手势。还有里面的performClick()会调用到onClick()
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
//mOnClickListener不是null,就会去调用它的onClick方法
而mOnClickListener是在这里赋值的
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}
touch事件的ACTION层级传递###
我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。
这里要补充一句,即使在onTouch事件里面返回了false。所以就一定会进入到onTouchEvent方法中其中有一个if用于判断是否可以点击,如果可以点击则返回一个true。是不是有一种被欺骗的感觉?明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得ACTION_UP可以得到执行。
所以将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false。在ACTION_DOWN执行完后,后面的一系列action都不会得到执行了。因为ImageView和按钮不同,它是默认不可点击的,所以不能进入onTouchEvent方法中其中的那个用于判断是否可以点击的if,直接在onTouchEvent中返回false,所以最终的返回还是false,所以就导致后面其它的action都无法执行了。
应用场景:为什么图片轮播器里的图片使用Button而不用ImageView?
主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable="true"的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。
布局嵌套时的事件分发##
分发顺序:
Acivity->Window->DecorView->ViewGroup->View
ViewGroup中有一个onInterceptTouchEvent方法
ViewGroup中有一个dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
//在ViewGroup中的dispatchTouchEvent方法,第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。
//disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。
//而子view的时事件分发就是在这个条件判断中,这个条件判断的内部是怎么实现的?在第19行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,之后的流程就跟不嵌套时的子view事件分发一样。
//所以说当跳出了这个条件判断后,即在onInterceptTouchEvent方法中返回true,就会让第二个值!onInterceptTouchEvent为false子View的事件分发将得不到执行,所以子View就会被屏蔽。
//而当子View没有被屏蔽时,即子View的dispatchTouchEvent得到执行,而子View是可点击的,子View的dispatchTouchEvent一定返回True,就会导致ViewGroup的dispatchTouchEvent第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,所以导致后面的代码中ViewGroup的touch事件就没有办法执行了
//
而当我们点击的只是ViewGroup的空白区域而不是子View时,首先也是会进入ViewGroup的dispatchTouchEvent,就算ViewGroup的onInterceptTouchEvent返回的是true,令ViewGroup的dispatchTouchEvent中的13行的条件判断的!onInterceptTouchEvent变成false,不进入这个条件判断里面,即拦截子View的事件。但是这时点击的不是子View控件,所以不会在dispatchTouchEvent的31行返回true,令方法立马结束,不会使ViewGroup的touch事件没有执行
此处输入图片的描述
***结论:
-
要想拦截子View的事件,就重写ViewGroup的onInterceptTouchEvent方法,使其返回true。***默认是false不拦截子View事件
-
而且子View没有onInterceptTouchEvent
-
如果ViewGroup的onInterceptTouchEvent方法,使其返回true即拦截子View事件,那么在拦截了同一序列事件中的ACTION_DOWN后,onInterceptTouchEvent不会在被调用,并且继续拦截剩下的ACTION_MOVE,ACTION_UP
-
另外注意下如果子View设置了FLAG_DISALLOW_INTERCEPT这个标记位,具体看一下《开发艺术探索》,可以让ViewGroup只能拦截ACTION_DOWN,不能拦截ACTION_MOVE,ACTION_UP
-
拦截方式有外部拦截法于内部拦截法,一般推荐使用较为简单的外部拦截法
-
所有函数的返回值都是显而易见的,True:拦截、消费处理。False:不拦截、不消费处理。只有onTouch()的返回值是怪怪的,返回的是false的时候onClick()才会执行!!!
事件消费传递###
如果子View的onTouchEvent返回的是false,那么就会交给他的父容器onTouchEvent处理,如果所有的元素都不处理这个事件的话,就会交给Activity的onTouchEvent处理。
只要找到了事件处理者的话,只要当前ACTION会有去找处理者的过程,而之后的每个ACTION会直接被之前找到的处理者消费掉,不会有那个找处理者的那个查找过程。
【那onTouch的返回值有对消费传递有影响吗?】
答案是有影响的,即使onTouch的返回值是false,就会调用onTouchEvent,所以事件传递消费还是要看onTouchEvent;如果onTouch的返回值是true的话,事件就会被处理掉,而且onTouchEvent不会被调用。
【在onTouchEvent的返回值指的是每个case判断中对每个ACTION的处理后的返回值,还是指onTouchEvent最底下的返回值?】
一般指的是每个case判断中对每个ACTION的处理后的返回值,但是onTouchEvent最底下的返回值是必须要写的,因为函数必须要有返回值。
View的onTouchEvent默认返回的是true,即处理消费事件