请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45071069,非允许请勿用于商业或盈利用途,违者必究。


Android触摸事件,网上也有很多文章来讲了,今天在这里想使用例子和源码相结合的方式,可能会看的更清晰一些。

在讲例子和源码之前,还是先把结论讲一下,这样可能会比较好,因为很多朋友时间都很宝贵,而研究源码可能会要花费不少时间,可以先初步理解事件的分发机制,等有时间再来慢慢琢磨源码。


触摸事件的传递机制


首先是最外层的viewgroup接收到事件,然后调用会调用自己的dispatchTouchEvent方法。如果在ACTION_DOWN的时候dispatchTouchEvent返回false则后续的ACTION_MOVE和ACTION_UP都接收不到了,如果在ACTION_DOWN的时候dispatchTouchEvent返回true,则在后续的动作都可以继续分发下去;


dispatchTouchEvent方法的调用过程中先会经过onInterceptTouchEvent方法判断,如果onInterceptTouchEvent返回true则会拦截,最终传递给本viewgroup的onTouchEvent方法;如果返回false,则不拦截,传递给子view/viewgroup的dispatchTouchEvent;而这个传递给子view/viewGroup的过程是这样的:


先会遍历所有的直属子view/ViewGroup ,看看点击事件是发生在哪个直属子view/ViewGroup上,确定之后,将事件传递给对应的直属直属子view/ViewGroup,直属子view/ViewGroup再调用自己的dispatchTouchEvent进行分发。。。这样循环,直到某一级ViewGroup的onInterceptTouchEvent进行拦截,onInterceptTouchEvent返回true传递给自己的onTouchEvent方法或者一直没有任何ViewGroup的onInterceptTouchEvent方法拦截事件,而事件传递到最终(最底层)的子view的onTouchEvent方法的时候,这时候如果onTouchEvent方法返回true,则会消费掉本次事件,如果这时候onTouchEvent方法返回false。。,则事件会依次向上传递,先传递给自己的上一级的view/viewGroup的onTouchEvent方法,然后依次上传,直到某一级的onTouchEvent方法返回true,消费掉本次事件,或者没有任何一个onTouchEvent方法方法消费掉本次事件,最后事件在最上一层onTouchEvent方法返回false之后消失掉。


onInterceptTouchEvent方法可以提供一个拦截能力,但是onInterceptTouchEvent方法只有在viewGroup里面才有,所以只有 viewGroup才有拦截事件的能力。


对于dispatchTouchEvent和onInterceptTouchEvent可以这样理解,dispatchTouchEvent方法是一个快递员,onInterceptTouchEvent方法是公司的门卫,快递员要给公司送的每批快递就是一个完整的触摸事件,每一批快递有一个为首的物品:Down事件;送货有一个规定:如果这批快递的为首的这个物品(Down)被门卫(onInterceptTouchEvent)给拦截了,那么这批快递之后的其他物品(Move,Up等)都不能通过门卫,而只有在第一个物品(Down)事件被门卫(onInterceptTouchEvent)放行的情况下,这批快递之后的其他物品才有可能投递成功。



一些要点:


1、Touch事件是由硬件捕获到触摸后由系统传递给应用的ViewRoot,再由ViewRoot往下一层一层传递.


2、处理过程都是自上而下的分发,可以看成是由布局的“包含”关系,自顶向下的方式


3、事件存在消耗,事件的处理方法都会返回一个boolean值,如果该值为true,则本次事件下发将会被消费掉,而不会继续往下一层传递.


4、Touch事件从ACTION_DOWN开始,也就是按下的这个action开始算起,到ACTION_UP抬起时结束;但是如果在ACTION_DOWN的时候,没有接受事件,那么后续的其他动作也将不会被接受


5、dispatchTouchEvent方法,是用来分发上层传递过来的事件的,它在View和ViewGroup中都有实现


6、onInterceptTouchEvent方法,是用来拦截事件传递的,它只在ViewGroup中有实现,在View中没有


