Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View 事件分發機制源碼詳解(ViewGroup篇)

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

編輯:關於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) {
            ...
        }

可以看出,這裡獲取了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 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的事件分發機制源碼已經分析完畢。下面給出一幅流程圖來描述一下以上所分析的內容:

\

總結

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()方法去詢問是否要攔截了。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved