Android事件分发之子View驳回ViewGroup拦截原理分析

虽然网上关于这一块的博文很多,但是找了很久都没有找到比较全面的分析,所以想自己也开始写一些博客,一来让自己加深印象,二来希望能够给大家多多少少带来一些帮助。好了废话不多说,直接进入主题。

Android事件分发图

如果用一张图来描述事件的流程走向的话,那么下面这张图可以说是比较全面的且通俗易懂。首先先解释下三个方法的意思,dispatchTouchEvent()–是否分发事件,onInterceptTouchEvent()–是否拦截事件,仅ViewGroup,onTouchEvent()–当事件触摸是否进行操作。

看完上图是否已经有一定的理解?在这里简单总结一下:

  • Activity中有dispatchTouchEvent()及onTouchEvent()方法
  • ViewGroup中有dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()方法。
  • View中有dispatchTouchEvent()、onTouchEvent()方法。
  • 如果不重写任何方法的情况下系统默认调用的父类的方法,事件的走向为Activity - ViewGroup - View - ViewGroup - Activity,最后由Activity自己消费掉。
  • 如果任何类中重写dispatchTouchEvent()方法,return false,那么表示不继续向下分发,而是直接流向上一级的onTouchEvent()方法中,如果return true,那么表示自己消费,本次事件在此终结,不再向任何地方分发。
  • ViewGroup中有一个onInterceptTouchEvent()方法,表示是否需要拦截子类的触摸事件,如果该方法中return true,那么表示该事件由ViewGroup自己消费,事件不会再向下传递,而是直接有ViewGroup的onTouchEvent()自己处理,如果return false,则表示不拦截,事件继续分发至子View中。
  • 同样,首先流到子View时,需要先走子View的dispatchTouchEvent()方法,如果不重写则该事件交由子View的onTouchEvent()处理消费。
  • 如果子View的onTouchEvent()重写返回true,那么表示该该事件由子View处理,事件就此终结,反之,如果不重写该方法或者返回false,那么事件继续流向上一级的onTouchEvent()处理,直到Activity终结。

以上就是事件分发的大致流向路线,看起来虽然复杂但其实结合图形分析的话,还是很容易理解的。

结合源码分析

当然了解到这里肯定还不够,以上只是事件的大致传递流程,基于不考虑触摸动作的情况下,那么我们结合源码继续分析,当触摸的动作不同的情况下,事件的走向又是什么样的呢?

在以上的所有类回调的所有方法中的参数会提供一个Event对象,该对象包含三种事件的动作,根据鼠标的动作,Event.getAction对应值如下:
- Event.ACTION_DOWN(鼠标点下去的那一瞬间调用)
- Event.ACTION_MOVE(当鼠标移动时)
- Event.ACTION_UP(当鼠标抬起时)

根据getAction判断鼠标的动作,当鼠标动作不一样时,事件走向又是怎么走的呢?先看源码我们再进行分析

dispatchTouchEvent()中ACTION_DOWN:

@Override  
   public boolean dispatchTouchEvent(MotionEvent ev) {  
       if (!onFilterTouchEventForSecurity(ev)) {  
           return false;  
       }  

       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;  

       boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  

       if (action == MotionEvent.ACTION_DOWN) {  
           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  
           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;  

               for (int i = count - 1; i >= 0; i--) {  
                   final View child = children[i];  
                   if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                           || child.getAnimation() != null) {  
                       child.getHitRect(frame);  
                       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.  
                               mMotionTarget = child;  
                               return true;  
                           }  
                           // The event didn't get handled, try the next view.  
                           // Don't reset the event's location, it's not  
                           // necessary here.  
                       }  
                   }  
               }  
           }  
       }                                         

dispatchTouchEvent()中ACTION_MOVE:

@Override  
   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;  

       boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  

      //...ACTION_DOWN  

      //...ACTIN_UP or ACTION_CANCEL  

       // The event wasn't an ACTION_DOWN, dispatch it to our target if  
       // we have one.  
final View target = mMotionTarget;  


       // if have a target, see if we're allowed to and want to intercept its  
       // events  
       if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
           //....  
       }  
       // 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);  

       return target.dispatchTouchEvent(ev);  
   }  

