1. 程式人生 > >Android View 事件分發機制 原始碼解析(ViewGroup篇)

Android View 事件分發機制 原始碼解析(ViewGroup篇)

1. 前言

android點選 事件一直以來都是很多安卓程式設計師的心病,之前通過demo模擬總結出一些經驗,但是不看原始碼的程式設計師不是好程式設計師,這段時間,系統的梳理了下整個事件傳遞的原始碼,希望可以幫助大家徹底理解andriod的點選事件傳遞過程。
通過前段時間讀原始碼,總結出一點心得:閱讀原始碼要深入淺出,抓住主流程,忽略掉部分無關程式碼,避免一行一行的扣細節,容易把自己繞進去,造成最終讀不下去。

2.原始碼解析

1)activity中傳遞解析
當我們手指點選手機螢幕的時候,首先會觸發activty的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();//此方法是activity的方法,當此activity在棧頂時,使用者對手機:觸屏點選,按home,back,menu鍵都會觸發此方法。注:下拉statubar,旋轉螢幕,鎖屏,不會觸發此方法.
} if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }

還好,程式碼不多,可以看出一共就兩個判斷,上面那個if註釋已經寫得很清晰了,由於跟點選分發其實沒有多大關係,可以忽略。主要就是if (getWindow().superDispatchTouchEvent(ev))這個判斷的程式碼理解,getWindow()返回Window物件,通過Window的原始碼可以看出,他是一個抽象類,那他的具體實現類是哪個?在Activity裡面搜尋window物件的建立部分程式碼,如下:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this);
        ......
        }

很明顯,PhoneWindow就是他的具體實現類,所以我們繼續跟蹤到PhoneWindow的superDispatchTouchEvent原始碼:

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

我們看到,這邊又有了一個mDecor,這個mDecor是什麼呢?看下注釋:

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

顯然這個view就是activity的頂級view,通過getWindow().getDecorView()可以獲取到mDecor物件,他繼承FrameLayout,並且我們經常在activity裡面通過setContentView加入的view其實就是mDecor的子view。到這裡,點選事件已經從activity傳到View當中了,接下來要分析的就是頂級View把事件如何分發到各個子view中了。
繼續瞭解mDecor.superDispatchTouchEvent(event)的具體實現:

public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }

實現很簡單,就是呼叫父類的dispatchTouchEvent方法,剛剛說了mDecor繼承FrameLayout,所以他的super肯定是ViewGroup,super.dispatchTouchEvent(event)則呼叫ViewGroup的dispatchTouchEvent方法,到此點選事件就傳遞到ViewGroup中。

View的結構圖如下所示:
這裡寫圖片描述

2)ViewGroup中傳遞解析
繼續檢視ViewGroup的dispatchTouchEvent方法的原始碼:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        // 輔助功能焦點檢視
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

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

            // 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);//mFirstTouchTarget初始化
                resetTouchState();//狀態位重置
            }

            // Check for interception.
            //是否攔截標誌
            final boolean intercepted;
            //ACTION_DOWN或者mFirstTouchTarget != null都會進行到if當中,mFirstTouchTarget物件很關鍵
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//這個標誌位通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)可以控制
                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;
            }

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // 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 the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                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 ArrayList<View> preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //倒序遍歷所有的子view
                        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);

                            // 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)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            //呼叫getTouchTarget方法去查詢當前子View是否在mFirstTouchTarget.next這條target鏈中,
                            //如果存在則返回這個target,否則返回null。     
                            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);
                            //呼叫dispatchTransformedTouchEvent()方法將Touch事件傳遞給特定的子View
                            //該方法返回false--則說明子view未消耗點選事件,從而下面的newTouchTarget = addTouchTarget(child, idBitsToAssign)方法無法呼叫,mFirstTouchTarget則為null
                            //該方法返回true--則說明子view消耗點選事件,從而進入if區域,從而mFirstTouchTarget不為null。
                            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);//給mFirstTouchTarget賦值,所有消耗點選事件的子View加入到mFirstTouchTarget連結串列中
                                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();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            // mFirstTouchTarget == null說明點選事件被攔截,或者子view沒有消耗事件
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);//呼叫父類dispatchTouchEvent,再呼叫onTouchEvent處理焦點
            } 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;
                    //根據alreadyDispatchedToNewTouchTarget 判斷,如果已經分發了,則返回true
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        //點選事件未被上面的子view消耗時,事件傳遞過程
                        //比如:有些人強制在ViewGroup中MotionEvent.ACTION_DOWN時,onInterceptTouchEvent返回false,ACTION_MOVE時,返回true,則進入到此方法
                        //清除掉mFirstTouchTarget連結串列中所有target,及mFirstTouchTarget==null;這樣下一次直接跑入到
                        //if (mFirstTouchTarget == null)內容區域內,則點選事件傳遞到ViewGroup的onTouchEvent處理焦點
                        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;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

程式碼比較多,沒有事,我們關注重點,忽略掉一些安全,控制的程式碼,分析上面的程式碼。

1)前面都是一些跟主流程無關的程式碼,我們直接看第19-25行
當點選事件是MotionEvent.ACTION_DOWN時,及手指剛剛觸碰螢幕第一個觸發的事件,裡面呼叫了兩個方法,cancelAndClearTouchTargets(ev)和resetTouchState();,下面分別來看下這兩個方法的實現:

