Android touch事件分发机制★

本文详细探讨了Android中的触摸事件分发机制,从事件传递顺序、Activity到View的分发过程,再到dispatchTouchEvent和onTouchEvent的处理逻辑。重点分析了ACTION_DOWN、ACTION_MOVE和ACTION_UP事件的分发规律,以及如何通过返回值控制事件的处理和传递。最后,通过实例展示了事件分发的源码解析和常见应用场景,帮助理解触摸事件在View和ViewGroup之间的流转和处理过程。

1.touch事件分发
touch事件分发指对触摸事件MotionEvent的分发过程。当一个MotionEvent产生后,系统需要把这个事件传递给某个具体的View进行处理,这个传递的过程就是分发过程。
当一个touch事件产生后,它的传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给Activity,Activity传递给Window,最后Window传递给DecorView。DecorView接收到事件后,就会按照事件分发机制从ViewGroup->View去分发事件。

由于事件总是先传递到Activity,看一下Activity的dispatchTouchEvent方法是怎样把触摸事件从activity传递到ViewGroup的。
Activity.class:
public boolean dispatchTouchEvent( MotionEvent ev) {
if(getWindow().superDispatchTouchEvent( ev) {
return true;
}
//所有view都不处理事件,则直接调用Activity的onTouchEvent方法
return onTouchEvent();
}
首先调用Window的superDispatchTouchEvent方法由Window去分发事件,如果Window不处理该事件则会调用Activity的onTouchEvent方法来进行处理。
跟踪源代码会发现事件是从Activity -> PhoneWindow -> DecoreView-> dispatchTouchEvent,如下:
在这里插入图片描述
DecorView是一个FrameLayout,由DecorView分发事件,最终调用DecorView的dispatchTouchEvent事件。

考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依此类推。如果所有的元素都不处理这个事件,那么这个事件最终将传递给Activity处理,即Activity的onTouchEvent方法会被调用(因为没有子view处理事件,dispatchTouchEvent返回false,因此会调用Activity的onTouchEvent函数)。

2.事件分发流程
在这里插入图片描述
注:上图中的事件都是针对ACTION_DOWN,对于ACTION_MOVE和ACTION_UP稍后单独分析。

仔细看整个图,得出事件流走向的几个结论:
①如果事件不被中断,整个事件流向是一个类U型图。
在这里插入图片描述
如果没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从Activity–>ViewGroup–> View从上往下调用dispatchTouchEvent方法,一直到叶子节点,再由View–> ViewGroup–>Activity从下往上调用onTouchEvent方法。
②dispatchTouchEvent和onTouchEvent一旦返回true,事件就停止传递了(即到达终点,没有谁能再收到这个事件)。所以return true表示事件被消费了。
③dispatchTouchEvent和onTouchEvent返回false时,事件都回传给父控件的onTouchEvent处理。
dispatchTouchEvent返回false指的是事件停止往子View传递和分发,同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传,直到某个控件的onTouchEvent返回true),事件分发机制就像递归,return false就是递归停止然后开始回溯。
onTouchEvent返回false指的是不消费事件,并让事件继续往父控件的方向从下往上流动。
④ViewGroup和View的dispatchTouchEvent方法返回super.dispatchTouchEvent()时事件流走向:
首先看ViewGroup的dispatchTouchEvent:return true是终结传递,return false是回溯到父View的onTouchEvent,那么ViewGroup怎样通过dispatchTouchEvent方法把事件分发到自己的onTouchEvent处理呢,return true和false都不行,那么只能通过Interceptor把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent。
而对于View的dispatchTouchEvent,由于View没有拦截器,return true是终结,return false是回溯回父类的onTouchEvent,怎样把事件分发给自己的onTouchEvent处理呢,那只能return super.dispatchTouchEvent了,所以view类的dispatchTouchEvent方法默认实现就是调用View自己的onTouchEvent方法的。

3.关于action_move和action_up
ACTION_MOVE和ACTION_UP在传递过程中并不总是和ACTION_DOWN一样。如果ACTION_DOWN返回false,则后面一系列其它的action就不会再执行了。也就是说,当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP事件,即并不是哪个函数收到了ACTION_DOWN就会收到 ACTION_MOVE等后续事件的。