7、view对象的TouchLitener中的onTouch方法总是会先于view自己的onTouchEvent(event)方法被执行,这是由View中的dispatchEvent方法决定。


8、Activity中的onTouchEvent只会在能响应的所有view都响应完事件,且都没有消费掉事件之后才会被调用。


9、如果一个ViewGroup被点击的地方,有多个子View/ViewGroup可以响应点击事件,那么它们响应的顺序是:后addView进来的子view/ViewGroup先响应事件或者是xml布局文件中后被添加的view先 响应触摸事件



触摸事件例子:


先来看一个简单的例子:这是一个底层布局为FrameLayout,其中又有一个RelativeLayout,RelativeLayout中又有一个TextView;这里说的“中”是指addView的关系。我们这里都使用自定义view的形式来实现,然后分别在MyFrameLayout,MyRelativeLayout和MyTextView中实现dispatchTouchEvent方法,并打印相关信息:


三层结构代码:


package com.example.eventdispatch;import android.app.Activity;import android.graphics.Color;import android.os.Bundle;import android.view.Gravity;import android.view.View;import android.widget.FrameLayout;import android.widget.FrameLayout.LayoutParams;import android.widget.RelativeLayout;public class MainActivity extends Activity {View view = null;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(initView());}private View initView() {// 初始化三个控件MyFrameLayout mFrameLayout = new MyFrameLayout(this);MyRelativeLayout mRelativeLayout = new MyRelativeLayout(this);MyTextView myTextView = new MyTextView(this);// 分别设置LayoutParamsLayoutParams mFrameLayoutParams = new FrameLayout.LayoutParams(200, 200);android.widget.RelativeLayout.LayoutParams mRelativeLayoutParams = new RelativeLayout.LayoutParams(100, 100);// 将RelativeLayout添加到FrameLayout中mFrameLayout.addView(mRelativeLayout, mFrameLayoutParams);// 将TextView添加到RelativeLayout中mRelativeLayout.addView(myTextView, mRelativeLayoutParams);// 设置Gravity,居中mRelativeLayout.setGravity(Gravity.CENTER);mFrameLayoutParams.gravity = Gravity.CENTER;// 设置三个控件的颜色mFrameLayout.setBackgroundColor(Color.RED);mRelativeLayout.setBackgroundColor(Color.GREEN);myTextView.setBackgroundColor(Color.BLUE);//将FrameLayout返回,作为Activity显示的Viewreturn mFrameLayout;}}


FrameLayout:

package com.example.eventdispatch;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.widget.FrameLayout;/** * @author : 苦咖啡 *  * @version : 1.0 *  * @date :2015年4月15日 *  * @blog : http://blog.csdn.net/cyp331203 *  * @desc : */public class MyFrameLayout extends FrameLayout {public MyFrameLayout(Context context, AttributeSet attrs, int defStyleAttr,int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}public MyFrameLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public MyFrameLayout(Context context, AttributeSet attrs) {super(context, attrs);}public MyFrameLayout(Context context) {super(context);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {System.out.println("--MyFrameLayout-->dispatchTouchEvent-->start");boolean b = super.dispatchTouchEvent(ev);System.out.println("--MyFrameLayout-->dispatchTouchEvent-->end-->"+ ev.getAction() + "-->" + b);return b;}}

MyRelativeLayout:

package com.example.eventdispatch;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.widget.RelativeLayout;/** * @author : 苦咖啡 *  * @version : 1.0 *  * @date :2015年4月15日 *  * @blog : http://blog.csdn.net/cyp331203 *  * @desc : */public class MyRelativeLayout extends RelativeLayout {public MyRelativeLayout(Context context, AttributeSet attrs,int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);// TODO Auto-generated constructor stub}public MyRelativeLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);// TODO Auto-generated constructor stub}public MyRelativeLayout(Context context, AttributeSet attrs) {super(context, attrs);// TODO Auto-generated constructor stub}public MyRelativeLayout(Context context) {super(context);// TODO Auto-generated constructor stub}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {System.out.println("--MyLinearLayout-->dispatchTouchEvent-->start");boolean b = super.dispatchTouchEvent(ev);System.out.println("--MyLinearLayout-->dispatchTouchEvent-->end-->"+ ev.getAction() + "-->" + b);return b;}}

MyTextView:

package com.example.eventdispatch;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.widget.TextView;/** * @author : 苦咖啡 *  * @version : 1.0 *  * @date :2015年4月15日 *  * @blog : http://blog.csdn.net/cyp331203 *  * @desc : */public class MyTextView extends TextView {public MyTextView(Context context, AttributeSet attrs, int defStyleAttr,int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}public MyTextView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public MyTextView(Context context, AttributeSet attrs) {super(context, attrs);}public MyTextView(Context context) {super(context);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {System.out.println("--MyTextView-->dispatchTouchEvent-->start");boolean b = super.dispatchTouchEvent(ev);System.out.println("--MyTextView-->dispatchTouchEvent-->end-->"+ ev.getAction() + "-->" + b);return b;}}


运行界面结构和UI关系如图:

红色为MyFrameLayout,绿色为MyRelativeLayout,蓝色为MyTextView




下面我们分别触摸红色(MyFrameLayout),绿色(MyRelativeLayout)和蓝色区域(MyTextView):


触摸红色(MyFrameLayout)打印信息:


触摸绿色(MyRelativeLayout)打印信息:



触摸蓝色区域(MyTextView)打印信息:



我们会发现三者的事件分发是包含关系:

MyTextView的dispatchToutchEvent方法是在MyRelativeLayout的dispatchToutchEvent方法调用的过程之中被执行完毕的,而MyRelativeLayout的dispatchToutchEvent方法是在MyFrameLayout的dispatchToutchEvent方法执行过程之中被执行完毕的。


下面我们将addView的方式改变一下,让MyTextView作为MyFrameLayout的直接子View,而不再是MyRelativeLayout的子view,而且让MyTextView在MyRelativeLayout之后被addView添加进MyFrameLayout中:


android.widget.FrameLayout.LayoutParams mFrameLayoutParams2 = new FrameLayout.LayoutParams(100, 100);// 将RelativeLayout添加到FrameLayout中mFrameLayout.addView(mRelativeLayout, mFrameLayoutParams);// 将TextView添加到FrameLayout中mFrameLayout.addView(myTextView, mFrameLayoutParams2);


这时,界面UI关系如图:




然后我们再一次点击蓝色区域,打印的信息如下:




我们发现,这一次打印的信息与之前点击蓝色MyTextView区域时的打印信息不一样,MyTextView的dispatchTouchEvent方法并没有在MyRelativeLayout的dispatchTouchEvent方法内被调用,而是在MyFrameLayout的dispatchToutchEvent方法执行过程之中被执行完毕的;而且MyTextView的dispatchTouchEvent方法在MyRelativeLayout的dispatchTouchEvent方法开始之前执行就已经执行完毕了。


这是为什么呢?我们暂且留下这个问题


层级关系:


下面,我们就从UI层级结构和源码出来,来一步步搞清楚这几个问题。

先来看看第一个例子的UI的层级关系图,为了简明起见,我们在setContentView之前加上一句:requestWindowFeature(Window.FEATURE_NO_TITLE);不显示ActionBar,这样会更清晰一些,层级图如下:



上图中的LineareLayout和FrameLayout以及ViewStub本来是与ActionBar相关的组件,由于我们添加了requestWindowFeature(Window.FEATURE_NO_TITLE);不显示ActionBar,所以变成了现在的这个布局。

我们可以看到我们自己写的MyFrameLayout、MyRelativeLayout和MyTextView并不是直接挂载在view树的根节点上,根节点是一个PhoneWindow类中的内部类DecorView对象,这是个什么玩意儿呢?我们可以从Activity的源码来看看:


在MainActivity中,我们调用setContentView来设置我们自己定义的布局的根View/ViewGroup,Activity中的setContentView是这样的:

    public void setContentView(View view) {        getWindow().setContentView(view);        initActionBar();//初始化ActionBar,这一句忽略,今天关注Touch事件    }

我们可以看到它实际上是调用getWindow()方法的返回值的setContentView方法;


再来看看getWindow()方法:

    public Window getWindow() {        return mWindow;    }

发现返回的是一个mWindow,而这个mWindow是一个Activity类中 Window类型的成员变量:

private Window mWindow;

可能你已经在猜测这个window和PhoneWindow的关系了,Window是一个抽象类,其中的setContentView方法也是一个抽象方法,并没有实现。来看看Window类的注释:


The only existing implementation of this abstract class is android.policy.PhoneWindow, which you should instantiate when needing a Window.

意思是说:Window类只有一个实现类,那就是PhoneWindow。


这下明白了,我们再去看看PhoneWindow类的源码,这个类我们并不能直接使用,它位于:Android源码目录/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java


在PhoneWindow中,有一个成员变量:DecorView mDecor,和一个内部类DecorView。那么这个DecorView和本文关注的触摸事件分发有什么联系呢?



系统有一个线程在循环收集屏幕硬件信息,当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装成一个MotionEvent对象,然后把该对象存放到一个消息队列中。


系统的另一个线程循环的读取消息队列中的MotionEvent,然后交给WMS去派发,WMS把该事件派发给当前处于活动的Activity,即处于活动栈最顶端的Activity.


这就是一个先进先出的消费者和生产者的模板,一个线程不停的创建MotionEvent对象放入队列中,另一个线程不断的从队列中取出MotionEvent对象进行分发.


当用户的手指从接触屏幕到离开屏幕,是一个完整的触摸事件,在该事件中,系统会不断收集事件信息封装成MotionEvent对象.收集的间隔时间取决于硬件设备,例如屏幕的灵敏度以及cpu的计算能力.目前的手机一般在20毫秒左右.


这里有一个和其他事件传递不同的地方,DecorView会优先传递给Activity,而不是它的子View.而Activity如果不处理又会回传给DecorView,DecorView才会再将事件传给子View.


dispatchTouchEvent就是触摸事件传递的对外接口,无论是DecorView传给Activity,还是ViewGroup传递给子View,都是直接调用对方的dispatchTouchEvent方法,并传递MotionEvent参数.


从源码看触摸事件分发:


由于专栏关注自定义控件,所以关于系统如何从硬件获取触摸事件以及传递到Activity的dispatchTouchEvent就不详细分解,下面将从Activity的dispatchTouchEvent方法来一步步看事件是如何被分发传递的:

Activity中的dispatchTouchEvent:

    public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }

其中onUserInteraction();是一个空实现,是系统留给我们的一个修改事件分发的一个方法,这里可以忽略。


所以实际上Activity的dispatchTouchEvent方法是调用的PhoneWindow的superDispatchTouchEvent方法,如果superDispatchTouchEvent返回false,没有消费掉事件,那么才会再交给activity的onTouchEvent方法去处理,从这个角度来讲,如果所有地方都没有消费掉事件,最后接收事件的会是Activity的onTouchEvent方法。


那么下面我们来看看PhoneWindow中的superDispatchTouchEvent方法:

    @Override    public boolean superDispatchTouchEvent(MotionEvent event) {        return mDecor.superDispatchTouchEvent(event);    }

发现实际上调用的是DecorView对象mDecor的superDispatchTouchEvent方法,来看看DecorView的superDispatchTouchEvent方法:

        public boolean superDispatchTouchEvent(MotionEvent event) {            return super.dispatchTouchEvent(event);        }


调用的super.dispatchTouchEvent,而再来看看这个DecorView的继承关系:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker


所以调用的是FrameLayout中的dispatchTouchEvent方法,而FrameLayout并没有重写dispatchTouchEvent方法,所以实际调用的是FrameLayout的父类 ---> ViewGroup中的dispatchTouchEvent方法,下面这个图描述了从系统得到MotionEvent实际到传递给DecorView的super.dispatchTouchEvent的过程:






ViewGroup中的dispatchTouchEvent:

下面就来分析一下ViewGroup中的dispatchTouchEvent源码,这个是比较重要的部分


@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {// 调试作用,忽略if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(ev, 1);}boolean handled = false;// handled相当于最后的返回值,初始是false// onFilterTouchEventForSecurity(ev)使用安全机制来过滤事件,true的话则继续,false则过滤掉事件if (onFilterTouchEventForSecurity(ev)) {final int action = ev.getAction();final int actionMasked = action & MotionEvent.ACTION_MASK;// 如果接收到的action是DOWN操作,则重置之前的状态,重新开始一个新的触摸事件// 这样不会被之前的事件影响if (actionMasked == MotionEvent.ACTION_DOWN) {cancelAndClearTouchTargets(ev);resetTouchState();}final boolean intercepted;// intercepted是决定是否要拦截事件的标志// 下面这一段if/else实际上就是说,如果第一次DOWN操作的时候,被拦截了,那么之后的UP,MOVE等操作,都会被拦截// 注意这里,如果这里是按下的操作,那么代表是第一次触发本次的触摸事件,这时候mFirstTouchTarget应该是等于null的// mFirstTouchTarget 代表处理触摸事件的第一个目标if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {// 如果是down事件或者已经有触摸事件的目标view,才考虑是否要拦截的问题// 读取是否禁止拦截,disallowIntercept为true,表示禁止拦截;false表示允许拦截final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {// 如果允许拦截,则获取拦截的标志intercepted的值,来判断是不是要真的拦截// 从onInterceptTouchEvent()方法中获取interceptedintercepted = onInterceptTouchEvent(ev);ev.setAction(action); // restore action in case it was// changed} else {// 如果不允许拦截,则拦截标志intercepted当然是要设置成falseintercepted = false;}} else {// mFirstTouchTarget=null且不是ACTION_DOWN事件,代表不是首次按下,而且也没有一个目标对象来处理这个action,则肯定要拦截掉intercepted = true;}// 获取是否要取消事件final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;// split默认为 true ,表示是否把事件分发给多个子Viewfinal boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;// newTouchTarget代表本次将要分发事件的目标,初始设置为nullTouchTarget newTouchTarget = null;// 是否已经分发到新触摸目标标志位,初始设置为falseboolean alreadyDispatchedToNewTouchTarget = false;// 开始响应触摸if (!canceled && !intercepted) {// 如果不cancel也不被拦截,则进入到里面if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {// 如果是action_down,则进来final int actionIndex = ev.getActionIndex(); // always 0 for// down// 如果split==true,则把pointerId与事件代码actionIndex关联起来final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex): TouchTarget.ALL_POINTER_IDS;// Clean up earlier touch targets for this pointer id in// case they// have become out of sync.removePointersFromTouchTargets(idBitsToAssign);final int childrenCount = mChildrenCount;if (newTouchTarget == null && childrenCount != 0) {// 拿到对应action的x,y的坐标,以便后面判断x,y是否落在子view范围内final float x = ev.getX(actionIndex);final float y = ev.getY(actionIndex);// 拿到所有的子view集合final View[] children = mChildren;final boolean customOrder = isChildrenDrawingOrderEnabled();// i 从 childrenCount - 1开始,遍历到 0;// 倒序遍历所有的子view,这是有原因的,这里的children中的顺序,实际上是按照addView或者XML布局文件中的顺序来的,// 后addView添加的子View,会因为Android的UI后刷新机制,显示在上层;假如点击的地方,有两个子View都包含的点击的坐标,那么后被添加// 到布局中的那个子view,会先响应事件;这样其实也是符合人的思维方式的,因为后被添加的子view会浮在上层,所以我们去点击的时候,一般// 都会希望点击最上层的那个组件,先去响应事件for (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;final View child = children[childIndex];// 判断一下子view是否能够接收到这个事件,这个事件的x,y坐标,是否落在子view上// 如果不能,则continue,继续遍历下一个// canViewReceivePointerEvents()方法实际上会去判断这个子view是否可见或者在播放动画if (!canViewReceivePointerEvents(child)|| !isTransformedTouchPointInView(x, y,child, null)) {// 如果这个子view,接收不到事件,那么就continue,查询下一个子viewcontinue;}// 通过getTouchTarget去查找View是否在mFirstTouchTarget.next这条target链中的某一个targe中了// 如果在则返回这个target,否则返回nullnewTouchTarget = getTouchTarget(child);if (newTouchTarget != null) {// 如果返回的newTouchTarget不为null,则表示当前子view已经接收当前事件了,则不需要再继续遍历寻找,break掉。// Child is already receiving touch within its// bounds.// Give it the new pointer in addition to the// ones it is handling.newTouchTarget.pointerIdBits |= idBitsToAssign;// 找到了接收了事件的子view了,所以这里break掉循环break;}resetCancelNextUpFlag(child);// 如果上面没有break,只有newTouchTarget为null,说明上面我们找到的子view和之前的肯定不是同一个了,是新增的,// 比如多点触摸的时候,两个手指分别按在不同的子view上的情况// 这时候我们就看子view上是否分发该事件。// 在这里dispatchTransformedTouchEvent实际上就是做了个判断:如果child==null,// 则调用super.dispatchTouchEvent,也就是view中的方法,如果chile!=null,则调用child.dispatchTouchEventif (dispatchTransformedTouchEvent(ev, false, child,idBitsToAssign)) {// 如果这个dispatchTransformedTouchEvent方法返回true,意味着在child这一条事件线上,事件被接收且消费掉了,// 那么就更新状态信息,把当前newTouchTarget设置成当前的子view,然后break掉循环mLastTouchDownTime = ev.getDownTime();mLastTouchDownIndex = childIndex;mLastTouchDownX = ev.getX();mLastTouchDownY = ev.getY();newTouchTarget = addTouchTarget(child,idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;break;}}//遍历children的for循环的结束括号}//if (newTouchTarget == null && childrenCount != 0) 的结束括号// newTouchTarget == null表示没有找到一个能够接收事件的子view,//如果这时mFirstTouchTarget不为空,那么我们可以顺着mFirstTouchTarget.next的链,去找最后一个不为空的targetif (newTouchTarget == null && mFirstTouchTarget != null) {// Did not find a child to receive the event.// Assign the pointer to the least recently added// target.newTouchTarget = mFirstTouchTarget;while (newTouchTarget.next != null) {newTouchTarget = newTouchTarget.next;}newTouchTarget.pointerIdBits |= idBitsToAssign;}}//ACTION_DOWN进入的结束括号}//if (!canceled && !intercepted) {的结束括号if (mFirstTouchTarget == null) {    // 这种情况一般发生在在Down事件的时候就被onIntercept方法拦截掉,所以mFirstTouchTarget还是null,
 // 或者发生在Down事件时没有被拦截,但是却没有任何一个子view/viewgroup的dispatchTouchEvent方法返回true,于是mFirstTouchTarget也是null// 该viewGroup里,没有touch目标,则当成一个普通view处理// 这里第三个参数,本来是响应事件的view,这里传一个null进去,// 则会调用super.dispatchTouchEvent,也就是当成一个view来处理// 实际上就是调用这个view的onTouchListener中的onTouch方法(如果设置了监听)// 如果没有设置监听,或者监听的onTouch方法返回false,则会调用view的onTouchEvent方法// 由于ViewGroup没有重写onTouchEvent方法,所以这个View的onTouchEvent方法也可以说是ViewGroup的onTouchEvent方法// 而且这里也要依赖canceled的值来决定是否cancel事件handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {    // 这个else里的情况,有可能是在Down事件时没有被拦截,而在之后跟随的其他Action时被拦截    // 所以mFirstTouchTarget不为null的情况    // 也有可能是没有被拦截,但是找不到一个子view/viewGroup来接收事件的情况// Dispatch to touch targets, excluding the new touch target if// we already// dispatched to it. Cancel touch targets if necessary.TouchTarget predecessor = null;// 从mFirstTouchTarget开始遍历TouchTarget target = mFirstTouchTarget;// 遍历所有target进行dispatch分发// 这里的遍历跟之前children的遍历不一样,那里第二个参数直接是false,而这里需要考虑是否cancel// dispatchTransformedTouchEvent(ev, false, child,// idBitsToAssign)while (target != null) {final TouchTarget next = target.next;if (alreadyDispatchedToNewTouchTarget&& target == newTouchTarget) {// 找到了新的子 View,并且这个是新加的对象,上面已经处理过了。handled = true;} else {// 如果不是接收目标// 则判断是否要cancel该target的事件final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;// 如果这个cancelChild=true,则在dispatchTransformedTouchEvent会有// event.setAction(MotionEvent.ACTION_CANCEL);这一句,然后再调用dispatchTouchEvent的时候// 就会走cancel的流程了if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {// 如果这里条件成立,则表示target.child接收了这个事件,则handled = truehandled = true;}if (cancelChild) {if (predecessor == null) {mFirstTouchTarget = next;} else {predecessor.next = next;}target.recycle();target = next;continue;}}// 记录当前target,然后继续下一个targetpredecessor = target;target = next;}}// Update list of touch targets for pointer up or cancel, if needed.// 返回之前的善后工作if (canceled || actionMasked == MotionEvent.ACTION_UP|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {resetTouchState();} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {// 手指抬起的时候,清除掉相关数据final int actionIndex = ev.getActionIndex();final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);removePointersFromTouchTargets(idBitsToRemove);}}if (!handled && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);}return handled;}



View中的dispatchTouchEvent:

我们再来看看View的dispatchTouchEvent方法:


 public boolean dispatchTouchEvent(MotionEvent event) {        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onTouchEvent(event, 0);        }        if (onFilterTouchEventForSecurity(event)) {            //noinspection SimplifiableIfStatement            ListenerInfo li = mListenerInfo;            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                return true;            }            if (onTouchEvent(event)) {                return true;            }        }        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);        }        return false;    }


View的dispatchTouchEvent方法比较简单,就是view如果设置了onTouchListener的话,就执行onTouchListener.onTouch方法,如果这个方法返回false或没有设置Listener的话,那么就执行view的onTouchEvent方法。如果上面两个方法都返回false,则返回false,否则返回true。


结合上面ViewGroup和view的事件分发代码,给出一个事件分发的主干流程,略去了中间一些细节和判断(newTouchTarget,cancel等):






那么到这里,可以给出前面留下的问题的答案了,因为事件分发总是一级一级的往下分发,每一级都会遍历自己所有的子view/viewGroup,然后这其中能够响应事件的子ViewGroup再调用自己的dispatchTouchEvent方法,继续遍历自己所有的子view/viewGroup,所以在最开始的那种情况中,MyFrameLayout的dispatchTouchEvent会包含MyRelativeLayout的dispatchTouchEvent方法调用,而MyRelativeLayout的dispatchTouchEvent方法调用会包含MyTextView的dispatchTouchEvent方法的调用。


而在改为将MyTextView作为MyFrameLayout的子view之后,在调用MyFrameLayout的dispatchTouchEvent时,会遍历它的所有的子view/viewGroup,这就包含了MyTextView和MyRelativeLayout,而这个遍历是倒序遍历,也就是说后addView进来的子view会先被遍历到,先响应触摸事件,而代码里MyTexitView是后被添加进来的,所以会在MyRelativeLayout的dispatchTouchEvent方法调用之前先执行完MyTexitView的dispatchTouchEvent方法。


一些总结:


Down事件:

通过onInterceptTouchEvent方法判断是否要拦截事件,默认fasle
根据scroll换算后的坐标找出所接受的子View。有动画的子View将 不接受触摸事件。
找到能接受的子View后把event中的坐标转换成子View的坐标
调用子View的dispatchTouchEvent把事件传递给子View。
如果子View消费了该事件,则把target记录为子View,方便后面的Move和Up事件的传递。
如果子View没有消费,则继续寻找下一个子View。
如果没找到,或者找到的子View都不消费,就会调用View的dispatchTouchEvent的逻辑,也就是判断是否有触摸监听,有的话交给监听的onTouch处理,没有的话交给自己的onTouchEvent处理


Move和Up事件:

判断事件是否被取消或者事件是否要拦截住,是的话,给Down事件找到的target发送一个取消事件。
如果不取消,也不拦截,并且Down已经找到了target,则直接交给target处理,不再遍历子View寻找合适的View了。
这种处理事件是正确的,我们用手机经常可以体会到,当我手指按在一个拖动条上之后,在拖动的时候手指就算移出了拖动条,依然会把事件分发给拖动条控制它的拖动。


View的onTouchEvent:

从View的dispatchTouchEvent可以看出,事件最终的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序员来编写逻辑,那么我们来看看onTouchEvent事件。View只能响应click和longclick,不具备滑动等特性。


onIntercept方法返回true,事件被拦截之后,去了哪里?

对于Down事件的时候,被Intercept方法拦截之后,这时候mFirstTouchTarget肯定是=null的,所以这时候会调用handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);方法,这里由于传入的view对象=null,所以会导致直接调用super.dispatchTouchEvent方法,所以Touch事件被拦截之后,会转到View的事件分发中去了,而在View.dispatchTouchEvent中,如果当前view/viewGroup设置了onTouchListener,则会调用TouchListener.onTouch方法,如果没设置Listener或者TouchListener.onTouch返回false,则会调用View.onTouchEvent方法,如果View.onTouchEvent也返回false,那么事件会依次往上传递,这与一开始描述的一样。

Down事件如果没有被拦截,那么随后的Up事件会不会被拦截?

Down事件没有被拦截,那么在up的时候却有可能会被拦截,这会发生在所有的子view/viewgroup的dispatchTouchEvent都没有返回true时,这时当up事件发生时,进入dispatchTouchEvent,会发现mFirstTouchEvent还是等于null,看下面这段源码:

 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); // 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;            }

如果是up,则最外面的if进不去,只能进它的else,而在else里面只有一句,那就是intercepted=true,拦截掉这次的up事件,那么这个up不会进入到子view/viewGroup的dispatchTouchEvent方法,而会进入到本ViewGroup的TouchListener.onTouch方法或者onTouchEvent方法中。而这种情况下,intercepted = onInterceptTouchEvent(ev);这一句也根本没有执行,所以这时候onInterceptTouchEvent是没用的。


以上内容都是自己琢磨源码和查阅资料得来,难免会有纰漏和错误,欢迎指正,谢谢!


请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45071069,非允许请勿用于商业或盈利用途,违者必究。


更多相关文章

  1. 深入Android的消息机制源码详解~Handler,MessageQueue与Looper关
  2. Android(安卓)事件拦截机制一种粗鄙的解释
  3. Android(安卓)自定义音乐播放器实现
  4. 巨佬Jake Wharton谈Android对Java 8的支持
  5. Android手机安全软件的恶意程序检测靠谱吗--LBE安全大师、腾讯手
  6. Android(安卓)HAL实现的三种方式(2) - 基于Service的HAL设计
  7. Android(安卓)Fragment完全解析,关于碎片你所需知道的一切
  8. Android(安卓)Activity生命周期详解
  9. 第24章、OnLongClickListener长按事件(从零开始学Android)

随机推荐

  1. Android studio,第一个生成,调用成功的jni
  2. aidl 在android tv中的应用
  3. js 区分浏览器来源是PC端还是移动端
  4. Android中WebView实现Javascript调用Java
  5. Android点击Home键后fragment重新加载问
  6. android 检测字符串是否为合法域名
  7. AndroidClipSquare安卓实现方形头像裁剪
  8. android 关于读取SD卡或者U盘的一些方法
  9. 【Android笔记】Activity涉及界面全屏的
  10. Android显示GIF动画完整示例(二)