1. 程式人生 > >Android事件分發之子View駁回ViewGroup攔截原理分析

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也可以獲取事件的方法及原理。個人見解,如果有誤,歡迎指正,謝謝!