View事件傳遞之父View和子View之間的那點事


Android事件傳遞流程在網上可以找到很多資料,FrameWork層輸入事件和消費事件,可以參考:

  1. [Touch事件派發過程詳解] 1

這篇blog闡述了底層是如何處理屏幕輸,並往上傳遞的。Touch事件傳遞到ActivityDecorView時,往下走就是ViewGroup和子View之間的事件傳遞,可以參考郭神的這兩篇博客

  1. [Android事件分發機制完全解析,帶你從源碼的角度徹底理解(上)] 3
  2. [Android事件分發機制完全解析,帶你從源碼的角度徹底理解(下)] 4

郭神的兩篇博客清楚明白地說明了View之間事件傳遞的大方向,但是具體的一些晦暗的細節闡述較少,本文主要是總結這兩篇博客的同時,側重於兩點:

  1. 事件分發過程中一些細節到底如何實現的?
  2. view到底如何和父View搶事件,父View又是如何攔截事件不發送給子View,以及如果我們需要處理這種混亂的關系才能讓兩者和諧相處?。

MotionEvent抽象

要明白View的事件傳遞,很有必要先說一下Touch事件是如何在Android系統中抽象的,這主要使用的就是MotionEvent。這個類經歷了幾次重大的修改,一次是在2.x版本支持多點觸摸,一次是4.x將大部分代碼甩給native層處理。

一次簡單的事件

我們先舉個栗子來說明一次完整的事件,用戶觸屏 滑動 到手機離開屏幕,這認為是一次完整動作序列(movement traces)。一個動作序列中包含很多動作Action,比如在用戶按下時,會封裝一個MotionEvent,分發給視圖樹,我們可以通過motionevent.getAction拿到這個動作是ACTION_DOWN。同樣,在手指抬起時,我們可以接收到Action類型是Action_UPMotionEvent。對於滑動(MOVE)這個操作,Android為了從效率出發,會將多個MOVE動作打包到一個MotionEvent中。通過getX getY可以獲取當前的坐標,如果要訪問打包的緩存數據,可以通過getHistorical**()函數來獲取。

加入多點觸摸

對於單點的操作來看,MotionEvent顯得比較簡單,但是考慮引入多點觸摸呢?我們定義一個接觸點為(Pointer)。我們從onTouch接受到一個MotionEvent,怎么拿到多個觸碰點的信息?為了解開筆者剛開始學習這部分知識時的困惑,我們首先樹立起一種概念:一個MotionEvent只允許有一個Action(動作),而且這個Action會包含觸發這次Action的觸碰點信息,對於MOVE操作來說,一定是當前所有觸碰點都在動。只有ACTION_POINTER_DOWN這類事件事件會在Action里面指定是哪一個POINTER按下。

MotionEvent的底層實現中,是通過一個16位來存儲ActionPointer信息(PointerIndex)。低8位表示Action,理論上可以表示255種動作類型;高8位表示觸發這個ActionPointerIndex,理論上Android最多可以支持255點同時觸摸,但是在上層代碼使用的時候,默認多點最多存在32個,不然事件在分發的時候會有問題。

MotionEvent中多個手指的操作API大部分都是通過pointerindex來進行的,如:獲取不同Pointer的觸碰位置,getX(int pointerIndex);獲取PointerId等等。大部分情況下,pointerid == pointeridex

ACTION_DOWN OR ACTION_POINTER_DOWN:

這兩個按下操作的區別是ACTION_DOWN是一個系列動作的開始,而ACTION_POINTER_DOWN是在一個系列動作中間有另外一個觸碰點觸碰到屏幕。

這部分詳細的描述,請參考:
android觸控,先了解MotionEvent

到這里,鋪墊終於結束了,我們開始直奔主題。

View的事件傳遞

AndroidTouch事件傳遞到Activity頂層的DecorView(一個FrameLayout)之后,會通過ViewGroup一層層往視圖樹的上面傳遞,最終將事件傳遞給實際接收的View。下面給出一些重要的方法。如果你對這個流程比較熟悉的話,可以跳過這里,直接進入第二部分。

dispatchTouchEvent

事件傳遞到一個ViewGroup上面時,會調用dispatchTouchEvent。代碼有刪減

