Android事件分發機制之ViewGroup

一,寫在前面  

       相信大家一定遇到過這樣的問題:在滑動某一控件時,由於有多個控件都能處理當前滑動,比如:Viewpager中有一個子元素是Viewpager,那麼在左右滑動的時候,到底是哪個Viewpager控件去處理事件,並最終消費掉事件呢?這裏其實就是產生了滑動衝突了,就像遊戲裏打野搶ad資源,本來團隊是將資源給ad發育的,打野一個技能收掉了,於是需要一個服衆的領導者去處理衝突,讓該拿資源的人吃資源。而,事件滑動衝突發生了,作爲領導者的我們,就去處理衝突,於是要理解衝突發生的原因,這樣控件才能“心服口服”。

二,初識事件分發

         當我們的手觸摸到屏幕上時,一般有這樣三個操作Action_down,Action_move,Action_up,分別對應手指的按下,移動,擡起操作。該三個操作加在一起是一個事件序列,按照Action_down,Action_move,Action_up的順序。當有一個事件發生時,最開始接受該事件的是應用程序的窗口-Activity,然後交給PhoneWindow處理,再傳遞給DecorView處理,最後會傳遞給setContentView(R.layout.xxx)的根View,然後傳遞給下一層的子元素(若子元素有多個,則會按時間逆序傳遞,即,最晚添加的子元素,最先傳遞),一層一層類此......直到傳遞到最後的葉子節點(必爲非容器控件)。

        在前面,一層層傳遞的過程中,是假設傳遞過程中沒有控件攔截事件,這樣才能傳遞到最後的葉子節點。當事件傳遞到某一個view時,這個view的dispatchTouchEvent(ev)會被調用,若該view對該事件攔截,那麼onInterceptTouchEvent(ev)被調用並返回true,並執行onTouchEvent(ev)對事件進行具體的處理。完整的事件處理走向,這裏就不碼文字介紹了,後面會通過源碼來解釋事件分發的流程。

        方法介紹

         下面介紹三個方法:

         dispatchTouchEvent(ev):只要事件傳遞到View,就會調用該方法。返回true,則事件被處理並消費;返回false,則事件沒有消費掉,如果是action_down,事件交給父view處理;如果是action_move,action_up,則事件直接交給Activity處理。

         onInterceptTouchEvent(ev):該方法只有繼承了ViewGroup的容器控件纔有,原始的View(非容器控件)沒有子View,因此沒有該方法。該方法被調用後,如果返回true,代表要攔截事件,則view會調用onTouchEvent(ev)處理事件;如果返回false,代表不攔截,則事件會傳遞到子View。

         onTouchEvent(ev):容器控件和原始的View處理事件的地方,如果返回true,則事件被消費,dispatchTouchEvent(ev)返回true;如果返回false,則事件沒有消費。

下面列出Activity,View,Viewgroup,AbsListView,TextView的關係圖,以便清晰認識哪些方法是繼承的,哪些方法是重寫。如下:


下面展示上述三個方法在處理事件時的僞代碼:

		public boolean dispatchTouchEvent(MotionEvent ev) {
			//是否消費標誌
			boolean isConsume = false;
			if (onInterceptTouchEvent(ev)) {
				//如果攔截
				isConsume = onTouchEvent(ev);
			} else {
				//子元素處理
				isConsume = child.dispatchTouchEvent(ev);
			}
			return isConsume;
		}

三,源碼分析

事件最開始交給Activity處理,查看Activity$dispatchTouchEvent源碼:

/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     * 
     * @param ev The touch screen event.
     * 
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }


        參數MotionEvent:對事件的觸摸位置,時間等數據的封裝。事件的傳遞具體在代碼上的體現,就是對MotionEvent對象的傳遞處理。

        當getWindow().superDispatchTouchEvent(ev)爲true時,return true,dispatchTouchEvent對事件處理完畢,事件交給if條件裏對應的view處理了(具體是哪個view後面會分析)。如果getWindow().superDispatchTouchEvent(ev)返回false,即沒有view處理,會執行return onTouchEvent(ev),交給Activity的onTouchEvent(ev)處理,不再說明Activity的onTouchEvent(ev)如何處理事件,這不是本篇文章重點。
        接下來看if 條件裏的內容,getWindow()對應Window的唯一子類PhoneWindow,查看PhoneWindow$superDispatchTouchEvent(ev)源碼:

 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

mDecor就是DecorView對象,事件於是交給DecorView的superDispatchTouchEvent(ev)處理,查看該源碼:

 private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

	//...code
	public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
	//...code
 }
        該方法裏調用了super.dispatchTouchEvent(event)對事件進行處理,分析DecorView的繼承關係,它是繼承FrameLayout,說明事件轉發肯定會調用View裏的dispatchTouchEvent(ev)。然後事件會傳遞到setContentView(R.layout.xxx)對應佈局文件裏的根View,然後一層層轉發。至於,事件是如何從DecorView傳遞到根View的,不作分析,可以肯定的是事件肯定是傳遞到了根View,不然如何能響應點擊事件呢,這裏不影響分析事件分發,主要是研究根View以後的轉發情況,來理解處理滑動衝突。


       一般情況,根View是一個容器控件,然後向下傳遞事件,那麼就先拉出容器控件的爸爸->ViewGroup的代碼進行分析。查看ViewGroup$dispatchTouchEvent(ev)源碼如下:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //  ...code

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

//A
		
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
		//設置變量mGroupFlags的最高位爲0
                resetTouchState();
            }
//B
            // Check for interception.
            final boolean intercepted;
	    //若子View處理並消費了action_down事件,mFirstTouchTarget不爲空
            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;
            }
//C
            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    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) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final View[] children = mChildren;

                        final boolean customOrder = isChildrenDrawingOrderEnabled();
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder ?
                                    getChildDrawingOrder(childrenCount, i) : i;
                            final View child = children[childIndex];
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownIndex = childIndex;
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }

                    // code...
                }
            }
//D
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            //code ...
        return handled;
    }



    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
        注意:在上面代碼中,註釋有ABCD4個區域,分4塊來分析,一塊塊解決。現在先以ACTION_DOWN事件爲例來分析源碼,然後再分析action_move,action_up事件。下面類似於A-13這樣的標號:代表A區域,代碼的第13行。


        現在有一個action_down事件傳遞到了容器控件(縮寫爲vp),於是dispatchTouchEvent(ev)被調用。第5行定義的 boolean handled = false;會被return,它的值表明該事件是否被消費掉了。執行到A-13,進入if語句,會調用resetTouchState(),進入該方法發現有這樣一段代碼:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;目的是設置變量mGroupFlags的最高位爲0,其中FLAG_DISALLOW_INTERCEPT爲0x8000;也就是說只要是action_down事件傳遞過來,那麼會對字段mGroupFlags進行一個最高位爲0的處理(後面自有用處)。

        程序繼續指定到A-25行,由於這裏是action_down事件,那麼actionMasked == MotionEvent.ACTION_DOWN爲true,變量mFirstTouchTarget是指當vp的子元素處理消費了action_down/move/up事件時,mFirstTouchTarget指向一個對象地址,不爲空(後面自有用處)。

       

        B-25值爲true,進入if語句,分析B-27行:mGroupFlags值受上面提到resetTouchState()的改變,還會受ViewGroup$requestDisallowInterceptTouchEvent(boolean)的改變,該方法會使mGroupFlags的最高位爲1。action_down事件在設置了mGroupFlags最高位爲0,會使disallowIntercept值爲false,進入B-28的if語句中,執行intercepted = onInterceptTouchEvent(ev)。requestDisallowInterceptTouchEvent一般在子View中調用,使mGroupFlags的最高位爲1,會使disallowIntercept值爲true,進入B-31的else語句中,不讓vp執行onInterceptTouchEvent(ev),而直接設置intercepted = false,不讓父控件vp攔截該事件。注意:若事件爲action_down,子控件調用requestDisallowInterceptTouchEvent方法無法阻止父控件vp對事件進行攔截,原因:前面A-13的作用就是爲了讓父控件始終能有機會攔截到action_down事件。至於,最終vp是否攔截事件,還要看onInterceptTouchEvent方法裏面的具體處理。於是,查看ViewGroup$onInterceptTouchEvent(ev)源碼:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
         哇,ViewGroup$onInterceptTouchEvent(ev)始終返回false,不攔截事件。所以一般容器控件了爲了處理一些滑動,會重寫onInterceptTouchEvent(ev)對事件進行攔截,例如AbsListView。

   

        那麼B-32行的else語句什麼時候執行呢?第一種情況:若前面的action_down/move/up事件被vp攔截了(onInterceptTouchEvent(ev)返回true),那麼intercepted = true,不會再執行onInterceptTouchEvent(ev)方法,vp會將一個事件序列全部攔截處理;第二種情況:若vp沒有攔截事件action_down/move/up,但是子view處理事件後,沒有消費掉事件,會使mFirstTouchTarget爲null,執行intercepted = true,後面的事件交給父控件vp處理了。

         結論

        1,事件爲action_down時,容器控件Viewgroup肯定會執行onInterceptTouchEvent(ev)方法,判斷是否要攔截,;若攔截了,則後面傳遞的action_move/up事件不會再執行onInterceptTouchEvent(ev)方法判斷是否攔截,而是都攔截給vp自己處理,這裏就沒有子View什麼事啦。

        2,若action_down事件能傳遞到子View,即vp沒有攔截,若子View及其子元素沒有一個view能消費掉action_down事件,那麼該事件會交給子View的父控件vp處理(後面有代碼分析證明),同時一個序列事件中的action_move/up也不會再傳遞給子View處理,而是都交給它的父View處理。(上面一段已證明)