dispatchTouchEvent()中ACTION_UP:

public boolean dispatchTouchEvent(MotionEvent ev) {  
       if (!onFilterTouchEventForSecurity(ev)) {  
           return false;  
       }  

       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;  

       boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  

       if (action == MotionEvent.ACTION_DOWN) {...}  

boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
               (action == MotionEvent.ACTION_CANCEL);  

if (isUpOrCancel) {  
           mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
       }  
final View target = mMotionTarget;  
if(target ==null ){...}  
if (!disallowIntercept && onInterceptTouchEvent(ev)) {...}  

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

       return target.dispatchTouchEvent(ev);  
   }  

正常情况下,即我们上例整个代码的流程我们已经走完了:
1、ACTION_DOWN中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则找到包含当前x,y坐标的子View,赋值给mMotionTarget,然后调用 mMotionTarget.dispatchTouchEvent
2、ACTION_MOVE中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)
3、ACTION_UP中,ViewGroup捕获到事件,然后判断是否拦截,如果没有拦截,则直接调用mMotionTarget.dispatchTouchEvent(ev)
当然了在分发之前都会修改下坐标系统,把当前的x,y分别减去child.left 和 child.top ,然后传给child;
也就是说,三个动作根据返回的值来决定是否继续分发至下一级,如果继续分发,那就将此动作的坐标值作为参数传给自己的onInterceptTouchEvent(ev)方法。这里值得注意的是返回值的关键在 if(disallowIntercept || !onInterceptTouchEvent(ev))这一句代码,通过这一句代码判断是否需要进行拦截,局部boolean类型的变量disallowIntercept (是否驳回拦截,通过该类中的viewGroup.requestDisallowInterceptTouchEvent(boolean)方法进行赋值,后面继续讲)及onInterceptTouchEvent(ev)的返回值,默认是进入该if语句中的方法,并通过遍历得到子View并调用子View的dispatchTouchEvent(ev)方法将事件传递下去

事件拦截源码

上面事件传递到子View的dispatchTouchEvent(ev)方法的前提是当ViewGroup.onInterceptTouchEvent(ev)不拦截的时候(disallowIntercept的默认为false,不重新赋值的情况下)
如果当ViewGroup.onInterceptTouchEvent(ev)不一样呢,我们复写下面这一段代码:

@Override  
    public boolean onInterceptTouchEvent(MotionEvent ev)  
    {  
        int action = ev.getAction();  
        switch (action)  
        {  
        case MotionEvent.ACTION_DOWN:  
            //如果你觉得需要拦截  
            return true ;   
        case MotionEvent.ACTION_MOVE:  
            //如果你觉得需要拦截  
            return true ;   
        case MotionEvent.ACTION_UP:  
            //如果你觉得需要拦截  
            return true ;   
        }  

        return false;  
    } 

默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这要该事件就不会往子View传递了,并且如果你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。
原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null 。

好的,重点来了,当MOVE时return true,子View还想拿到MOVE和UP事件怎么办呢?方法很简单,我们回顾下一开始的ViewGroup中的dispatchTouchEvent(ev)–ACTION_DOWN中的源码:

  • 在源码里如果不重写,该方法默认会找到被点击的子View。
  • 并调用该子View的dispatchTouchEvent(ev)方法
  • 如果子View想禁止ViewGroup在MOVE的时候拦截事件,那么机会来了,当调用子类的dispatchTouchEvent(ev)方法的时候,我们在子类的方法中添加这句代码getParent().requestDisallowInterceptTouchEvent(boolean)传入true即将ViewGroup中的ViewGroup的变量disallowIntercept的值 设置为true了,也就是驳回拦截,那么当ViewGroup再次MOVE和UP时,就直接忽略ViewGroup的拦截方法的返回值了,直接进入IF语句将事件往下传递。
  • 当然,如果ViewGroup一开始DOWN时就直接反馈true,那么上面的方式是无效的

总结

事件分发的流程大致情况如上,本次重点想让大家理解是当ViewGroup的MOVE时事件被拦截了,View也可以获取事件的方法及原理。个人见解,如果有误,欢迎指正,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值