Android 事件分發機制源碼解析-ViewGroup層

在上篇文章中我們分析了view的事件分發機制**《Android 事件分發機制源碼解析-view層》**,在本篇文章中我們繼續分析另一層viewGroup的事件分發,viewGroup本質上是一組view的集合,它的裏面包含了view和另一組viewGroup,我們平常使用的各種佈局如LinearLayout、RelativeLayout、FrameLayout等等都是繼承的viewGroup,對於viewgroup與view之前的關係,我們可以用一張圖來描述一下:
在這裏插入圖片描述

ViewGroup和View組成了一棵樹形結構,最頂層爲Activity的ViewGroup,下面有若干的ViewGroup節點,每個節點之下又有若干的ViewGroup節點或者View節點,依次類推。

接下來我們就開始進入正題分析viewGroup的事件分發。

分析工具

//Android源碼環境
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
}

//分析工具
Android Studio 2.2.3
Build #AI-145.3537739, built on December 2, 2016
JRE: 1.8.0_112-release-b05 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o

對於view事件分發,主要用到兩個方法,而對於viewGroup來說,比view多了一個方法,

onInterceptTouchEvent(MotionEvent ev)

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

另外兩個方法分別是:

//該方法用來進行事件的分發,即無論ViewGroup或者View的事件,都是從這個方法開始的。
public boolean dispatchTouchEvent(MotionEvent ev)

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

就是這三個方法決定着viewGroup層的事件分發,它們主要的作用可以通過以下的僞代碼來表示:

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 -> Window -> View

事件總是會傳遞給Activity,之後Activity再傳遞給Window,最後Window再傳遞給頂級的View,頂級的View在接收到事件後就會按照事件分發機制去分發事件。如果一個View的onTouchEvent返回了FALSE,那麼它的父容器的onTouchEvent將會被調用,依次類推,如果所有都不處理這個事件的話,那麼Activity將會處理這個事件。

源碼分析

由於事件總是會先傳遞到Activity,所以我們就從Activity裏面的事件分發開始分析。首先來看下Activity的dispatchTouchEvent的代碼。

//Activity.java
 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

我們看到第二個if判斷,getWindow返回的是Window的實現類PhoneWindow,所以這個判斷的意思就是,Activity的事件會交給它所屬的Window進行分發,如果它返回了TRUE,就代表整個事件就結束了,如果返回了FALSE的話就代表事件沒有人處理,那麼它終將會被Activity自己所處理,即會調用自己的onTouchEvent方法。

接下來我們就來分析一下Window是如何將事件分給ViewGroup的。Window是個抽象類,所以我們來看下的實現類PhoneWindow的dispatchTouchEvent的源碼,看看裏面是如何進行分發的。

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

我們可以發現,代碼很短,PhoneWindow直接將事件傳遞給了DecorView, 這個DecorView即是頂級view了,所以事件就已經傳到了頂級view這裏,一般情況下,頂級view是ViewGroup。所以從下面開始我們進入到ViewGroup的事件分發階段。

對於ViewGroup的事件分發過程,大概是這樣的:如果頂級的ViewGroup攔截事件即onInterceptTouchEvent返回true的話,則事件會交給ViewGroup處理,如果ViewGroup的onTouchListener被設置的話,則onTouch將會被調用,否則的話onTouchEvent將會被調用,也就是說:兩者都設置的話,onTouch將會屏蔽掉onTouchEvent,在onTouchEvent中,如果設置了onClickerListener的話,那麼onClick將會被調用。如果頂級ViewGroup不攔截的話,那麼事件將會被傳遞給它所在的點擊事件的子view,這時候子view的dispatchTouchEvent將會被調用,從這開始就進入了view的事件分發過程(可以參考《Android 事件分發機制源碼解析-view層》),這就是整個事件的分發過程。

首先,我們來看下ViewGroup的事件分發過程,進入到dispatchTouchEvent方法裏,由於這個方法比較長,所以我們對重要代碼進行分析,其他的省略,可以自行去看看。

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
			// 處理初始狀態
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
//...省略無關代碼
 // Check for interception.
            final boolean intercepted;
            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;
            }
//...省略無關代碼
}

首先這裏先判斷事件是否爲DOWN事件,如果是,則初始化,把mFirstTouchTarget置爲null。由於一個完整的事件序列是以DOWN開始,以UP結束,所以如果是DOWN事件,那麼說明是一個新的事件序列,所以需要初始化之前的狀態。這裏的mFirstTouchTarget非常重要,後面會說到當ViewGroup的子元素成功處理事件的時候,mFirstTouchTarget會指向子元素,接着我們來看下ViewGroup什麼時候會進行攔截呢?從上面那個if判斷可以知道,在兩種情況下,ViewGroup纔會攔截事件,第一種是:事件類型是ACTION_DOWN,第二種就是mFirstTouchTarget != null,第一種好理解,但是第二種什麼意思呢?我們通過後面的代碼分析可以知道這個mFirstTouchTarget的意思就是,如果ViewGroup裏面的子元素view能夠處理事件的話,那麼這個mFirstTouchTarget就會指向這個子元素view。

在上面代碼裏面有個方法,onInterceptTouchEvent(),我們進入裏面查看一下

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

默認返回false,也就是說,ViewGroup默認是不攔截事件的,如果想讓ViewGroup攔截事件的話,需要重寫該方法。接着往下看,裏面有個變量FLAG_DISALLOW_INTERCEPT,這個標誌位的作用是禁止ViewGroup攔截除了DOWN之外的事件,一般通過子View的requestDisallowInterceptTouchEvent來設置。所以,當ViewGroup要攔截事件的時候,那麼後續的事件序列都將交給它處理,而不用再調用onInterceptTouchEvent()方法了,所以該方法並不是每次事件都會調用的。

判斷是否攔截後,我們來看看ViewGroup不攔截的情況,ViewGroup不攔截的話那麼它將會把事件交給它的子view來處理。

 
						final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, 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;
                            }
//判斷觸摸點位置是否在子View的範圍內或者子View是否在播放動畫,如果均不符合則continue
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                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)) {
                                // 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();
                                //如果子View消耗掉了事件,那麼mFirstTouchTarget就會指向子View。
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

上面代碼比較清楚,首先就是遍歷ViewGroup的所有子元素,然後判斷子元素是否能夠接收到點擊事件(1、是否正在播放動畫,2、點擊事件的座標是否在子元素的區域內),這裏面有個方法private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)實際上就是調用子元素的dispatchTouchEvent方法的,方法內部有個判斷,判斷child是否爲null,如果不爲null的話,直接調用子元素的dispatchTouchEvent方法,這樣事件就交給子元素處理。完成了ViewGroup到子View的事件傳遞,當事件處理完畢,就會返回一個布爾值handled,該值表示子View是否消耗了事件。怎樣判斷一個子View是否消耗了事件呢?如果說子View的onTouchEvent()返回true,那麼就是消耗了事件。

if (child == null) {
             handled = super.dispatchTouchEvent(event);
         } else {
             handled = child.dispatchTouchEvent(event);
         }

如果子元素的dispatchTouchEvent返回了true的話,那麼這個變量mFirstTouchTarget就會被賦值

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

上面這段代碼就是給mFirstTouchTarget賦值的,可以看出來,這個mFirstTouchTarget其實是一個單鏈表結構,這個值直接影響ViewGroup是否對事件的攔截,如果爲null的話,那麼ViewGroup將會默認攔截同一序列中所有的點擊事件,如果遍歷所有的元素後發現事件沒有被處理的話,那麼只有兩種情況,一是ViewGroup沒有子元素,二是子元素處理了點擊事件,但是在dispatchTouchEvent事件中返回了false,也就是在onTouchEvent事件中返回了false,在這種情況下,ViewGroup只能自己處理事件了。即繼續往下分析代碼可以看出來:

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;
           }
       }

在上面,如果mFirstTouchTarget==null的話,就說明子view不處理該事件,那麼該事件將交給ViewGroup來處理。而如果在上面已經找到一個子View來消耗事件了,那麼這裏的mFirstTouchTarget不爲空,接着會往下執行。接着有一個if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)判斷,這裏就是區分了ACTION_DOWN事件和別的事件,因爲在在上面我們知道,如果子View消耗了ACTION_DOWN事件,那麼alreadyDispatchedToNewTouchTarget和newTouchTarget已經有值了,所以就直接置handled爲true並返回;那麼如果alreadyDispatchedToNewTouchTarget和newTouchTarget值爲null,那麼就不是ACTION_DOWN事件,即是ACTION_MOVE、ACTION_UP等別的事件的話,就會調用下面代碼,把這些事件分發給子View。

if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
           handled = true;
}

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

以上基本就是ViewGroup的事件分發過程。看到這裏估計上面的也忘了,所以我們就用流程圖來總結一下這個ViewGroup的事件分發過程。

總結

ViewGroup默認不攔截任何事件,所以事件能正常分發到子View處(如果子View符合條件的話),這時候子view的dispatchTouchEvent將會被調用,從這開始就進入了view的事件分發過程。如果沒有合適的子View或者子View不消耗ACTION_DOWN事件,那麼接着事件會交由ViewGroup處理,並且同一事件序列之後的事件不會再分發給子View了。如果ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的話,那麼最後事件會交由Activity處理。

如果頂級的ViewGroup攔截事件即onInterceptTouchEvent返回true的話,則事件會交給ViewGroup處理,如果ViewGroup的onTouchListener被設置的話,則onTouch將會被調用,否則的話onTouchEvent將會被調用,也就是說:兩者都設置的話,onTouch將會屏蔽掉onTouchEvent,在onTouchEvent中,如果設置了onClickerListener的話,那麼onClick將會被調用。這就是ViewGroup的事件分發過程。

關於作者

專注於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎大家關注,一起交流學習~

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章