3,在2的基礎上,子View及其子元素有一個view能消費掉action_down事件時,但後面的action_move/up事件沒有消費,那麼該事件不會再給父控件vp處理,而是直接交給Activity處理。(後面有分析證明這點,這裏先一起提出來)

         

        繼續分析源碼(這裏還是先分析action_down):如果vp不攔截action_down事件,那麼C-48中值爲true,進入if語句;C-49,C-50,C-51都是判斷action_down事件的,其他事件無法進入if語句。執行到C-61,若vp的子元素不爲0,則進入if語句;繼續執行到C-69,遍歷子元素,執行到C-73,C-74。isTransformedTouchPointInView(x, y, child, null):代表觸摸點是否在該子元素佈局內,canViewReceivePointerEvents(child):是否在執行動畫。若兩者均爲true,則繼續執行到C-87;否則執行continue,當前循環結束,遍歷下一個子元素。

         代碼執行到C-87,調用dispatchTransformedTouchEvent方法,這裏就是子View處理action_down的地方,查看ViewGroup$dispatchTransformedTouchEvent源碼,裏面有這樣一段代碼:

if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
         簡單解釋下:當child爲null時,事件由vp處理,並調用super.dispatchTouchEvent(event)處理。(後面會分析這個調用)

        child不爲空時,上面C-87的child就不爲空,調用child.dispatchTouchEvent(event),這就是前面說的事件傳遞到view,那麼會調用view的dispatchTouchEvent(event)方法。若child爲原始的View(非容器控件),無法再傳遞事件了,那麼直接交給child處理。若child爲容器控件,則繼續重複上面的流程,將事件依次轉發下去,直到有一個view消費了事件,child.dispatchTouchEvent(event)返回true,handled爲true;如果沒有一個view消費事件,那麼child.dispatchTouchEvent(event)返回false,handled爲false。

        這裏的handled對應C-87的if語句的條件,若條件爲true,會執行newTouchTarget = addTouchTarget(child, idBitsToAssign),alreadyDispatchedToNewTouchTarget = true。若條件爲false,則不執行上面兩步。進入ViewGroup$addTouchTarget源碼:

/**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
        addTouchTarget方法內部會對mFirstTouchTarget設置值,使其不爲空。於是證明前面的分析:當子View消費了action_down/move/up事件時,mFirstTouchTarget不爲空。另外,還設置標誌位alreadyDispatchedToNewTouchTarget 爲true。(後面自有用處)


       繼續分析源碼,現在分析到了D區域,這裏將對ViewGroup$dispatchTouchEvent(ev)方法確定返回值,反應事件在容器控件vp的處理情況。如果返回值handled爲true,則該事件(action_down/move/up)被消費了,會使得Activity$dispatchTouchEvent(ev)中的getWindow().superDispatchTouchEvent(ev)) 的值爲true,執行if語句,return true,消費了事件,事件處理結束;反之,繼續執行Activity$dispatchTouchEvent(ev)中的return onTouchEvent(ev);將事件交給Activity處理。

       流程執行到D-105,若action_down事件被vp攔截,或者能傳遞到子View但是沒消費。那麼,if語句值爲true,執行handled = dispatchTransformedTouchEvent(ev, canceled, null,ouchTarget.ALL_POINTER_IDS),參數裏的null,就是前面說的child爲null的情況,執行super.dispatchTouchEvent(event)。這裏ViewGroup的super就是View啦,所以容器控件vp處理事件是調用View$dispatchTouchEvent(ev),裏面調用onTouchEvent(ev)處理事件,下篇文章會分析View$dispatchTouchEvent(ev)如何決定是否消費事件,這裏就打住了。事件處理最終會交給onTouchEvent方法(下一篇文章會講),若onTouchEvent(ev)返回true,則super.dispatchTouchEvent(ev)返回true,消費了事件,那麼此事件的處理就結束了;反之,交給Activity處理。執行了D-105的if語句內容,那麼else中不會執行。


        若子view消費了action_down事件,D-116行if條件表達式值爲true,action_down事件消費了,該事件處理流程結束。若子view已經消費了action_down事件,現在傳遞的是action_move/up,則代碼會執行到D-121,D-122行,如果能消費action_move/up事件,return true,該事件處理流程結束;若不能消費,即D-121,D-122行條件表達式爲false,那麼handled不設置爲true,return默認的值false,事件不會交給父View處理,直接交給Activity處理。(此處證明前面的一個結論)

四,另外

       最後,分析到這裏,相信不管是action_down事件,還是其他action_move/up事件,這些事件在View中的分發流程基本理清楚了。但是,還沒有介紹事件交給View$onTouchEvent(ev)處理的流程,放在下一篇博客中分析吧。本篇文章基於源碼的分析,並沒有列出利用其解決滑動衝突的方案,但是相信理清了事件分發的流程,稍微記住些小結論用於實踐,處理滑動衝突就可以知其所以然的。其實滑動衝突有兩種解決方案:1,外部攔截法;2,內部攔截法。這裏先作一個引子,後面會碼一篇blog展示如何處理滑動衝突。


       





            

        





發佈了47 篇原創文章 · 獲贊 28 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章