public boolean dispatchTouchEvent(MotionEvent ev) {

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

        // Attention 1 :在按下時候清除一些狀態
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            //注意這個方法
            resetTouchState();
        }

        // Attention 2:檢查是否需要攔截
        final boolean intercepted;
        //如果剛剛按下 或者 已經有子View來處理
        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 {
            //  不是一個動作序列的開始 同時也沒有子View來處理,直接攔截
            intercepted = true;
        }

		  //事件沒有取消 同時沒有被當前ViewGroup攔截,去找是否有子View接盤
        if (!canceled && !intercepted) {
				//如果這是一系列動作的開始  或者有一個新的Pointer按下 我們需要去找能夠處理這個Pointer的子View
            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
                
                //上面說的觸碰點32的限制就是這里導致
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    
                    //對當前ViewGroup的所有子View進行排序,在上層的放在開始
                    final ArrayList<View> preorderedList = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder
                                ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = (preorderedList == null)
                                ? children[childIndex] : preorderedList.get(childIndex);
							
							  // canViewReceivePointerEvents visible的View都可以接受事件
							  // isTransformedTouchPointInView 計算是否落在點擊區域上
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
							
							  //能夠處理這個Pointer的View是否已經處理之前的Pointer,那么把
                        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;
                        }                           }
                        //Attention 3 : 直接發給子View
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                // childIndex points into presorted list, find original index
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                    }
                }

            }
        }

        // 前面已經找到了接收事件的子View,如果為NULL,表示沒有子View來接手,當前ViewGroup需要來處理
        if (mFirstTouchTarget == null) {
            // ViewGroup處理
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
           
           	if(alreadyDispatchedToNewTouchTarget) {
           		               	//ignore some code
            		if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
             	}
           	}

        }
    return handled;
}

上面代碼中的Attention在后面部分將會涉及,重點注意。

這里需要指出一點的是,一系列動作中的不同Pointer可以分配給不同的View去響應。ViewGroup會維護一個PointerId和處理View的列表TouchTarget,一個TouchTarget代表一個可以處理Pointer的子View,當然一個View可以處理多個Pointer,比如兩根手指都在某一個子View區域。TouchTarget內部使用一個int來存儲它能處理的PointerId,一個int 32位,這也就是上層為啥最多只能允許同時最多32點觸碰。

看一下Attention 3 處的代碼,我們經常說viewdispatchTouchEvent如果返回false,那么它就不能系列動作后面的動作,這是為啥呢?因為Attention 3處如果返回false,那么它不會被記錄到TouchTarget中,ViewGroup認為你沒有能力處理這個事件。

這里可以看到,ViewGroup真正處理事件是在dispatchTransformedTouchEvent里面,跟進去看看:

dispatchTransformedTouchEvent

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {

	  //沒有子類處理,那么交給viewgroup處理
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }
    return handled;
}

可以看到這里不管怎么樣,都會調用ViewdispatchTouchEvent,這是真正處理這一次點擊事件的地方。

