请尊重原创劳动成果,转载请注明出处: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. Android8.0 存储系统
  2. 条件数据库Android:sqllite的简单使用
  3. Android(安卓)AccessibilityService机制源码解析
  4. Android学习感悟之消息机制
  5. 信息提醒之Notification,兼容全部SDK-更新中
  6. Android自定义Lint实践
  7. Android(安卓)RxJava实际应用教学:你该什么时候使用RxJava?
  8. android之复选框点击事件(掌握CheckBox复选控件)
  9. Android(安卓)RxJava操作符详解系列: 变换操作符

随机推荐

  1. Android Animation初步
  2. UML详解:解析Android消息处理机制:Handler/
  3. android生命周期神器--Lifecycle
  4. 微信转发度最高的十大Android文章
  5. 第一章 Adnroid体系与系统架构
  6. android 使用JavaMail发送邮件
  7. 【Error】MPermissions引入错误 android-
  8. 【Android】第21章 2D图形和动画
  9. ToolBar 去掉默认左间距
  10. Android 下拉框第三方控件 NiceSpinner