private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
    /**
     * Clears all touch targets.
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }

可以看到cancelAndClearTouchTargets方法裡面進行了mFirstTouchTarget的初始化工作,及把mFirstTouchTarget連結串列清空。

/**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

通過註釋也可以看到,resetTouchState方法是重置所有觸控狀態以準備新的迴圈,其中FLAG_DISALLOW_INTERCEPT標誌位其實就是requestDisallowInterceptTouchEvent(boolean disallowIntercept)設定的標誌位用來阻止父類攔截點選事件,所以在子view中呼叫requestDisallowInterceptTouchEvent方法的時候,對於MotionEvent.ACTION_DOWN事件是無效的,因為MotionEvent.ACTION_DOWN事件會清楚所有觸控狀態。

2)繼續看29-44行,判斷是否父類是否攔截點選事件
在29行可以看到,通過intercepted布林值來標記是否攔截,接下來通過if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null)判斷,來確定是否進入到onInterceptTouchEvent方法中,一般兩種情況下肯定會呼叫onInterceptTouchEvent方法:1.點選事件為MotionEvent.ACTION_DOWN。2.mFirstTouchTarget !=null及子類處理點了擊事件,因為mFirstTouchTarget 是在dispatchTransformedTouchEvent返回true的時候賦值的(後面程式碼會介紹,現在先記住)。反過來,點選事件MotionEvent.ACTION_DOWN被ViewGroup攔截,及mFirstTouchTarget =null,則後面move,up事件將不在呼叫onInterceptTouchEvent方法,因為mFirstTouchTarget =null,直接跳過if語句執行下面的else語句。

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//這個標誌位通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)可以控制

通過位運算,判斷是否進入onInterceptTouchEvent()方法,可見,我們一般控制滑動衝突時候,在子View中呼叫的requestDisallowInterceptTouchEvent(boolean disallowIntercept),就是在這裡發揮的作用。

3)繼續看61-169行,事件傳遞到子View
當沒有被取消並且沒有被攔截點選事件時,進入到if語句內,通過一個for迴圈i從childrenCount - 1開始遍歷到0,倒序遍歷所有的子view,第118行, newTouchTarget = getTouchTarget(child);呼叫getTouchTarget方法去查詢當前子View是否在mFirstTouchTarget.next這條target鏈中,如果存在則返回這個target,並且跳出迴圈,並且此時alreadyDispatchedToNewTouchTarget =false,則事件的分發在186行裡面執行,否則返回null。

130行到148行註釋寫得比較多,這邊就不在說明了。

4)繼續看172-210行,事件傳遞到子View
if (mFirstTouchTarget == null)如果條件成立,則說明mFirstTouchTarget 為空,沒有被賦值,因為mFirstTouchTarget 是在第146行被賦值,根據分析不執行這行程式碼的邏輯如下:dispatchTransformedTouchEvent返回false或者點選事件被攔截。總之,就是事件沒有被子View處理。這個時候,就入到if語句中,發現就一個方法,如下:

// Dispatch to touch targets.
            // mFirstTouchTarget == null說明點選事件被攔截,或者子view沒有消耗事件
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);//呼叫父類dispatchTouchEvent,再呼叫onTouchEvent處理焦點
            }

繼續看下dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);方法的原始碼:

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

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        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);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

可以看到,由於child=null,所以呼叫了handled = super.dispatchTouchEvent(transformedEvent);,及父類的dispatchTouchEvent方法,ViewGroup的父類也就是View,關於View的相關事件,在後面一篇部落格中講解,這裡只要知道,呼叫了父類的dispatchTouchEvent。
如果mFirstTouchTarget !=null時,則執行179-210行程式碼,這裡面的邏輯比較複雜,註釋也比較多,主要做兩件事:1.未下子View分發的事件繼續向下分發(193行)2.清除掉mFirstTouchTarget連結串列中所有target(197行)

剛剛提到在第35行,會呼叫intercepted = onInterceptTouchEvent(ev);//呼叫攔截方法,我們看下這個方法的具體原始碼:

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

預設返回false,如果重寫了ViewGroup的onInterceptTouchEvent方法,返回false就不阻止事件繼續傳遞派發,否則阻止傳遞。

3.總結

1.android的事件傳遞是先傳遞到父類,再到子類的。
2.ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,但是子View可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)控制父類的攔截事件是否呼叫。
3.子View消耗掉點選事件後,父類onTouchEvent方法不會呼叫,子View不消耗點選事件,會傳到父類onTouchEvent方法,父類onTouchEvent方法返回false,則最終傳遞到activity的onTouchEvent方法。
4.ViewGroup一旦呼叫onInterceptTouchEvent方法攔截點選事件後,本次點選序列事件則都交於該ViewGroup處理,並且onInterceptTouchEvent將不再執行。原因見【2)繼續看29-42行,判斷是否父類是否攔截點選事件】中的解釋。
5.當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發下一個action.也就是說,子view 未消耗點選事件,及dispatchTouchEvent返回false,這樣mFirstTouchTarget =null,後面只能執行172行程式碼,則後續action直接由ViewGroup執行,不傳遞給子View

如有錯誤歡迎指出來,一起學習。

交流討論群
群號:469890293