Android 开发艺术探究V第三章之view的事件分发机制

                   在介绍点击事件的传递机制,首先我们要分析的对象就是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)
发布了45 篇原创文章 · 获赞 6 · 访问量 4万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章