Android View 事件分發機制源碼詳解(ViewGroup篇)

JarredMulli 8年前發布 | 26K 次閱讀 Android開發 移動開發 Android

前言

我們在學習View的時候,不可避免會遇到事件的分發,而往往遇到的很多滑動沖突的問題都是由于處理事件分發時不恰當所造成的。因此,深入了解View事件分發機制的原理,對于我們來說是很有必要的。由于View事件分發機制是一個比較復雜的機制,因此筆者將寫成兩篇文章來詳細講述,分別是ViewGroup和View。因為我們平時所接觸的View都不是單一的View,往往是由若干個ViewGroup組合而成,而事件的分發又是由ViewGroup傳遞到它的子View的,所以我們先從ViewGroup的事件分發說起。注意,以下源碼取自安卓5.0(API 21)。

三個重要方法

public boolean dispatchTouchEvent(MotionEvent ev)

該方法用來進行事件的分發,即無論ViewGroup或者View的事件,都是從這個方法開始的。

public boolean onInterceptTouchEvent(MotionEvent ev)

在上一個方法內部調用,表示是否攔截當前事件,返回true表示攔截,如果攔截了事件,那么將不會分發給子View。比如說:ViewGroup攔截了這個事件,那么所有事件都由該ViewGroup處理,它內部的子View將不會獲得事件的傳遞。(但是ViewGroup是默認不攔截事件的,這個下面會解釋。)注意:View是沒有這個方法的,也即是說,繼承自View的一個子View不能重寫該方法,也無需攔截事件,因為它下面沒有View了,它要么處理事件要么不處理事件,所以最底層的子View不能攔截事件。

public boolean onTouchEvent(MotionEvent ev)

這個方法表示對事件進行處理,在dispatchTouchEvent方法內部調用,如果返回true表示消耗當前事件,如果返回false表示不消耗當前事件。

以上三個方法非常重要,貫穿整個View事件分發的流程,它們的關系可以用如下偽代碼呈現:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean handle = false;
    if(onInterceptTouchEvent(ev)){
        handle = onTouchEvent(ev);
    }else{
        handle = child.dispatchTouchEvent(ev);
    }
    return handle;
}

由以上偽代碼可得出如下結論:如果一個事件傳遞到了ViewGroup處,首先會判斷當前ViewGroup是否要攔截事件,即調用onInterceptTouchEvent()方法;如果返回true,則表示ViewGroup攔截事件,那么ViewGroup就會調用自身的onTouchEvent來處理事件;如果返回false,表示ViewGroup不攔截事件,此時事件會分發到它的子View處,即調用子View的dispatchTouchEvent方法,如此反復直到事件被消耗掉。
接下來,我們將從源碼的角度來分析整個ViewGroup事件分發的流程是怎樣的。

從Activity到根ViewGroup

我們知道,事件產生于用戶按下屏幕的一瞬間,事件生成后,經過一系列的過程來到我們的Activity層,那么事件是怎樣從Activity傳遞到根ViewGroup的呢?由于這個問題不在本文的討論范圍,所以這里簡單提一下:事件到達Activity時,會調用Activity#dispatchTouchEvent方法,在這個方法,會把事件傳遞給Window,然后Window把事件傳遞給DecorView,而DecorView是什么呢?它其實是一個根View,即根布局,我們所設置的布局是它的一個子View。最后再從DecorView傳遞給我們的根ViewGroup。
所以在Activity傳遞事件給ViwGroup的流程是這樣的:Activity->Window->DecorView->ViewGroup

ViewGroup事件分發源碼解析

接下來便是本文的重要,對ViewGroup#dispatchTouchEvent()方法源碼進行解讀,由于源碼比較長,所以這里分段貼出。

1、對ACTION_DOWN事件初始化

首先看如下所示的源碼:

    ...
    // 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.
          //這里把mFirstTouchTarget設置為null
          cancelAndClearTouchTargets(ev);
          resetTouchState();
      }

首先這里先判斷事件是否為DOWN事件,如果是,則初始化,把mFirstTouchTarget置為null。由于一個完整的事件序列是以DOWN開始,以UP結束,所以如果是DOWN事件,那么說明是一個新的事件序列,所以需要初始化之前的狀態。這里的mFirstTouchTarget非常重要,后面會說到當ViewGroup的子元素成功處理事件的時候,mFirstTouchTarget會指向子元素,這里要留意一下。

2、檢查ViewGroup是否要攔截事件

接著我們往下看:

// Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {  // 1
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);    // 2
            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;
    }
    ...
    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)  || actionMasked == MotionEvent.ACTION_CANCEL;