具体分析一下,讨论的布局如下:
在这里插入图片描述
通过几张图看看不同场景下ACTION_MOVE事件和ACTION_UP事件的具体走向并总结一下规律:
①在ViewGroup1的dispatchTouchEvent方法返回true消费这次事件。
红色的箭头代表ACTION_DOWN 事件的流向,蓝色的箭头代表ACTION_MOVE 和 ACTION_UP事件的流向。
在这里插入图片描述
②在ViewGroup2的dispatchTouchEvent返回true消费这次事件。
在这里插入图片描述
③在View的dispatchTouchEvent返回true消费这次事件
效果和在ViewGroup2的dispatchTouchEvent return true的差不多,同样的收到ACTION_DOWN 的dispatchTouchEvent函数都能收到 ACTION_MOVE和ACTION_UP。
所以得出结论:如果在某个控件的dispatchTouchEvent返回true消费终结事件,那么收到ACTION_DOWN 的函数也能收到 ACTION_MOVE和ACTION_UP。
④在View的onTouchEvent返回true消费这次事件。
在这里插入图片描述
⑤在ViewGroup2的onTouchEvent返回true消费这次事件。
在这里插入图片描述
⑥在ViewGroup1的onTouchEvent返回true消费这次事件。
在这里插入图片描述
⑦在Activity的onTouchEvent 返回true消费这次事件。
在这里插入图片描述
⑧在View的dispatchTouchEvent返回false并且Activity的onTouchEvent返回true消费这次事件。
在这里插入图片描述
⑨在View的dispatchTouchEvent返回false并且ViewGroup1的onTouchEvent返回true消费这次事件。
在这里插入图片描述
⑩在View的dispatchTouchEvent返回false并且在ViewGroup2的onTouchEvent返回true消费这次事件
在这里插入图片描述
⑩在ViewGroup2的dispatchTouchEvent返回false并且在ViewGroup1的onTouchEvent返回true消费这次事件
在这里插入图片描述
⑩在ViewGroup2的onInterceptTouchEvent返回true拦截此次事件并且在ViewGroup1的onTouchEvent返回true消费这次事件。
在这里插入图片描述
可以看出规律了,对于在onTouchEvent消费事件的情况:在哪个View的onTouchEvent返回true,那么ACTION_MOVE和ACTION_UP事件就会从上往下传到这个View后就不再往下传递了,而是直接传给自己的onTouchEvent处理并结束本次事件传递过程。

对于ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,而且只会传到这个控件,不会继续往下传。如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

3.View的事件分发源码解析
从一个简单的例子入手。一个Activity中只有一个按钮,给这个按钮注册一个点击事件:
button.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
Log.d(“TAG”, “onClick execute”);
}
});
再给这个按钮注册一个touch事件:
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是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(可能还会有多次ACTION_MOVE,比如手抖了一下)。因此可以知道事件传递的顺序是先经过onTouch,再传递到onClick。
onTouch方法是有返回值的,刚刚返回的false,现在把onTouch方法的返回值改成true再运行一次:
在这里插入图片描述
此时onClick方法不再执行了。也就是说onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。

