首先我重新写了这了几个控件类,只是加了点打印日志,来观察里面的事件分发机制.
然后写了个布局,如图.
MainActivity有dispatchTouchEvent, onTouchEvent方法
MyRelativieLayout有dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent方法
TextView和Button有dispatchTouchEvent, onTouchEvent方法.
这里拿MyRelativeLayout1的代码来举例,如下
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "MyRelativeLayout1 dispatchTouchEvent begin >> "
+ (ev.getAction() == MotionEvent.ACTION_DOWN ? "down" : ev.getAction() == MotionEvent.ACTION_UP ? "up" : ev.getAction() == MotionEvent.ACTION_MOVE ? "move" : "other"));
boolean flag = super.dispatchTouchEvent(ev);
Log.e(TAG, "MyRelativeLayout1 dispatchTouchEvent end " + flag + " >> "
+ (ev.getAction() == MotionEvent.ACTION_DOWN ? "down" : ev.getAction() == MotionEvent.ACTION_UP ? "up" : ev.getAction() == MotionEvent.ACTION_MOVE ? "move" : "other"));
return flag;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "MyRelativeLayout1 onInterceptTouchEvent "
+ (ev.getAction() == MotionEvent.ACTION_DOWN ? "down" : ev.getAction() == MotionEvent.ACTION_UP ? "up" : ev.getAction() == MotionEvent.ACTION_MOVE ? "move" : "other"));
boolean flag = super.onInterceptTouchEvent(ev);
Log.e(TAG, "MyRelativeLayout1 onInterceptTouchEvent end " + flag + " >> "
+ (ev.getAction() == MotionEvent.ACTION_DOWN ? "down" : ev.getAction() == MotionEvent.ACTION_UP ? "up" : ev.getAction() == MotionEvent.ACTION_MOVE ? "move" : "other"));
return flag;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e(TAG, "MyRelativeLayout1 onTouchEvent "
+ (ev.getAction() == MotionEvent.ACTION_DOWN ? "down" : ev.getAction() == MotionEvent.ACTION_UP ? "up" : ev.getAction() == MotionEvent.ACTION_MOVE ? "move" : "other"));
boolean flag = super.onTouchEvent(ev);
Log.e(TAG, "MyRelativeLayout1 onTouchEvent end " + flag + " >> "
+ (ev.getAction() == MotionEvent.ACTION_DOWN ? "down" : ev.getAction() == MotionEvent.ACTION_UP ? "up" : ev.getAction() == MotionEvent.ACTION_MOVE ? "move" : "other"));
return flag;
}
然后点击MyRelativeLayout2区域时打印出
MainActivity: MainActivity dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent end false >> down
MyRelativeLayout2: MyRelativeLayout2 dispatchTouchEvent begin >> down
MyRelativeLayout2: MyRelativeLayout2 onInterceptTouchEvent begin >> down
MyRelativeLayout2: MyRelativeLayout2 onInterceptTouchEvent end false >> down
MyRelativeLayout2: MyRelativeLayout2 onTouchEvent begin >> down
MyRelativeLayout2: MyRelativeLayout2 onTouchEvent end false >> down
MyRelativeLayout2: MyRelativeLayout2 dispatchTouchEvent end false >> down
MyRelativeLayout1: MyRelativeLayout1 onTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onTouchEvent end false >> down
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent end false >> down
MainActivity: MainActivity onTouchEvent begin >> down
MainActivity: MainActivity onTouchEvent end false >> down
MainActivity: MainActivity dispatchTouchEvent end false >> down
MainActivity: MainActivity dispatchTouchEvent begin >> up
MainActivity: MainActivity onTouchEvent begin >> up
MainActivity: MainActivity onTouchEvent end false >> up
MainActivity: MainActivity dispatchTouchEvent end false >> up
这里能看到. down事件走了这么多步,up事件才走了4步.先来张图展示一下流程.
其实事件传递,在 MainActivity 和 MyRelativeLayout1 之间,还会经过 PhoneWindow 这层,然后是 DecorView这层,接着是DecorView 的子View ——一个LinearLayout, 这个LinearLayout的子View.....一直到这个我们需要观测的子View——我们的MyRelativeLayout1。
为什么ACTION_UP的流程才走了这么一点?下面看源码回答
点击左边的MyTextView
MainActivity: MainActivity dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent end false >> down
MyTextView: MyTextView dispatchTouchEvent begin >> down
MyTextView: MyTextView onTouchEvent begin >> down
MyTextView: MyTextView onTouchEvent end false >> down
MyTextView: MyTextView dispatchTouchEvent end false >> down
MyRelativeLayout1: MyRelativeLayout1 onTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onTouchEvent end false >> down
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent end false >> down
MainActivity: MainActivity onTouchEvent begin >> down
MainActivity: MainActivity onTouchEvent end false >> down
MainActivity: MainActivity dispatchTouchEvent end false >> down
MainActivity: MainActivity dispatchTouchEvent begin >> up
MainActivity: MainActivity onTouchEvent begin >> up
MainActivity: MainActivity onTouchEvent end false >> up
MainActivity: MainActivity dispatchTouchEvent end false >> up
图和上面的差不多,差别是TextView不是ViewGroup.没有onInterceptTouchEvent事件
点击右边的MyButton
MainActivity: MainActivity dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent end false >> down
MyButton: MyButton dispatchTouchEvent begin >> down
MyButton: MyButton onTouchEvent begin >> down
MyButton: MyButton onTouchEvent end true >> down
MyButton: MyButton dispatchTouchEvent end true >> down
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent end true >> down
MainActivity: MainActivity dispatchTouchEvent end true >> down
MainActivity: MainActivity dispatchTouchEvent begin >> up
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent begin >> up
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent begin >> up
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent end false >> up
MyButton: MyButton dispatchTouchEvent begin >> up
MyButton: MyButton onTouchEvent begin >> up
MyButton: MyButton onTouchEvent end true >> up
MyButton: MyButton dispatchTouchEvent end true >> up
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent end true >> up
MainActivity: MainActivity dispatchTouchEvent end true >> up
当点击了Button这种clickable控件,就会在onTouchEvent中返回true.
之后的代码其实用处就不大了,因为返回true后会绕过后面MyRelativeLayout1和Activity的onTouchEvent.
意思是拦截了当前事件的继续派发,自己处理和消化.
所以down流程走完了,up流程会跟down流程走同样的方法路径.
看了上面log和图,接着来分析下源码.(源码基于android-23)
先看Activity的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
很简单.getWindow().superDispatchTouchEvent(ev)意思就是调用子View的dispatchTouchEvent方法.
如果子View没有拦截返回false,就执行Activity的onTouchEvent方法.拦截了就不执行.
getWindow().superDispatchTouchEvent(ev)会把事件传给第一个View——DecorView
然后来看看看ViewGroup的dispatchTouchEvent.几乎所有原生的安卓布局都没有重写这个方法,而是用的他们父类ViewGroup的.
整个diapatchTouchEvent太长了,以下截取部分重要代码...
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;//这是2103行
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}//这是2117行
if (!canceled && !intercepted) {//这是2133行
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}//这是2235行
}
点击非clickable控件流程
1.down流程
如果是点击的非clickable控件,那么down事件会执行intercepted = onInterceptTouchEvent(ev);
intercepted人如其名,表示是否拦截掉事件交给自己的onTouchEvent处理. 不拦截就交给子View处理.
现在讨论的是正常流程,所以默认布局的onInterceptTouchEvent会返回false, intercepted 为false.
然后2133行的 !canceled && !intercepted 判断为真(canceled不在本文讨论范围)
然后会执行里面最重要的依据dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign), 这个就是把事件派发给子View,交由子View处理的方法.
假设子View都没有处理,就会返回false.跳过了里面的逻辑.
2.up流程
up事件因为actionMasked不等于ACTION_DOWN.所以跑了else的部分,使的intercepted = true;
就不会跑2133那里的if代码段,也就不会派发事件给子View.
所以就解释了上面的问题"为什么ACTION_UP的流程才走了这么一点?"
解释一下设计原理就是,我down事件都派发过给子View了,但是儿子们都没处理,所以后续的up事件(包括中间如果有的move事件)都不会再派发到子类了.
省时省力!
点击clickable控件流程
1.down流程
如果是点击的是clickable控件,流程像上面那样,走到了dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
然后clickable控件返回了true.执行了里面的addTouchTarget(child, idBitsToAssign)
这个方法大意就是,如果子View有clickable控件,就让当前这个View(父View)持有这个TouchTarget.
从MyRelativelayout1执行这个持有方法,持有MyButton
到FrameLayout执行这个持有方法,持有MyRelativelayout1
到LinearLayout持有FrameLayout
到DecorView持有LinearLayout
一步步赋值(持有)上去.
所有ViewGroup都会有一个变量mFirstTouchTarget, 它是存放该ViewGroup中能消费事件的子View(既可clickable的控件). 好根据它来用来判断后面的流程要怎么走.
ps:明明父布局就是MyRelativeLayout1,为什么会有多出来这么多其他控件.因为手机本质是这样的布局.给个图但是不展开讲了
2.up流程
然后在up事件分发的时候,就在mFirstTouchTarget != null判断为真,执行给intercepted赋值为假的逻辑.
然后在2133代码判断后,up流程就会继续派发事件给子View.
以此达到down,move,up所有事件都最终交给子View处理.
以上是正常流程..
下面讲讲根据自己的需要改写流程的情况.
以下情景是点击右边的MyButton
1.把dispatchTouchEvent改写成直接返回false
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "MyRelativeLayout1.java - dispatchTouchEvent() ---------- return false");
return false;
}
就会打印log
MainActivity: MainActivity dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1.java - dispatchTouchEvent() ---------- return false
MainActivity: MainActivity onTouchEvent begin >> down
MainActivity: MainActivity onTouchEvent end false >> down
MainActivity: MainActivity dispatchTouchEvent end false >> down
MainActivity: MainActivity dispatchTouchEvent begin >> up
MainActivity: MainActivity onTouchEvent begin >> up
MainActivity: MainActivity onTouchEvent end false >> up
MainActivity: MainActivity dispatchTouchEvent end false >> up
意思是不会交给子View处理,自己也不处理.
返回false也不拦截,所以就会把事件处理交给Activity的onTouchEvent.
2.把dispatchTouchEvent改成返回true
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "MyRelativeLayout1.java - dispatchTouchEvent() ---------- return true");
return true;
}
就会打印log
MainActivity: MainActivity dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1.java - dispatchTouchEvent() ---------- return true
MainActivity: MainActivity dispatchTouchEvent end true >> down
MainActivity: MainActivity dispatchTouchEvent begin >> up
MyRelativeLayout1: MyRelativeLayout1.java - dispatchTouchEvent() ---------- return true
MainActivity: MainActivity dispatchTouchEvent end true >> up
这样改写就把事件留住,不再分发子View,然后可以把业务逻辑写在这个方法里面
但是这样不规范,一般想要实现这种效果,都是用下面的方法.
3.在onInterceptTouchEvent中返回true, 在onTouchEvent也返回true,并在其中写处理逻辑
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "MyRelativeLayout1 onInterceptTouchEvent >> "
+ (ev.getAction() == MotionEvent.ACTION_DOWN ? "down" : ev.getAction() == MotionEvent.ACTION_UP ? "up" : ev.getAction() == MotionEvent.ACTION_MOVE ? "move" : "other"));
return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e(TAG, "MyRelativeLayout1 onTouchEvent >> "
+ (ev.getAction() == MotionEvent.ACTION_DOWN ? "down" : ev.getAction() == MotionEvent.ACTION_UP ? "up" : ev.getAction() == MotionEvent.ACTION_MOVE ? "move" : "other"));
Log.e(TAG, "MyRelativeLayout1.java - onTouchEvent() ---------- 写处理逻辑");
return true;
}
MainActivity: MainActivity dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent begin >> down
MyRelativeLayout1: MyRelativeLayout1 onInterceptTouchEvent >> down
MyRelativeLayout1: MyRelativeLayout1 onTouchEvent >> down
MyRelativeLayout1: MyRelativeLayout1.java - onTouchEvent() ---------- 写处理逻辑
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent end true >> down
MainActivity: MainActivity dispatchTouchEvent end true >> down
MainActivity: MainActivity dispatchTouchEvent begin >> up
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent begin >> up
MyRelativeLayout1: MyRelativeLayout1 onTouchEvent >> up
MyRelativeLayout1: MyRelativeLayout1.java - onTouchEvent() ---------- 写处理逻辑
MyRelativeLayout1: MyRelativeLayout1 dispatchTouchEvent end true >> up
MainActivity: MainActivity dispatchTouchEvent end true >> up
这是重写事件处理中最常用的方法了.
onInterceptTouchEvent中返回true使得不会再把事件派发到子View,然后回转到onTouchEvent执行自己的业务代码
使用场景经常是一个布局文件里面有很多子View,但是却不想分发自己处理,就会用这种写法.
稍微讲解一下这个流程的源码
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;//这是2103行
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}//这是2117行
if (!canceled && !intercepted) {//这是2133行
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);//这个方法会给成员变量mFirstTouchTarget赋值
...
}//这是2235行
if (mFirstTouchTarget == null) {//这是2238行
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,//这是2511行
View child, int desiredPointerIdBits) {
...
handled = super.dispatchTouchEvent(event);//这是2547
...
}//这是2581行
前面讲过,onInterceptTouchEvent如果返回true,会给变量intercepted赋值.
接着在2133行判断中为假,跳过里面派发给子View的代码.
没子View什么事,这时mFirstTouchTarget也会为null
然后在经过2238的判断里,进入dispatchTransformedTouchEvent方法
这个方法里面又会调用到父类,既View的dispatchTouchEvent方法,
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (!result && onTouchEvent(event)) {//9294行
result = true;
}//9296行
...
return result;
}
里面就会执行熟悉的onTouchEvent方法.
我曾经想着事件都留到这了,这方法应该不需要返回true.事实
事实是假设返回false还是会让事件最终又回到MainActivity的onTouchEvent方法里.
毕竟全部方法都遵循返回false表示不拦截,事件继续往下一层流转.返回true表示在这里处理,不分发.
关于setOnTouchListener和setOnClickListener是在什么时候调用?他们的优先级是怎么样?
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
View类里面有这么一个判断。
li.mOnTouchListener!=null && 中间省略 && li.mOnTouchListener.onTouch(this, event)
所以假设我们给某个控件执行了setOnTouchListener方法,就会在这里执行onTouch方法。
并且重要的一点是,如果onTouch方法中返回true。result置为true,就不会进入onTouchEvent方法了。
ps:如果是ViewGroup,则是在dispatchTransformedTouchEvent中会调用super.onDispatchTouchEvent,同样会进入到这个流程。
public boolean onTouchEvent(MotionEvent event) {
...
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
performClick();
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
在View的onTouchEvent中,里面会判断这个View是不是CLICKABLE和LONG_CLICKABLE。Button这种默认CLICKABLE,TextView默认不是CLICKABLE。所有的View都不是LONG_CLICKABLE的。
然后我们可以通过xml属性或者java代码,或者更直接的,一旦调用了setOnClickListener或setOnLongClickListener方法,就会把对应的这两个CLICKABLE和LONG_CLICKABLE设为true。
进入这个判断里面,里面会根据各种传来MotinEvent事件进行判断,如果符合判断就会调用performClick方法,里面有执行mOnClickListener.onClick这个回调的方法。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
ps:无意中发现有一个View的onTouch方法中有一个TouchDelegate,人如其名,是用来给View设置一个点击代理的。代理返回true的话,就会跳过下面的逻辑
所以得出OnTouchListener的优先级是高于onTouchEvent方法的,自然也就高于onClick,onLongClick这些方法。
对于View.onTouchEvent,如果自己重写这个方法,那么给他们设置的onClickListener和onLongClickListener监听单击和长按事件就作废了。
想要两者兼得,请根据业务逻辑好好构思怎么写。
ps:
View中有一个很重要的方法requestDisallowInterceptTouchEvent,可以在子控件中控制父控件是否拦截。一般用法如下
if (getCurrentItem() != 0) {
getParent().requestDisallowInterceptTouchEvent(true);// 请求父View不拦截
} else {
getParent().requestDisallowInterceptTouchEvent(false);// 允许父View拦截
}