以上代碼主要判斷ViewGroup是否要攔截事件。定義了一個布爾值intercept來記錄是否要進行攔截,這在后面發揮很重要的作用。
①號代碼處,首先執行了這個語句:if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null),也即是說,如果事件是DOWN或者mFirstTouchTatget值不為空的時候,才有可能執行②號代碼,否則會直接跳過判斷是否攔截。為什么要有這個判斷呢?這里解釋一下,比如說,子View消耗了ACTION_DOWN事件,然后這里可以由ViewGroup繼續判斷是否要攔截接下來的ACTION_MOVE事件之類的;又比如說,如果第一次DOWN事件最終不是由子View消耗掉的,那么顯然mFirstTouchTarget將為null,所以也就不用判斷了,直接把intercept設置為true,此后的事件都是由這個ViewGroup處理。
②號處調用了onInterceptTouchEvent()方法,那么我們可以跟進去看看這個onInterceptTouchEvent()做了什么。
2.1、ViewGroup#onInterceptTouchEvent()

public boolean onInterceptTouchEvent(MotionEvent ev) { 
    return false; 
}

可以看出,ViewGroup#onInterceptTouchEvent()方法是默認返回false的,即ViewGroup默認不攔截任何事件,如果想要讓ViewGroup攔截事件,那么應該在自定義的ViewGroup中重寫這個方法。
2.2、我們再看看原來的代碼,會發現還有一個FLAG_DISALLOW_INTERCEPT標志位,這個標志位的作用是禁止ViewGroup攔截除了DOWN之外的事件,一般通過子View的requestDisallowInterceptTouchEvent來設置。
2.3、最后判斷是否是CANCEL事件。

根據以上分析,這里小結一下:當ViewGroup要攔截事件的時候,那么后續的事件序列都將交給它處理,而不用再調用onInterceptTouchEvent()方法了,所以該方法并不是每次事件都會調用的。

3、對ACTION_DWON事件的特殊處理

返回ViewGroup#dispatchTouchEvent()源碼,我們繼續往下看。
接下來是一個If判斷語句,內部還有若干if語句,以下先省略所有if體的內容,我們從大體上認識這塊代碼的作用:

TouchTarget newTouchTarget = null;  // 1
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
    ...// IF體1
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        ...// IF體2
    }
}

首先,在每一次調用這個方法的時候,會執行①號代碼:在進行判斷之前,已經把newTouchTarget和alreadyDispatchedToNewTouchTarget置為null了,這里尤其注意。
接著,判斷if(!canceled && !intercepted),表示如果不是取消事件以及ViewGroup不進行攔截則進入IF體1,接著又是一個判斷if (actionMasked == MotionEvent.ACTION_DOWN …)這表示事件是否是ACTION_DOWN事件,如果是則進入IF體2,根據以上兩個IF條件,事件是ACTION_DOWN以及ViewGroup不攔截,那么IF體2內部應該是把事件分發給子View了,我們展開IF體2,看看內部實現了什么:

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) {
        ...// IF體3
    }
    if (newTouchTarget == null && mFirstTouchTarget != null) {
        ...
    }</code></pre> 

可以看出,這里獲取了childrenCount的值,表示該ViewGroup內部有多少個子View,如果有則進入IF體3,意思就是說,如果有子View就在IF體3里面開始遍歷所有子View判斷是否要把事件分發給子View。我們展開IF體3:

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 ArrayList<View> preorderedList = buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) { // 1
        final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex);
        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        if (!canViewReceivePointerEvents(child)                
                || !isTransformedTouchPointInView(x, y, child, null)) {  // 2
            ev.setTargetAccessibilityFocus(false);
            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);
        //把事件分發給子View
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 3
           // 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);  // 4
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

            // The accessibility focus didn't handle the event, so clear
            // the flag and do a normal dispatch to all children.
            ev.setTargetAccessibilityFocus(false);
        }
        if (preorderedList != null) preorderedList.clear();
}

代碼也比較長,我們只關注重點部分。先看①處的代碼,是一個for循環,這里表示對所有的子View進行循環遍歷,由于以上判斷了ViewGroup不對事件進行攔截,那么在這里就要對ViewGroup內部的子View進行遍歷,一個個地找到能接受事件的子View,這里注意到它是倒序遍歷的,即從最上層的子View開始往內層遍歷,這也符合我們平常的習慣,因為一般來說我們對屏幕的觸摸,肯定是希望最上層的View來響應的,而不是被覆蓋這的底層的View來響應,否則這有悖于生活體驗。然后②號代碼是If語句,根據方法名字我們得知這個判斷語句是判斷觸摸點位置是否在子View的范圍內或者子View是否在播放動畫,如果均不符合則continue,表示這個子View不符合條件,開始遍歷下一個子View。接著③號代碼,這里調用了dispatchTransformedTouchEvent()方法,這個方法有什么用呢?
3.1、我們看看這個方法,ViewGroup#dispatchTransformedTouchEvent():