dispatchTouchEvent

	public boolean dispatchTouchEvent(MotionEvent event) {
		if (onFilterTouchEventForSecurity(event)) {
        //先走View的onTouch事件,如果onTouch返回True
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
  	  return result;
	}

我們給View設置的onTouch事件處在一個較高的優先級,如果onTouch執行返回true,那么就不會去走viewonTouchEvent,而我們一些點擊事件都是在onTouchEvent中處理的,這也是為什么onTouch中返回true,view的點擊相關事件不會被處理。

小小總結一下這個流程

ViewGroup在接受到上級傳下來的事件時,如果是一系列Touch事件的開始(ACTION_DOWN),ViewGroup會先看看自己需不需要攔截這個事件(onInterceptTouchEventViewGroup的默認實現直接返回false表示不攔截),接着ViewGroup遍歷自己所有的View。找到當前點擊的那個View,馬上調用目標ViewdispatchTouchEvent。如果目標ViewdispatchTouchEvent返回false,那么認為目標View只是在那個位置而已,它並不想接受這個事件,只想安安靜靜的做一個View(我靜靜地看着你們裝*)。此時,ViewGroup還會去走一下自己dispatchTouchEvent,Done!

子View和父View的撕*大戰

終於來到本文的重要環節,子View和父布局(ViewGroup)是如何撕逼的。我們經常遇到這樣的問題:在ListView中放一個ViewPager不能滑動的問題,其實這里就會涉及到子View和布局之間的協商,事件處理到底你上還是我上。

首先需要明確一點的是,一個事件肯定是由ViewGroup傳遞給自己的子View的,所以ViewGroup具有絕對的權威來禁止事件往下傳,這就是onInterceptTouchEvent方法。可以看上面ViewGroup中的dispatchTouchEventAttention 1 Attention 2

先看Attetion2
進行判斷有有兩個條件:1,如果是一次新的事件 or 在一次事件中但是已經有子View來處理這個事件,那么父類需要去看看是否攔截這次事件。否則,直接攔截(此時處於一系列動作的中間,而且沒有子view來接盤,那么ViewGroup就直接攔下來)。

決定是否攔截有兩個步驟,

  1. disallowIntercept 是否駁回攔截,默認false。注意這個值是子View和撕*的關鍵,因為ViewGroup開放了給這個標記賦值的接口requestDisallowInterceptTouchEvent(),而且這個方法直接往上遞歸,這個ViewGroup的各級父容器都會設置駁回攔截。
  2. onInterceptTouchEvent 雖然ViewGroup中默認返回false,但是在很多有滑動功能的ViewGroup里面(如scrollview ListView等)會處理各種情況,決定是否攔截這個事件,所以就會出現之前說的ListView中的Viewpager不能滑動的問題,原因是事件被父View攔截了。

Attetion1的位置如果是一次新的ACTION_DOWN,那么會把之前事件傳遞設置的各種狀態清除。

對ViewGroup來說需要做什么

對於一個需要攔截事件的ViewGroup,它通常都有一些特殊的操作,比如ScrollView,比如ViewPager,它重寫onInterceptTouchEvent是非常關鍵的,這也是能和子View和諧相處的關鍵。舉個例子,我自己定義了一個ViewGroup

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if(ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
        return true;
    }
    return super.onInterceptTouchEvent(ev);
}

這樣會發生什么?

所有位於MyViewGroup中的子View收不到任何的事件,原因可以看一下Attention2的代碼,判斷是否攔截是在系列動作按下時會進行判斷,如果此時攔截,那么直接不會去查找相應處理的子View,所以touchtarget為空,那么接下來的動作都直接被ViewGroup笑納。

所以哪怕再強勢的ViewGroup,一般都是在Down的時候給子類機會去掉用requestDisallowInterceptTouchEvent,如設置駁回攔截,那么在ViewGroup分發事件的時候,會跳過onInterceptTouchEvent的執行。

子View需要做什么

對於子View來說,在合適的時機調用requestDisallowInterceptTouchEvent即可。當然啥時候合適?對於一個View來說,那就是在dispatchTouchEvent或者onTouchEvent來調用。

對於ViewGroup來說,通常我們會在onInterceptTouchEvent進行判斷。比如我們經常會遇到在ListView里面套了ViewPager導致ViewPager不能滑動的問題,通常的處理方式:

 @Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (absListView != null) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();
                mDownY = event.getY();
					//ACTION_DOWN的時候,趕緊把事件hold住
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:

                if(Math.abs(event.getX() - mDownX)>Math.abs(event.getY()-mDownY)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }else {
                	//發現不是自己處理,還給父類
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
            		//其實這里是多余的
                getParent().requestDisallowInterceptTouchEvent(false);
        }

    }
    return super.onInterceptTouchEvent(event);
}

總結

本來打算寫一個短篇的,結果一個不小心,弄成了長篇大論。

最后需要注意一點的是,所有我們上述討論的內容都是在一層層遞歸中進行,而且requestDisallowInterceptTouchEvent這個函數也是遞歸調用的。

我們可以認為ViewGroup是一個具有絕對話語權但是從不專政的霸道總裁,它自己可以攔截處理某些事件,比如Viewpager的橫滑,但是它也可以給子View足夠的空間去要求這個事件給自己處理。作為一名開發者,一方面在自己定義ViewGroup時需要考慮能夠給子View足夠空間中斷自己的攔截;一方面自己定義View時,我們需要在合適的時候跟父View索要事件。ViewPager(新版)作為容器來說,它需要攔截橫滑事件,同時,自己具備了和父View爭搶事件的能力,所以不管把ViewPager放到什么布局中,它都能正確處理。看看它的onInterceptTouchEvent怎么寫的吧,完美的體現了這一思想。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM