勤奋可以弥补聪明的不足,但聪明无法弥补懒惰的缺陷。
最近工作挺忙的,但是感觉不写博客的话,心里空荡荡的,每写一篇博客心里都踏实很多,也不知道写博客能坚持多久,但是我会继续努力认真学习每一个知识点。废话不多说了,进入正题吧。
在上一篇文章中我们详细介绍了View的事件分发,在学习ViewGroup的事件分发之前最好先学习一下Android事件分发机制——View(一),在了解了View的事件分发机制之后来学习
ViewGroup的事件分发就简单多了,我们一起来探讨一下吧,如有谬误欢迎批评指正,如有疑问欢迎留言。(注:本文采用的是Android 2.3的源码)
1.ViewGroup相关知识
在探讨事件分发机制之前,我们必须明白android两个基础控件view和viewgroup,以及它们之间的关系:View是没有子控件的,像button,textview都是view控件。而viewgroup继承自view,是可以存在子控件的,像LinearLayout,FrameLayout等。也就是说Viewgroup就是一组View或者是Viewroup的集合,它是所有页面布局的父类。View在ViewGroup内,ViewGroup也可以在其他ViewGroup内,这时候把内部的ViewGroup当成View来分析。
ViewGroup的继承关系图如下
ViewGroup的相关事件有三个:
onInterceptTouchEvent---------负责事件的拦截
dispatchTouchEvent-------------负责事件分发
onTouchEvent--------------负责事件的处理。
2.案例
我们先从一个案例说起这个案例很简单包含一个自定义的MyLinearLayout在其中有一个自定义的MyButton。
MyLinearLayout的源码
package com.example.viewgrouppractice; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; public class MyLinearLayout extends LinearLayout { private static final String TAG = "MyLinearLayout"; public MyLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.i(TAG,"MyLinearLayout--dispatchTouchEvent--ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG,"MyLinearLayout--dispatchTouchEvent--ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(TAG,"MyLinearLayout--dispatchTouchEvent--ACTION_UP"); break; } return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.i(TAG,"MyLinearLayout--onInterceptTouchEvent--ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG,"MyLinearLayout--onInterceptTouchEvent--ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(TAG,"MyLinearLayout--onInterceptTouchEvent--ACTION_UP"); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.i(TAG,"MyLinearLayout--onTouchEvent--ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG,"MyLinearLayout--onTouchEvent--ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(TAG,"MyLinearLayout--onTouchEvent--ACTION_UP"); break; } return super.onTouchEvent(event); } }
可以看到我们只是添加了日志的打印,其它的都和系统默认的是一样的。
package com.example.viewgrouppractice; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.Button; public class MyButton extends Button { private static final String TAG = "MyLinearLayout"; public MyButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.i(TAG,"MyButton--dispatchTouchEvent--ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG,"MyButton--dispatchTouchEvent--ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(TAG,"MyButton--dispatchTouchEvent--ACTION_UP"); break; } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.i(TAG,"MyButton--onTouchEvent--ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.i(TAG,"MyButton--onTouchEvent--ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.i(TAG,"MyButton--onTouchEvent--ACTION_UP"); break; } return super.onTouchEvent(event); } }
同样MyButton也是只添加了日志的打印
MainActivity的布局文件如下
<com.example.viewgrouppractice.MyLinearLayout android:id="@+id/ll_main" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.example.viewgrouppractice.MyButton android:id="@+id/mbtn_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="test" /> </com.example.viewgrouppractice.MyLinearLayout>
清楚的了解了这些后,我们来运行下程序,点击按钮并滑动一下会有如下的日志打印
此时点击MyLinearLayout的空白区域的日志如下
然后将MyLinearLayout的onInterceptTouchEvent方法的返回值直接return true然后点击按钮和空白区域发现打印的日志相同如下
在上面的日志打印中第一张图我们对事件的传递没有做任何的人为的改变,因此它也是系统默认的打印,认真的看日志我们会发现事件的执行顺序是:
MyLinearLayout的dispatchTouchEvent---->MyLinearLayout的onInterceptTouchEvent--->MyButton的dispatchTouchEvent---->MyButton的onTouchEvent
为什么日志会这样打印呢?ViewGroup的事件到底是怎么分发的呢?唯有源码更具有说服力,说到源码我们首先想到的应该是ViewGroup的dispatchTouchEvent方法,好下面我们就来分析分析ViewGroup的事件分发的相关源码。
3.分析源码
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; /** * disallowIntercept表示是否允许事件拦截,默认是false,即不拦截事件 * 此值可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法进行设置 */ boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { /** * 因为从ACTION_DOWN开始要开启新一轮的事件分发,所有要将mMotionTarget(目标)置为空 */ if (mMotionTarget != null) { // this is weird, we got a pen down, but we thought it was // already down! // XXX: We should probably send an ACTION_UP to the current // target. mMotionTarget = null; } // If we're disallowing intercept or if we're allowing and we didn't // intercept /** * 当disallowIntercept(默认是false)为true,或者onInterceptTouchEvent(ev)(默认返回为false)方法的返回值 * 为false,取反则为true,则判断条件成立 */ if (disallowIntercept || !onInterceptTouchEvent(ev)) { // reset this event's action (just to protect ourselves) ev.setAction(MotionEvent.ACTION_DOWN); // We know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; /** * 遍历当前ViewGroup的所有子View */ for (int i = count - 1; i >= 0; i--) { final View child = children[i]; /** * 如果当前的View是VISIBLE的或者有动画执行 */ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); /** * 如果子View包含当前触摸的点 */ if (frame.contains(scrolledXInt, scrolledYInt)) { // offset the event to the view's coordinate system 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)) { // Event handled, we have a target now. /** * 如果子View的dispatchTouchEvent方法的返回值为true,则表示子View已经消费了事件 * 此时将子View赋值给mMotionTarget */ mMotionTarget = child; /** * 直接返回true,表示down事件被消费掉了 */ return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { // Note, we've already copied the previous state to our local // variable, so this takes effect on the next event /** * 如果是ACTION_UP或者ACTION_CANCEL, 将disallowIntercept设置为默认的false * 因为ACTION_UP或者ACTION_CANCEL表示事件执行完,要将前面设置的值复位 */ mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; /** * 当mMotionTarget为空表示没有找到消费事件的View,此时需要调用ViewGroup父类的dispatchTouchEvent方法, * ViewGroup的父类即为View */ if (target == null) { // We don't have a target, this means we're handling the // event as a regular view. 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 have a target, see if we're allowed to and want to intercept its // events /** * 如果执行到此说明target!=null,然后判断是否允许拦截和是否想要拦截 * 如果允许拦截(!disallowIntercept=true),并且想要拦截(onInterceptTouchEvent(ev)返回值为true) * 则条件成立 */ if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; /** * 设置动作为ACTION_CANCEL,此处与ev.getAction相对应 */ ev.setAction(MotionEvent.ACTION_CANCEL); /** * */ ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { // target didn't handle ACTION_CANCEL. not much we can do // but they should have. } // clear the target /** * */ mMotionTarget = null; // Don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal onTouchEvent(). return true; } if (isUpOrCancel) { mMotionTarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. 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; } /** * 将事件分发给target返回target.dispatchTouchEvent(ev)的返回值 */ return target.dispatchTouchEvent(ev); }
上面的注释已经很详细了,但是为了能够理解的更加清楚,我们再来详细的分析一遍,我们首先分析下ViewGroup的事件分发的流程首先第14行会
进入第一个事件ACTION_DOWN的判断在这判断的内部首先会将mMotionTarget置为空,然后进入 if (disallowIntercept ||
!onInterceptTouchEvent(ev))这个判断,这个判断的条件有两个
①disallowIntercept:表示当前不允许拦截,如果为false就双重否定等于肯定,即允许拦截,如果为true就表示不允许拦截。它的默认值为false,它的值可以通过viewGroup.requestDisallowInterceptTouchEvent(boolean);进行设置
②!onInterceptTouchEvent:对onInterceptTouchEvent的返回值进行取反操作,它的值我们可以通过在ViewGroup中复写onInterceptTouchEvent这个方法进行改变,就像上面的案例那样1.如果onInterceptTouchEvent的返回值为false那么取反后就为true就会进入到if语句内部
2.如果onInterceptTouchEvent的返回值为true那么取反后就为false,就会跳出这个if判断。
我们首先看下第一种情况if语句的内部是怎么实现的,第43行遍历所有的子View,然后判断当前点击的“点”是否在当前所遍历到的
子View内,这里的子View有两种可能
①子View是ViewGroup则递归的去遍历。
②子View是View如果在当前所遍历的子View内,那么调用View的dispatchTouchEvent方法,之后就进入了View的事件分发,参考View的事件分发机制
在调用View的dispatchTouchEvent方法时如果此方法返回true则表示已经找到消费事件的View,将mMotionTarget = child然后return true;表示
down事件已经消费了。ACTION_DOWN后面的代码也就无法执行了。如果此方法返回false则mMotionTarget ==null,此时会进入到102行调用View的
dispatchTouchEvent方法到这里ACTION_DOWN的逻辑就走完了。
在ACTION_DOWN中如果我们找到了消费事件的View就会执行mMotionTarget = child此时mMotionTarget !=null,我们进行Move操作
它会怎么执行呢?它会进入到第120行的逻辑为了便于查看我们将代码复制到下面
/** * 如果执行到此说明target!=null,然后判断是否允许拦截和是否想要拦截 * 如果允许拦截(!disallowIntercept=true),并且想要拦截(onInterceptTouchEvent(ev)返回值为true) * 则条件成立 */ if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; /** * 设置动作为ACTION_CANCEL,此处与ev.getAction相对应 */ ev.setAction(MotionEvent.ACTION_CANCEL); /** * */ ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { // target didn't handle ACTION_CANCEL. not much we can do // but they should have. } // clear the target /** * */ mMotionTarget = null; // Don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal onTouchEvent(). return true; }
这个if判断语句有两个条件
①!disallowIntercept:对disallowIntercept取反,如果disallowIntercept=false(允许拦截)则!disallowIntercept=true
②onInterceptTouchEvent:ViewGroup中onInterceptTouchEvent的返回值,onInterceptTouchEvent=true表示我要拦截
这两个条件是与的关系即当ViewGroup允许拦截并且我要拦截时会进入if语句的内部进行事件的拦截,它拦截的方式就是将mMotionTarget置为null然后返回true,因为mMotionTarget==null所以事件就不会分发给子View了。如果没有进行拦截,则它会执行165行的target.dispatchTouchEvent最后当为ACTION_UP时进入147行将mMotionTarget置为null即复位,准备下次事件的触发。到这里ViewGroup的事件分发就分析完了,我们通过一张图来把上面的流程描绘出来
现在你能分析我们刚开始的案例的日志打印吗?
案例的分析:
在ViewGroup的事件分发过程中首先会调用ViewGroup的dispatchTouchEvent方法然后在ACTION_DOWN中会调用onInterceptTouchEvent由于默认情况
下onInterceptTouchEvent的返回值是false所以进入if语句在if语句里遍历所有子View找到消费事件的View,接着就进入了View的
dispatchTouchEvent。所以案例中的第一张图会那样打印
而点击除Button之外的空白区域由于没有找到消费事件的View,mMontionTarget==null,所以会调用super.dispatchTouchEvent,由
于ViewGroup的父类是View所以此时的MyLinearLayout就相当于一个View,调用调用super.dispatchTouchEvent也就相当于调用
View.dispatchTouchEvent,而MyLinearLayout是不可点击的根据我们上篇的View的事件分发机制可以知道此时只会触发Down事件所
以第二张图会那样打印。
而当我们将onInterceptTouchEvent的返回值置为true时,mMontionTarget始终为null此时就会调用super.dispatchTouchEvent此时的
super.dispatchTouchEvent就是View的dispatchTouchEvent原理和上面说的一样。
4.开发中常遇到的问题
在实际的开发中我们有可能会遇到这样的问题
如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截子View的MOVE以及UP事件;
此时子View希望依然能够响应MOVE和UP时我们应该怎么办呢?
在上面我们也提到过可以调用ViewGroup的requestDisallowInterceptTouchEvent方法我们可以采用如下的形式
@Override public boolean dispatchTouchEvent(MotionEvent event) { getParent().requestDisallowInterceptTouchEvent(true); int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(event); }getParent().requestDisallowInterceptTouchEvent(true); 这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。
这一点可以从我们上面分析的ViewGroup的拦截的代码中看出,也就是120-145行,我们会看到这两个条件是与的关系只要有一个为
false则会跳过拦截事件的代码,
5.总结
1.Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
2.在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件
继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
3.子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦
截;
5.假如我们在某个ViewGroup的onInterceptTouchEvent中,将Action为Down的Touch事件返回true,那便表示将该ViewGroup的所有
下发操作拦截掉,这种情况下,mTarget会一直为null,因为mTarget是在Down事件中赋值的。由于mTarge为null,该ViewGroup的
onTouchEvent事件被执行。这种情况下可以把这个ViewGroup直接当成View来对待。
6.当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接
进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在
的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,ViewGroup1
也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至
TextView。