在介绍点击事件的传递机制,首先我们要分析的对象就是MOtionEvent,即点击事件,(当点击屏幕时由硬件传递过来,关于MotionEvent在View的基础知识中做了介绍),所谓的点击事件的分发就是MotionEvent的分发过程。即当一个MoTionEvent产生以后,系统需要把这个事件具体传递给一个具体的View,而这个传递过程就是分发过程,点击事件传递过程有三个很重要的方法,下面先来介绍这几个方法。

                 public boolean dispatchTouchEvent(MOtionEvent ev)

用来进行事件分发,如果事件能够传递给当前的view,那么此方法一定会被调用,返回结果受当前View的OnTouchEvent和下级View的dispatchEvent方法的影响,表示十分消耗当前事件,

              public boolean onInterceptTouchEvent(MOtionEvent ev)

上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会再调用,返回结果表示是否拦截当前事件,(拦截Action_Down事件,或者在Acton_Down事件当中子VIew  dispatchTouchEvent(MotionEvent ev)返回为false)下面关于这个方法什么情况下会调用将会做详细的介绍。

        public boolean onTouchEvent(MOtionEvent ev)

在dispathcTouchEvent(MotionEvent ev)方法的调用,用来处理点击事件,返回结果是否消耗当前事件,如果不消耗,在同一个事件序列中,当前View无法再次接收到事件。

                  

              上面三个方法区别:

                     public boolena dispathcTouchEvent(MotionEvent ev){

boolean cosume=false;

if(onInterceptTouchEvent(ev)){

consume=onTouchEvent(ev);

}else{

consume=child.dispatchTouchEvent(ev);

}

}

                   


   上面代码已经将三者的关系表现的淋漓尽致,通过上面的代码实现,我们也可以大致了解事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它 如果ViewGroup的  onInterceptTouchEvent 方法返回true,就表示它要拦截当前事件,接着事件就会交给ViewGroup的onTouchEvent(ev)方法处理即它的onTouchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent(MotionEvent ev);方法被调用,如此反复知道事件被最终结束。

  

    当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的OnTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用,这个dispatchTouchEvent将会返回true,由此可见,给View设置的有OnTouchListener,其优先级比OnTouchEvent要求高,在onTouchEvent方法中,如果设置的有OnClick,那么它的onClick方法会被调用。后面分析:如果onClick被调用,dispatchonTouchEvent(ev)将会返回true,表示消耗了当前的事件


    当一个点击事件产生后,它的传递过程如下:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window在传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件,一种特殊情况,当View不消耗除ACIONT_DOWN以外的其他事件,那么这个点击事件会消失,那么最终的传递给Activity处理,即Activity的onTouchEven方法会被调用。


有以下几点:

1.   同一个事件不能由两个View同时处理,但是通过特殊手段可以做到,

2.   某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false也就是disptchOnTouchEvent返回false,那么同一事件都不会再交给它处理,即父容器的onTouchEvent回被调用,

        3.在第二点的基础上,比较重要:如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用。并且当前View可以持续收到后续的事件,最后这些消失的点击事件会传递给Activity, (是在父View的OnInteceptTouchEent不被从写的情况下,因为父View默认不拦截事件,当前还有一种情况就是设置标志位,控制自View是否需要点击事件。)

       4.View的enable属性影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true那么它onTouchEvent返回true,TextView和Button的onTouchEvent的chickable不相同,onTouchEvent方法返回值也就不同,如果setOnclickable方法或者setLongClickable 方法将会让clickable和longclickable自动变为true

      5.事件传递过程是由外向内的,即 事件总是先传递给父元素,然后在由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在干预子元素的事件分发过程。但是除了Action_Down以外。

     


   事件分发机制的源码解析


      1.Activity对点击事件的分发过程

            

   点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,有Activity的dispatchTouchEvent来进行事件派发,具体工作由Activity内部的Window来完成,Window会将事件传递给decorview,decorview一般就是当前界面的底层容器,(即setContextView所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。我们先从Activity的dispatchTouchEvent开始分析。


public boolean dispatchTouchEvent(MotionEvent ev){

if(ev.getAction==ACTON_DOWN){

onUserInteraction();

}

if(getWindow().superDispatchTouchEvent(ev)){

return true;

}

return onTouchEvent(ev);

}


分析上面的源码,首先事件开始交给Activity所附属的Window进行分发,如果返回true,这个事件循环结束,返回false意味着事件没人处理,所有View的onTouchEvent都返回false,那么Activity的OnTouchEvent就会被调用。

       

        接下来看Windown是如何将事件传递给ViewGroup的,通过源码我们知道,Window是一个抽闲类,而Window的superDisPatchTouchEvent方法也是个抽象方法,因此我们找到Window的实现才行。


        public absract boolean superDispatchTouchEvent(MotionEvent event);


那么Window的实现类是什么?其实是PhoneWindow,这一点从Window的源码中也可以看出来,在Window的说明中,Window类可以控制顶级View的行为和策略,它的唯一实现位于Android.policy.Phonewindow中,当你要实例化这个Window类的时候,你并不会知道它的实现细节,因为这个类被重构,只有一个工厂方法可以使用。尽管这看起来有点模糊,不过我们可以看依稀Android.policy.PhoneWindow这个类,尽管实例化的时候此类会被重构,仅是重构而已,功能是类似的。


        由于Window的唯一实现是PhoneWindow,因此接下来看一下PhoneWindow是如何处理点击事件的,


           public boolean superDispathcTouchEvent(MotionEvent event){

return decor.superDispatchTouchEvent(event);

}


       到这里逻辑就很清晰了,PhoneWindow将事件传递给了DecorView,我们知道通过(ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)这个方式就可以获取Activity所设置的View,这个子View很显然是我们setContextView所设置的View。另外顶级View也叫根View,顶级View一般来说都是ViewGroup。由此为止:上面分析Activity都顶级View的分发机制就介绍到这里了:


     下面将 会分析ViewGroup的分发过程,上面我们对View进行理论上的分析,但还是没有对源码进行分析。首先先看ViewGroup的DispatchTouChEvent方法中,这个方法比较长,这里分段说明。很显然,它描述的是当前View是否拦截这个事件的逻辑。

   下面这段代码很重要

    //check for interception

    final boolean intercepter;

    if(actionMasked==MotionEvent.Action.Down || mFirstTouchTarger!=null){

final boolean disallowIntercept=mGrouFlags& FLAG_DISALLOW_INTERCEPT!=0){//这个标志位是在自View调用 requestDisallowInterceptTouchEvent方法来设置的。

if(!disallowIntercept){

intercepted=onInterceptTouchEvent(ev);

ev.setAction(action);//restore Action in case it was Changed

}else{

intercepter=false;

}

}else{

intercepter=true;

}

}


(上面这段代码的说明)

从上面的代码我们可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为Action_DOWN或者mFirstTouchETager!=null .Action_DOWN事件好理解,那么mFirstTouchTaget!=null是什么意思?这个从后面的代码逻辑可以看出来,当事件由ViewGroup的子元素成功处理是,mFirstTouchTarget会被赋值并指向子元素,换种方式来说,当ViewGroup不拦截事件,并且交个子元素处理时,mFirstTouchTarget!=null。反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget!=null就不成立,那么当Action_move和Action_up事件到来时,由于(actionMasked==MOtionEvent.ACION_DOWN||mFirstTouchTarget!=null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一个序列的其他事件都会默认交给它处理。


       FLAG_DISALLOW_INTERCEPT标志位一旦设置后,ViewGroup将无法拦截出了Action_down以外的其他事件,如果是Action_down事件,救回重置标记位,将导致子View中设置的这个标记位无效。当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件,


我们可以得到结论:当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEent方法,那么制度单分析对总结起来有两点:

              1.onInterceptTouchEvent方法不是每次都会被调用,如果我们想要处理所有的点击事件必须到 dispatchTouchEvent方法,只有这个方法保证每次都会调用,当然前提是事件能够传递到dispathcTouchEvent,

     2.FALG_DISALLOW_INTERCEPT标记位的作用给我们提供了思路,当面对滑动冲突时,我们可以考虑这种方法。


接着在看当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行chul

final View[]  childern=mChildren;

for(int i=childlern-1;i>=0;i--){

final int childIndex=customOrder?getChildDrawingOrder(childdrenCount,i);i;?getChildDrawingOrder(childrenCount,i):i;

......

if(dispathcTrasfromedTouchEvent(ev,false,cihld,idBitsToAssing)){

//child wants to receive touch within ists bounds

mlastTouchDownTime=ev.getDownTime();

if(preorderedList!=null){

....

new TouchTarget=addTouchTarger(child,idBitsToAssign);

}

}

}

重上面这段代码分析首先遍历所有ViewGroup的所有子元素,然后判断子元素是否能够接受到点击事件,是否能够接受点击事件主要由两点来衡量:

子元素是否有动画,和坐标是否落在子元素上。


dispathcTrasfromedTouchEvent实际调用了子元素的dispatchTouchEvent方法,在它的内部有如下一段内容,而在上面的代码中,child传递的不是null它会直接调用子元素的dispatchTouchEvent方法,这样事件就交给子元素处理,从而完成一轮事件分发。

                      if(child==null){

handler=super.dispatchTouchEvent(event);//抛出给父View 的父容器

}else{

  handler=child.dispatchTouchEvent(event);

}


  从上面看出,如果子元素为null,或者dispathcTrasfromedTouchEvent返回为false,就会调用父View的父View的dispatchTouchEvent(evnet)

如果子元素的dispatchTouchEvent返回true,就会交给子元素分发,那么mFirstTouchTarget就会被赋值,如果子元素的FirstTouchTarget是在addTouchTarger中进行赋值的  ,如果遍历所有的子元素都没有被合适的处理,将会交给父View自己处理

                if(mFirstTouchTarget==null){

handled=dispatchTransformTouchEvent(ev,canceled,nullTouchTarget,ALL_POINTER_IDS);

}



从上面ViewGroup 理论和源码分析总,我总结了上述分析:

1.ViewGroup 最先调用的是dispatchTouchEvent(event); 然后判断是不是Action_down 或者mFirstTouchTarget是不是为null,如果两者都不成立,事件将不会调用onInterceptTouchEvent(ev)进行判断是否拦截事件,事件将会直接交给ViewGroup本身自己处理。如果两者有一个不成立,将会判断FLAG_DISALLOW_INTERCEPT标志位,如果标志位true,将会调用onInterceptTouchEvent方法判断是否将事件交给自View处理,如果 onInterceptTouchEvent返回为true,将会交给父View自己处理,如果返回false,事件有2种方式传递:

1.事件将会传递给子View,如果找到了合适的子View,如果子View返回true,也就是事件被消耗掉,那么也就完成了整个事件的传递,

2.如果找不到合适子View 就会交个父View的的父容器处理。由于父View的父容器也找不到合适的子View进行分发事件,由此往上,所以会继续向上抛出,一直抛出给Window,然后Window找不到合适的子View继续抛出,抛出给Activity。所以会调用Activity的onTouchEvent方法;

             也就是说,如果ViewGroup一旦不处理事件,将会没有机会再得到事件。事件不是给子View处理,就是向上抛出 ,当然,本身也就是上角是有最上面的ViewGroup不出的。同理,事件会直接交给Activity处理。



     通过上面的总结分析:我们大致知道了ViewGroup的事件分析机制。所有我们经常通过逻辑来改变标志位,和通过重写onInterceptTouchEvent方法,来进行事件传递。


  上面分析了ViewGroup的事件分析下面分析View的事件传递机制

        View的事件传递机制,相对来说比较简单,注意这个View不包括ViewGroup,ViewGroup重写了view的dispatchOnInterceptTouchEvent的方法。

                   当View调用的dispatchOnInterceptTouchEvent方法,会判断有没有注册OnTOuch,如果注册了,将会调用,OnTouch(this,enent);如果 OnTouch(this,event)返回true,也就是事件被消耗了,OnTouchEvent将不会再被调用,如果返回false,或者不注册onTouch(this,event)将会都会调用onTouchEvent方法。也就是说,onTouch不决定当前事件是否被当前View消耗,因为当前View如果OnTouch返回true,那么dispatchOnInterceptTouchEvent方法也会返回true。事件将被消耗,如果onTouch返回false,将会调用TouchEvent,如果TouchEvent返回false,dispatchOnInterceptTouchEvent也会返回false,TouchEvent返回true,那么dispatchOnInterceptTouchEvent返回为true,另外,在TouchEvent在会判断有没有注册OnClick方法和onLongClick,如果注册了将会调用,在注册Onclick,或者onLongClick的时候,CLICKABLE和CLICKLONGABEL会自动变成true,所有会被调用。



这里提两个问题:(包含了事件分发机制的精髓)

1.如果在Viewgroup,子View在ACTION_DOWN 中返回了false还会传递给子View吗?(如果不会,通过设置标志位或者起作用吗)

2.如果子View值消耗了ACTION_DOWN事件,其余事件都不消耗,那么事件是分发过程是怎样的?(可以通过设置标志位来决定事件是否由谁来执行)

 

答案:

                    1.不会,当子View在ACTON_DOWN总返回false,也就是dispathcTrasfromedTouchEvent在ACTION中返回false,将不会给mFIrstTouchTarget进行赋值,然后在ViewGroup中将不会调用onInterceptTouchEvent进行判断,直接将事件交给ViewGROP自己处理。

设置标志位也不会起作用,因为,标志位的判断,是先进行ACTION_DOWN判断和mFirstTouchTarget是否为null之后。

   

              2.通过上面的事件分发机制我们可以知道,ACION_DOWN传递给子View,并且子View消耗了ACTION_DOWN事件,dispatchOnInterceptTouchEvent返回true,mFirstTouchTarget将会被赋值,由于Viewgroup进行的onInterceptTouchEvent方法默认是不拦截任何事件,所有事件将会给子View进行执行。但是子View并不消耗Action_move,和action_up,所有ViewGroup会调用super.dispatchOnInterceptTouchEvent方法,事件将会一层一层向上面抛出。一直会调用Activity的事件OnTouchEvent方法

                   可以通过设置标志位和重写onInterceptTouchEvent方法来决定是否交给当前的ViewGroup;




View的滑动冲突


通过上面的分析,我们将进入一个深入的话题,滑动冲突,相信开发Android 的人深有体会,本来在网上下载的demo运行的好好的,但是由于组合在一起将会出现各种问题,滑动冲突的产生:其实在界面上只要内外两层同时可以滑动,这个时候就会产生滑动冲突,如何解决滑动冲突,这既是一件困难的事又是一件简单的事,说简单是因为解决滑动冲突有固定的套路,本节是View体系的核心章节,


常见的滑动冲突场景:

1.外部滑动方向和内部滑动方向不一致;

2.外部滑动方向和内部滑动方向一致;

3.两种情况的嵌套;

 先说场景1:主要是ViewPager和Fragment配合使用所组成的滑动效果,主流应用几乎都会使用这个效果,而每个页面会嵌套一个listView,本来这种情况是有滑动冲突的,但是ViewPager处理了这种滑动冲突,如果我们不是采用的ViewPager而是采用的ScrollView等,那就必须手动处理滑动冲突,否则造成的后果就是就是内外两层只有一层能够滑动。

场景2:当内外两层都在同一个方向上滑动的时候,显然存在逻辑为,因为当手指开始滑动时,系统无法知道用户到底想让哪一层滑动,所以当手指滑动的时候就会出现问题。要么只有一层呢能够滑动,要么就时内外两层都能够的很卡顿。

       场景3: 是场景1和场景2的嵌套,因此场景3的滑动冲突看起来更加的复杂,比如在许多应用中会有这么一个效果,内层有一个场景1中滑动效果,然而外出又有一个场景2中的滑动效果,具体说,就是外部有slideMenu效果,然后内部有一个ViewPager的每一个页面有一个ListView。虽然说场景3看起来很复杂,但它是几个单一的滑动冲突的叠加,因此只需要分别处理内层.外层和中层之间的滑动冲突即可。



   滑动冲突的解决规则

场景1.让内部View拦截点击事件,这个时候我们可以根据他们的特征来解决滑动冲突,具体来说:根据滑动时水平滑动还是竖直滑动来判断是否是谁拦截事件。(判断方式:1.可以根据滑动路径和水平方向形成的夹角,也可以依据水平方向竖直方向上的距离差来判断,某些特殊情况还可以根据水平方向和竖直方向的速度来判断 比如竖直方向滑动距离大于水平方向滑动的距离)

                 场景2:比较特殊,它无法根据滑动的角度,距离差以及速度来判断,这个时候一般都能在业务上找到突破点。

场景3:同样也只能在业务上找到突破点;



   滑动冲突的解决方式

1.外部拦截法

所谓的外部拦截发:是指所有的点击事件都先经过父容器拦截处理,如果父容器需要此事件,就拦截,如果不需要就不拦截。就这样解决滑动冲突问题

外部拦截法需要重新onInterceptTouchEvent方法

public boolean onInterceptTouchEvent(MOtionEvent evet){

boolean intercepter=false;

int  x=(int)evet.getx();

int y=(int)   evet.getY();

switch(evetn.getAction){

case MotionEvetn.ACTION_DOWN:

                 
//必须返回false,不消耗事件,交给子View去消耗,否则mfirsTouchTarger将不会被赋值,后续事件将不会传递给View

intercepter=false;

break;

case  MotionEvent.getMOVE;

if(父容器需要当前事件){

intercepter=true;//交给父view自己拦截

}else{

intercepter=false;

}

break;

  case Moton.ACTION_UP;

intercepter=false;//必须返回false;如果是true,子View将无法接收到ACTION_UP

break;

}

mlastXintercept=x;

mlaseYintercept=y;

return intercepted;

}


            上述代码是外部拦截发的经典逻辑



2.内部拦截法:

 内部拦截法是指View不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉。否则剧交给父容器进行处理。这个方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptEvent方法才能正常工作,我们需要重写子元素的dispatchTouchEvent方法。


public boolean dispatchTouchEvent(MotionEvent event){

int x=(int)event.getX();

int y=(int)event.getY();

switch(event.getAction){

case MotionEvent.ACTON_DOWN:

parent.requestDIsallowInterceptTouchEvent(true);//设置标志位,子view处理事件。

break;

case MoTionEvent.ACTON_MOVE;

int detaX=x-mlastX;

int deltaY=y-mLastY;

if(父容器需要此类点击事件){

parent.requestDisallowInterceptTouchEventet(false);

}

break;

case MotionEvent.ACTION_UP;

break;

}


                       mLastX=x;

mLastY=y;

}



除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用,parent.requestDisallowInterceptTouchEvent(false);方法时,父元素才能继续拦截所需事件。

public  boolean onInterceptTouchEvent(MotionEvent event){

int action=event.getAction();

if(action==MotionEvent.ACTION_DOWN){

                 return false;

}

        return true;

}


上述代码是内部拦截发的经典代码,当面对不同的滑动策略时,只需要修改里面的的各种条件即可,这种方法和Android中的事件分发机制不一样,其实内部拦截法,就是不让父元素有判断onInterceptTouchEvent方法的机会,只要判断,就会给父View自己处理。  (当然,前提是父View在onInterceptTouchEvent没有拦截ACTION_DOWN,让mFIrstTouchTarget不等于null,让父View先判断标志位,如果标志位true,就不判断onInterceptTouchEvent方法,直接将事件给子View,如果标志为false,就会去判断onInterceptTouchEvent方法,而onInterceptTouchEvent方法除了Action_Down总是返回true,就会给父容器处理拦截。



  最后

    注意:在写事件是否拦截事件时;

如果是外部拦截法 如果滑动事件父View惯性滑动,在父View 的OnInterceptTouchEvent 中的ACTION_DOWN中加上如下代码,让父View继续处理事件,不让让其传递给子View


switch(event.getAction()){

case MotionEvent.ACITION_DOWN:                 

intercepted=false;

if(!mScroller.isFinished()){

mScroller.abortAnimation();

intercepted=true;

}

........

}

  如果是内部拦截法:也想上一样在父View的onInterceptTouchEvent中加入上述代码:



                  public boolean dispatchTouchEvent(MOtionEvent ev)

更多相关文章

  1. 菜鸟进阶之Android(安卓)Touch事件传递(一)
  2. Android(安卓)源码分析实战 - 授权时拦截 QQ 用户名和密码
  3. android手机震动的节奏例子--Vibrator对象及周期运用
  4. [置顶] android实现向右滑动返回功能
  5. Android(安卓)代码实现来电拦截
  6. 第三部分:Android(安卓)应用程序接口指南---第二节:UI---第十章 拖
  7. Android(安卓)View学习笔记(二):View滑动方式总结
  8. Android监听ListView停止的时候是不是滑动到底部
  9. Android从普通发送和接收短信到对短信进行拦截

随机推荐

  1. Android(安卓)如何实现ios中的UIPageCont
  2. Android Bluetooth使用详解
  3. android 学习视频汇总
  4. Others
  5. Android Button,TextView的显示大小写问
  6. Android 中的两端对齐实例详解
  7. 安卓自定义流式布局
  8. Android中使用Intent打开本地图库
  9. Ubuntu Lucid(10.04)上安装Google Androi
  10. Android 分析ANR和死锁