从源码的角度分析一下出现该现象的原理。
看一下View类的dispatchTouchEvent方法:
public boolean dispatchTouchEvent( MotionEvent event){
boolean result = false;
… …
ListenerInfo li = mListenerInfo;
//onTouch先于onTouchEvent执行
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;
}
首先进行判断,如果 li != null && li.mOnTouchListener != null 、 (mViewFlags & ENABLED_MASK)==ENABLED和li.mOnTouchListener.onTouch(this, event)这三个条件都为真,就返回true,否则就执行onTouchEvent(event)方法并返回。
先看第一个条件,li 和li.mOnTouchListener变量是在哪里赋值的呢?
ListenerInfo li = mListenerInfo;
而mListenerInfo赋值的地方在View的getListenerInfo()方法里:
ListenerInfo getListenerInfo() {
if(mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
而getListenerInfo方法是在View的setOnTouchListener方法里调用的:
public void setOnTouchListener( OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
mOnTouchListener是在setOnTouchListener方法里赋值的,也就是说只要给控件注册了touch事件,li和li.mOnTouchListener就会被赋值了。
第二个条件(mViewFlags & ENABLED_MASK) ==ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。
第三个条件就比较关键了,li.mOnTouchListener.onTouch(this, event),也就是回调控件注册touch事件时的onTouch方法。如果在onTouch方法里返回true就会让这三个条件全部成立,从而整个方法直接返回true;如果在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法。

现在可以结合前面的例子来分析一下,首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的,也是印证了刚刚的打印结果。而如果在onTouch方法里返回true就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。而打印结果也证实了如果onTouch返回true,onClick就不会再执行了。

这里还透漏出一个重要的信息,就是onClick的调用肯定是在onTouchEvent(event)方法里。来看一下onTouchEvent的源码,onTouchEvent方法很复杂,只取用到的地方:
public boolean onTouchEvent(MotionEvent event){
if(clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
……
if(!post(mPerformClick)) {
performClickInternal();
}
break;
……
}
}
return false; // clickable为false(即控件不可点击)时,onTouchEvent直接返回false
}
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
首先从if(clickable || (viewFlags & TOOLTIP) ==TOOLTIP) 可以看出,如果该控件是可以点击的就会进入switch判断中,如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case中。在经过种种判断之后,会执行到performClick()方法,进入到这个方法里:
public boolean performClick() {
final boolean result;
final ListenerInfo little = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect( SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else{
result = false;
}
……
return result;
}
只要li和li.mOnClickListener不是null就会调用它的onClick方法,那li和li.mOnClickListener是在哪里赋值的呢?和mOnTouchListener类似:
public void setOnClickListener( OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
当通过setOnClickListener方法给控件注册一个点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时都会在performClick()方法里回调被点击控件的onClick方法。
注意:只要onTouch或OnTouchEvent方法返回true,就会使dispatchTouchEvent返回true,代表当前View消费了事件。

View的整个事件分发流程就搞清楚了。需要说明一下touch事件的层级传递。如果给一个控件注册了touch事件,每次点击都会触发一系列的ACTION_DOWN、ACTION_MOVE、ACTION_UP等事件。如果在ACTION_DOWN返回了false,则后面一系列其它的action就不会再执行。也就是说当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true才会触发后一个action。

现在将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false:
imageView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(“TAG”, "onTouch execute, action " + event.getAction());
return false;
}
});
运行程序,点击ImageView,结果如下:
在这里插入图片描述
在ACTION_DOWN执行完后,后面的一系列action都不会执行。这是因为ImageView和按钮不同,它默认不可点击,因此在onTouchEvent的if判断时无法进入到if的内部,直接跳到最后返回了false,导致后面其它的action都无法执行了。

View的事件分发就结束了,总结一下:
①onTouch和onTouchEvent有什么区别,该如何使用?
这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中返回true将事件消费掉,那么onTouchEvent将不会再执行。
注意,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener不能为空,第二当前点击的控件必须是enable的。因此如果某个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
②为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?
滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。

3.ViewGroup的事件分发源码
ViewGroup是View的子类,并且重写了dispatchTouchEvent方法。

通过一个Demo演示VewGroup的事件分发流程。
首先自定义MyLayout布局继承自LinearLayout:
public class MyLayout extends LinearLayout {
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
在主布局文件中加入自定义的布局:
<com.example.MyLayout
android:id="@+id/my_layout"
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical” >
< Button
android:id="@+id/button1"
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:text=“Button1” />
< Button
android:id="@+id/button2"
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:text=“Button2” />
</com.example.MyLayout>
在MyLayout中添加两个按钮,然后在MainActivity中为两个按钮和MyLayout注册监听事件:
myLayout.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(“TAG”, “myLayout on touch”);
return false;
}
});
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d(“TAG”, “You clicked button1”);
}
});
button2.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d(“TAG”, “You clicked button2”);
}
});
运行项目,分别点击Button1、Button2和空白区域:
在这里插入图片描述
当点击按钮的时候,MyLayout注册的onTouch方法并不会执行,只有点击空白区域的时候才会执行该方法。

接着在MyLayout中重写onInterceptTouchEvent方法,使它返回true:
@override
public boolean onInterceptTouchEvent( MotionEvent ev) {
return true;
}
再次运行项目,然后分别Button1、Button2和空白区域:
在这里插入图片描述
此时不管点击哪里,永远都只会触发MyLayout的touch事件了,按钮的点击事件完全被屏蔽掉了。

下面通过阅读源码,搞清楚ViewGroup的事件分发机制。
首先要知道,Android中touch事件的传递是先传递到ViewGroup,再传递到View的。当点击了某个控件,首先会调用该控件所在布局ViewGroup的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。
比如点击MyLayout中的按钮,会先调用MyLayout的dispatchTouchEvent方法,MyLayout中并没有这个方法,那就到它的父类LinearLayout中找,也没有,再找LinearLayout的父类ViewGroup,终于在ViewGroup中看到了这个方法,按钮的dispatchTouchEvent方法就是在这里开始调用的。

首先看一下ViewGroup的dispatchTouchEvent方法:
@override
public boolean dispatchTouchEvent( MotionEvent ev) {
……
boolean handled = false;
//每次一进入dispatchTouchEvent方法就会判断,如果当前是down事件就会重置状态
if(actionMasked== MotionEvent.ACTION_DOWN){
cancelAndClearTouchTarget(ev);
resetTouchState();
}
//第一部分:是否拦截
final boolean intercepted;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if(!disallowIntercept){
intercepted = onInterceptTouchEvent( ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
//如果这个ViewGroup曾经把事件交给下面的View去处理,而下面的View没有处理的话,那么下次事件ViewGroup的intercepted就直接被赋值为true,将事件拦截,自己处理
intercepted = true;
}
……
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTa rget = false;//每次进入第二部分代码之前,先将该变量置为false
//第二部分:如果不拦截,则遍历子view,选择分发给哪个子view处理事件
if(!canceled && !intercepted) {
//每次刚进入第二部分代码,就会判断是不是down事件,只有down事件时才会去遍历子view
if(actionMasked ==MotionEvent.ACTION_DOWN || (split && actionMasked ==MotionEvent.ACTION_POINTER_DOWN || actionMasked ==MotionEvent.ACTION_HOVER_MOVE) {
……
final int childrenCount = mChildrenCount;
if(newTouchTarget ==null && childrenCount != 0) {
final ArrayList< View> preOrderedList = buildTouchDispatchChildList(); //获取所有的子view,并按布局的最外层到最里层的顺序放入ArrayList中
//从最里层开始遍历子view
for(int i = childrenCount - 1; i >= 0; i–){
……
//点击事件不在该子view范围内则中断,开始循环下一个子view
if(!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x,y,child,null) {
continue;
}
newTouchTarget = getTouchTarget();
//询问子view是否处理事件,if里返回false说明子view不处理,如果所有子view都不处理,则直接进入第三部分代码;只要有一个子view的dispatchTouchEvent返回true,则说明子view处理该事件,break退出遍历子view
if(dispatchTransformedTouchEvent( ev,false,child,idBitsToAssign)) {
……
newTouchTarget = addTouchTarget(child,idBitsToAssign);
alreadyDispatchedToNewTouchTa rgeted = true;
break;
}
}
}
//第三部分:如果没有子view处理事件,则询问自己是否处理事件,否则就是子view处理(down事件直接返回true,move事件会询问子view是否处理事件)
if(mFirstTouchTarget ==null) { //没有分发给子view,则询问当前viewGroup自己是否处理
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget target = mFirstTouchTarget;
while(target != null) {
final TouchTarget next = target.next;
if(alreadyDispatchedToNewTouchTa rget && target ==newTouchTarget){
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
if(dispatchTransformedTouchEven t(ev,cancelChild,target.child,target.pointerIdBits)){
handled = true;
}
if(cancelChild) {
if(predecessor ==null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next:
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
return handled;
}

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child,pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private TouchTarget getTouchTarget(View child){
for(TouchTarget target = mFirstTouchTarget; target != null; target = target.next){
if(target.child == child){
return target;
}
}
return null;
}

ViewGroup的dispatchTouchEvent方法可以分为三部分来看:
①第一部分:判断当前ViewGroup是否拦截事件
②第二部分:如果不拦截则遍历子view,选择分发事件给哪个子view
③第三部分:如果没有子view处理事件,则询问自己是否处理事件,否则就是子view处理

其中disallowIntercept指是否禁用事件拦截功能,默认是false,可以通过调用子view调用getParent().requestDisallowInterceptTouchEvent方法对该值进行修改。

下面用源码来解答问题:
①MyLayout的onInterceptTouchEvent返回true,为什么button的事件不执行了?
在ViewGroup的dispatchTouchEvent方法中,如果是ACTION_DOWN事件,首先会调用OnInterceptTouchEvent方法,该方法返回true,则第二部分不会执行,直接进入第三部分代码,此时mFirstTouchTarget为null,调用handled = dispatchTransformedTouchEvent( ev, canceled, null, TouchTarget.ALL_POINTER_IDS),第三个参数是null说明这是在询问自己是否处理该事件(如果自己处理了则返回true,否则让上一层view去处理)。
看一下dispatchTransformedTouchEvent方法:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
……
if(child ==null) {
handled = super.dispatchTouchEvent( transformEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation( offsetX,offsetY);
if(!child.hasIdentityMatrix()) {
transformedEvent.transform( child.getInverseMatrix());
}
handled = child.dispatchTouchEvent( transformedEvent);
}
transformedEvent.recycle();
return handled;
}
因为第二部分没执行,此时第三个参数child传入的是null,意味着不分发给子view,需要当前这个viewgroup处理事件,因此调用super.dispatchTouchEvent(transformEvent);因为viewGroup的父类是view,因此这里super实际上调用的是View类里的dispatchTouchEvent方法。在该例子中,MyLayout重写了onTouch事件,所以MyLayout的dispatchTouchEvent方法会调用onTouch处理事件,所以不论点击哪个地方,都是MyLayout的事件。由于第二部分代码都没进入,整个事件没有机会分发给button,所以button不会响应任何事件。

这时思考一下,由于刚刚在MyLayout中重写了onInterceptTouchEvent方法,让这个方法返回true,导致所有按钮的点击事件都被屏蔽了,那就完全有理由相信,按钮点击事件的处理就是在第二部分代码进行的!
那重点来看下条件判断的内部是怎么实现的。通过一个for循环:for(int i = childrenCount - 1; i >= 0; i–)遍历了当前ViewGroup下的所有子View,然后通过if(!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x,y,child,null)判断当前遍历的View是不是正在点击的View,如果不是就继续遍历其他子view,直到找到点击的子view,然后通过if(dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign))判断该View是否要处理该事件,如果处理则给newTouchTarget赋值,并把alreadyDispatchToNewTouchTargeted置为true,然后到第三部分代码中直接返回true;如果没有任何一个子view处理,则到第三部分时mFirstTouchTarget为null,又询问这个ViewGroup是否处理,如果处理,这个ViewGroup的dispatchTouchEvent整个返回true,否则返回false。因此证实了,按钮点击事件的处理确实就是在这里进行的。

注意,调用子View的dispatchTouchEvent后是有返回值的。前面已经知道,如果一个view是可点击的,那点击该控件时dispatchTouchEvent的返回值必定是true。因此会导致if(dispatchTransformedTouchEvent( ev,false,child,idBitsToAssign))的条件判断成立,最终给ViewGroup的dispatchTouchEvent方法直接返回了true。也是印证了前面的Demo打印的结果,如果按钮的点击事件得到执行,就会把MyLayout的touch事件拦截掉。

那如果点击的不是按钮,而是空白区域呢?这种情况下就不会进入到第二部分代码中的if (dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign))了,而是会直接执行第三部分代码。看第三部分代码,if (mFirstTouchTarget ==null)就会进入到该条件判断内部,去询问该ViewGroup是否处理事件,因此MyLayout中注册的onTouch方法会得到执行。

至此View和ViewGroup的dispatchTouchEvent方法都讲清楚了,现在对比一下会发现:View的dispatchTouchEvent主要是通过onTouch和onTouchEvent方法处理事件。而ViewGroup的dispatchTouchEvent主要是分发事件(到底是分发给子View处理还是自己处理),如果是自己处理,就把自己当做一个View调用View的dispatchTouchEvent方法去处理自己的onTouch和onTouchEvent方法;如果分发给子view处理,就会调用该子view的dispatchTouchEvent方法。

看一下整个ViewGroup事件分发过程的流程图:
在这里插入图片描述
总结:
①onInterceptTouchEvent用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用(下一个MotionEvent的时候,在dispatchTouchEvent里判断,不是down则第一部分不会去调用onInterceptTouchEvent方法,而是直接给intercepted赋值为true,这样也导致不会进入第二部分代码,即不会再去遍历其他子view了。
②onTouchEvent方法中用来处理touch事件,返回结果表示是否消耗当前事件。如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
③正常情况下,一个事件序列只能被一个View拦截且消耗。因为一旦一个view拦截了此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
④某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。
⑤如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
⑥View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable 和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
⑦View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
⑧onClick发生的前提是当前View是可点击的,并且它收到了down和up的事件。
⑨事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

5.拦截标志位
为了让底部的控件处理事件而不被父控件拦截,一般会调用
getParent().requestDisallowInterceptTouchEvent(true);来阻止父控件对事件的拦截。

来看下它的实现原理。
view.getParent()对于底部的View来说得到的就是上层的父控件,即上层的ViewGroup,来看下ViewGroup的requestDisallowInterceptTouchEvent方法:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { //设置的禁用状态和已有的状态一致
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
if (mParent != null) {
mParent.requestDisallowInterceptTouchE vent(disallowIntercept);
}
}
首先判断是否已经设置过,如果没有就在mGroupFlags中添加FLAG_DISALLOW_INTERCEPT标记位。如果该控件还有父控件,则一层一层再往上传递。也就是说调用该方法就是将其上层所有控件的mGroupFlags中添加上FLAG_DISALLOW_INTERCEPT标记位。

那怎么起到让父控件不拦截事件的作用呢?
在ViewGroup的dispatchTouchEvent方法里,有如下代码:
if(actionMasked==MotionEvent.ACTION_DOWN){
cancelAndClearTouchTargets(ev);
resetTouchState();
}
然后看下resetTouchState方法:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
也就是说如果是down事件的起点,就会清除掉禁用拦截的标记位。

接着就是关键点,决定是否调用onInterceptTouchEvent方法。看第一部分中第一个if中的判断语句为
actionMasked ==MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
也就是说必须满足至少一个条件才有可能去执行onInterceptTouchEvent方法。
看下mFirstTouchTarget的说明 : First touch target in the linked list of touch targets.
即mFirstTouchTarget代表这个ViewGroup下第一个处理了事件的控件。
也就是说要么为ACTION_DOWN事件,要么底下的控件处理了事件,控件才可能去执行onInterceptTouchEvent方法。反言之,如果这个ViewGroup曾经把事件交给下面的View去处理,而下面的View没有处理的话,那么下次事件ViewGroup的intercepted就直接被赋值为true,即将事件拦截,自己处理。

接着看第一部分的第二个if判断语句:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
disallowIntercept是禁用拦截的标记位。
如果disallowIntercept为false,也就是默认值,那么就会走正常的onInterceptTouchEvent去判断是否拦截,像RelativeLayout、LinearLayout这样的一般不需处理事件的ViewGroup一般都会返回false,事件还是会继续传递下去;但是像RecyclerView、ViewPager等,它们往往会根据判断事件的具体情况决定是否拦截,可能自己就将事件消费了。而如果disallowIntercept为true的话,即设置不允许父控件拦截事件,那么它的onInterceptTouchEvent方法就不会执行,intercepted直接设置为false,即不拦截事件,事件会传递给下层的View,当然了如果最终下面的View没有处理事件的话,之后的事件在第一个if判断的时候就会决定事件不再往下传递了。

6.Down、up事件的分发过程

7.以ViewPager内嵌套ListView来具体分析一下源码:
新建一个app,自定义MyViewPager和MyListView:
public class MyViewPager extends ViewPager {
……//默认构造方法2个
@override
public boolean onInterceptTouchEvent( MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
}
public class MyListView extends ListView {
……//默认构造方法3个
}
在MainActivity中完成ViewPager内嵌套ListView的适配操作。
activity_main.xml:
< FrameLayout>
< MyViewPager />
< /FrameLayout>
整体布局就是ViewPager的每一页都是一个ListView。
在不做任何处理时,可以同时实现ViewPager左右滑动和ListView上下滑动,这是因为谷歌开发者已经把touch事件冲突解决好了。
下面开始做一些修改:
①让ViewPager的onInterceptTouchEvent方法返回true——现象:ViewPager可以左右滑,ListView不能滑动。
解释:手指触碰屏幕时,第一个事件是down,down首先会传递到ViewPager的dispatchTouchEvent方法,因为是down事件,所以会重置状态(导致在down事件时即使在子view里设置getParent().requestDisallowInterceptTouchEvent为true,父view的onInterceptTouchEvent仍然会执行,也就是说当事件为down时,父view的onInterceptTouchEvent一定会执行!),在dispatchTouchEvent方法的第一部分代码中,满足if条件并且disallowIntrcept为false,所以执行onInterceptTouchEvent方法,因为这里重写了该方法返回true,因此intercepted为true。intercepted为true导致第二部分代码无法执行,直接进入第三部分代码,此时mFirstTouchTarget为null(mFirstTouchTarget指向获取down事件的子view,为空表示没有子控件消耗),执行if里的dispatchTransformedTouchEvent方法询问父view是否处理该事件,父view是ViewPager,在ViewPager代码内部onTouchEvent方法对该事件进行了处理,down之后的一系列动作都会由ViewPager处理。所以ViewPager可以滑动,而ListView不能滑动(因为第二部分代码都不能进入,直接不分发事件给子view,导致ListView接收不到事件)
②让ViewPager的onInterceptTouchEvent方法返回false——现象:ViewPager不可以左右滑,ListView能滑动。
解释:第一个事件down首先传递到ViewPager的dispatchTouchEvent方法,同样必须执行onInterceptTouchEvent,该方法返回false,因此intercepted为false,则进入第二部分代码,在第二部分代码中会首先遍历ViewPager下的子view( 只有ListView),询问子view是否处理该事件。ListView中的onTouchEvent对该事件做了处理,因此导致mFirstTouchTarget不为空,且alreadyDispatchToNewTouchTargeted为true。
然后进入第三部分代码,if条件不满足,走else里由于alreadyDispatchToNewTouchTargeted为true,直接返回true了。因此此时down事件是由子view(ListView)处理的,父view(ViewPager)没有处理。down之后的一系列动作都会交由ListView处理。
③让ViewPager的onInterceptTouchEvent方法返回false,ListView的dispatchTouchEvent方法返回false——现象:ViewPager可以左右滑动,ListView不能上下滑动
解释:down事件还是先传递到ViewPager的dispatchTouchEvent中,它的onInterceptTouchEvent方法一定执行,因为onInterceptTouchEvent返回false,所以intercepted为false,此时进入第二部分代码开始遍历子view,然后在dispatchTransformedTouchEvent方法中调用子view(ListView)的dispatchTouchEvent方法,因为该方法返回false,则不会给mFirstTouchTarget和alreadyDispatchToNewTouchTargeted赋值。然后进入第三部分代码,此时由于mFirstTouchTarget为null,会进入if里,在dispatchTransformedTouchEvent方法里调用父view(ViewPager)的dispatchTouchEvent方法。因此该down事件由ViewPager处理,后续一系列动作也会交由ViewPager处理。因此,ViewPager可以左右滑动,ListView不能上下滑动。
④修改ViewPager的onInterceptTouchEvent方法如下:
@override
public boolean onInterceptTouchEvent( MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent();
return false;
}
return true;
}
使ViewPager的onInterceptTouchEvent方法在down事件时返回false,在除down以外的其他事件时返回true。
同时修改ListView的dispatchTouchEvent方法:
@override
public boolean dispatchTouchEvent( MotionEvent ev){
int x = (int) ev.getX();
int y = (int) ev.getY();
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent( ).requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(Math.abs(deltaX) > Math.abs(deltaY)){
getParent( ).requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
这样修改后,现象:ViewPager和ListView同时都能滑动。
解释:down事件依然先传递到ViewPager的dispatchTouchEvent中,虽然在ListView的dispatchTouchEvent里对down事件进行了requestDisallowInterceptTouchEvent(true)处理,设置了禁用拦截标志位,但是由于是down事件,父view依然会走onInterceptTouchEvent方法,进入第一部分代码里:
if(actionMasked ==MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntrcept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if(!disallowIntrcept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else{
intercepted = false;
}
} else {
intercepted = true;
}
disallowIntrcept为false,所以ViewPager的onInterceptTouchEvent方法一定执行,由于重写了onInterceptTouchEvent使它在down事件时返回false,所以intercepted为false。
然后进入第二部分代码遍历子view,在dispatchTransformedTouchEvent方法中调用子view(ListView)的dispatchTouchEvent,重写ListView的dispatchTouchEvent方法时返回的是super.dispatchTouchEvent,因此这个down事件就被子view处理了,同时给mFirstTouchTarget和alreadyDispatchToNewTouchTargeted赋值。然后进入第三部分代码,由于mFirstTouchTarget不为null,会走else部分,在else的while循环里,由于alreadyDispatchToNewTouchTargeted为true,会使ViewPager的dispatchTouchEvent方法直接返回true。看第三部分源码:
if(mFirstTouchTarget ==null) {
handled = dispatchTransformedTouchEvent( ev,canceled,null,TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget target = mFirstTouchTarget;
while(target != null) {
final TouchTarget next = target.next;
if(alreadyDispatchToNewTouchTargeted && target ==newTouchTarget) {
handled = true;
} else {
……
}
}
}
接下来move事件来了,move事件依然是先进入ViewPager的dispatchTouchEvent方法,由于不是down事件因此不会重置状态。由于此时mFirstTouchTarget不为null,满足第一部分代码的if条件,因此进入第一部分代码,在这里,由于刚才在down的时候子view设置了requestDisallowInterceptTouchEvent(true)(这里不要乱,在down事件时,先进的ViewPager的dispatchTouchEvent,在这里由于是down就清除了禁用拦截标志位,然后才进入的ListIew的dispatchTouchEvent方法,在这个方法的down里设置了禁用拦截标志位,所以进move之前是有禁用拦截标志位的),因此disallowIntrcept为true,则不会进入onInterceptTouchEvent(虽然在ViewPager的onInterceptTouchEvent的move事件时返回true ,但是进不去啊),所以此时intercepted为false,然后进入第二部分代码,由于在第二部分代码里给子view分发事件有一个大前提,就是必须是down事件,此时是move,不满足,直接退出第二部分代码。看第二部分源码:
if(!canceled && !intercepted) {
if(actionMasked
==MotionEvent.ACTION_DOWN){
……
}
}
退出第二部分代码后,进入第三部分代码,由于mFirstTouchTarget不为空,进入else,else里是一个while循环,看这个while源码:
while(target != null){
final TouchTarget next = target.next;
if(alreadyDispatchToNewTouchTargeted && target ==newTouchTarget){
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
if(dispatchTransformedTouchEvent( ev,cancelChild,target.child,target.pointerIdBits)){
handled = true;
}
if(cancelChild){
if(predecessor ==null){
mFirstTouchTarget = next;
} else{
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target:
target = next;
}
此时判断alreadyDispatchToNewTouchTargeted是true还是false呢?答案是false,因为每次进入dispatchTouchEvent方法的第二部分代码之前都会先将alreadyDispatchToNewTouchTargeted置为false!那么这时候进入else里,在这个else里调用dispatchTransformedTouchEvent方法询问target.child(这里是ListView)是不是处理事件,所以这个move依然是子view处理的。
由于在重写ListView的dispatchTouchEvent方法时,在move事件里x轴滑动距离大于y轴滑动距离会执行requestDisallowInterceptTouchEvent(false),接下来该分情况了:如果x轴滑动距离大于y轴滑动距离时,这时候的move进入ViewPager的dispatchTouchEvent方法,在第一部分代码里disallowIntrcept为false,则调用ViewPager的onInterceptTouchEvent方法,因为重写onInterceptTouchEvent时,除了down事件都是返回true,所以此时intercepted为true,因此不会进入第二部分代码,直接进入第三部分,在第三部分代码里,mFirstTouchTarget不为空,走else,alreadyDispatchToNewTouchTargeted为false,走else,此时要注意了,有一个变量cancelChild,看源码:
cancelChild=resetCancelNextUpFlag(target.child) || intercepted;
此时由于执行过requestDisallowInterceptTouchEvent(false),因此intercepted为true,所以cancelChild也为true。此时调用dispatchTransformedTouchEvent方法询问子View是否处理事件时,跟以前就不一样了,看源码:
private boolean dispatchTransformedTouchEvent (MotionEvent event,boolean cancel,View child,int desired pointerIdBits){
final boolean handled;
final int oldAction = event.getAction();
if(cancel || oldAction
==MotionEvent.ACTION_CANCEL){
event.setAction( MotionEvent.ACTION_CANCEL);
if(child ==null){
handled=super.dispatchTouchEvent( event);
} else {
handled= child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
………
}
可以看到,此时传入的第二个参数cancel为true,所以进入第一个if里,进来后将event事件设置为ACTION_CANCEL,由于child不为空,所以调用child(ListView)的dispatchTouchEvent方法去处理cancel事件。处理完cancel事件后,返回父view的第二部分代码继续向下执行,由于此时cancelChild为true,会使mFirstTouchTarget变为为next,在这个例子里,由于是单指操作,next为空,因此此时mFirstTouchTarget变为了null。
接下来又会来一个move,先进入ViewPager的dispatchTouchEvent方法,在第一部分代码里,由于此时不是down事件,而且mFirstTouchTarget为null,intercepted直接为true,导致第二部分代码不会执行,直接进入第三部分代码,此时由于mFirstTouchTarget为null,则会询问父view是否处理事件,ViewPager类的dispatchTouchEvent就接到了事件处理权。到这里我们可以看到,这一系列动作由刚开始的子view处理,在x滑动距离大于y滑动距离的情况下,已经交由父view处理,其他情况子view处理。这就是内部拦截法。
还有一个外部拦截法,原理是事件交由父view根据情况进行分发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值