...
final boolean handled;
if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
...

方法大體上是這樣,做了適當刪減,顯然,當傳遞進來的的child不為null時,就會調用子View的dispatchTouchEvent(event)方法,表示把事件交給子View處理,也即是說,子Viwe符合所有條件的時候,事件就會在這里傳遞給了子View來處理,完成了ViewGroup到子View的事件傳遞,當事件處理完畢,就會返回一個布爾值handled,該值表示子View是否消耗了事件。怎樣判斷一個子View是否消耗了事件呢?如果說子View的onTouchEvent()返回true,那么就是消耗了事件。
3.2、在③號代碼處判斷子View是否消耗事件,如果消耗了事件那么最后便會執行到④號代碼:addTouchTarget()。我們來看看這個方法:ViewGroup#addTouchTarget():

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

可以看到,在這個方法里面,把mFirstTouchTarget指向了child,同時把newTouchTarget也指向child,也即是說,如果子View消耗掉了事件,那么mFirstTouchTarget就會指向子View。在執行完④號代碼后,直接break了,表示跳出了循環,因為已經找到了處理事件的子View,所以無需繼續遍歷了。

小結:整一個if(!canceled && !intercepted){ … }代碼塊所做的工作就是對ACTION_DOWN事件的特殊處理。因為ACTION_DOWN事件是一個事件序列的開始,所以我們要先找到能夠處理這個事件序列的一個子View,如果一個子View能夠消耗事件,那么mFirstTouchTarget會指向子View,如果所有的子View都不能消耗事件,那么mFirstTouchTarget將為null

4、對除了ACTION_DOWN之外的其他事件的處理

第3點是對ACTION_DOWN事件的處理,那么不是ACTION_DOWN的事件將從以下開始處理:

// 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); // 1
    } 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)) { // 2
                handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    ...
    return handled;
}

首先是一個if判斷語句,判斷mFirstTouchTarget是否為Null,如果為null,那么調用①處的代碼:dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS),這個方法上面出現過了(見3.1),這里第三個參數為null,那么我們看方法體,會執行super.dispatchTouchEvent(event);這里意思是說,如果找不到子View來處理事件,那么最后會交由ViewGroup來處理事件。接著,如果在上面已經找到一個子View來消耗事件了,那么這里的mFirstTouchTarget不為空,接著會往下執行。
接著有一個if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)判斷,這里就是區分了ACTION_DOWN事件和別的事件,因為在第3.2點的分析我們知道,如果子View消耗了ACTION_DOWN事件,那么alreadyDispatchedToNewTouchTarget和newTouchTarget已經有值了,所以就直接置handled為true并返回;那么如果alreadyDispatchedToNewTouchTarget和newTouchTarget值為null,那么就不是ACTION_DOWN事件,即是ACTION_MOVE、ACTION_UP等別的事件的話,就會調用②號代碼,把這些事件分發給子View。

小結:最后這段代碼處理除了ACTION_DOWN事件之外的其他事件,如果ViewGroup攔截了事件或者所有子View均不消耗事件那么在這里交由ViewGroup處理事件;如果有子View已經消耗了ACTION_DOWN事件,那么在這里繼續把其他事件分發給子View處理。

至此,關于ViewGroup的事件分發機制源碼已經分析完畢。下面給出一幅流程圖來描述一下以上所分析的內容:
ViewGroup事件分發流程

總結

1、ACTION_DOWN事件為一個事件序列的開始,中間有若干個ACTION_MOVE,最后以ACTION_UP結束。
2、ViewGroup默認不攔截任何事件,所以事件能正常分發到子View處(如果子View符合條件的話),如果沒有合適的子View或者子View不消耗ACTION_DOWN事件,那么接著事件會交由ViewGroup處理,并且同一事件序列之后的事件不會再分發給子View了。如果ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的話,那么最后事件會交由Activity處理。即:逐層分發事件下去,如果都沒有處理事件的View,那么事件會逐層向上返回。
3、如果某一個View攔截了事件,那么同一個事件序列的其他所有事件都會交由這個View處理,此時不再調用View(ViewGroup)的onIntercept()方法去詢問是否要攔截了。

 

來源:http://blog.csdn.net/a553181867/article/details/51287844

 本文由用戶 JarredMulli 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!