從源碼角度分析Android 事件傳遞流程

自從開始負責公共控件模塊開始,我一直都想好好分析一下Android事件傳遞流程,相信網上有一大堆相關文章,但是我個人覺得作為一個專業的控件開發人員,如果隻是知道一下大概,而不知其所以然,則不算一個合格的公共控件人員,感謝我曾經一位同事,在我剛開始接觸控件的時候帶著我,很耐心的教會我控件的內在,下面我個人從源碼角度來分析Android事件傳遞流程,基於Android5.0的代碼,如果有錯誤的地方,還望指出。

一,根視圖內部消息派發過程
對於上層代碼來說,最先處理事件的是viewRootImpl,下面先看一下viewRootImpl與TouchEvent相關的內容:

protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                mKeyEventStatus = INPUT_DISPATCH_STATE_VIEW_POST_IME_STAGE;
                return processKeyEvent(q);
            } else {
                mMotionEventStatus = INPUT_DISPATCH_STATE_VIEW_POST_IME_STAGE;
                // If delivering a new non-key event, make sure the window is
                // now allowed to start updating.
                handleDispatchDoneAnimating();
                final int source = q.mEvent.getSource();
               //判斷為屏幕點擊事件或者鼠標點擊事件
                if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                    return processPointerEvent(q);
                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                    return processTrackballEvent(q);
                } else {
                    return processGenericMotionEvent(q);
                }
            }
        }

        private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;
            mAttachInfo.mUnbufferedDispatchRequested = false;
            boolean handled = mView.dispatchPointerEvent(event);
            if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
                mUnbufferedInputDispatch = true;
                if (mConsumeBatchedInputScheduled) {
                    scheduleConsumeBatchedInputImmediately();
                }
            }
            return handled ? FINISH_HANDLED : FORWARD;
        }

從上面代碼我們可以看出事件出傳遞到mView的dispatchPointerEvent方法裡面,通過追蹤mView,不難發現mView其實是PhoneWindow.DecorView。
而dispatchPointerEvent()是View裡面的方法,如果當前事件是Touch事件則會調用當前View的dispatchTouchEvent(),再看一下PhoneWindow.DecorView dispatchTouchEvent():

  @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            final Callback cb = getCallback();
          //DecorView繼承自FrameLayout,但FrameLayout沒有重寫dispatchTouchEvent,cb為空則直接執行ViewGroup.dispatchTouchEvent()
            return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)
                    : super.dispatchTouchEvent(ev);
        }
        //CallBack 是Window的一個內部Interface,作用是可以讓用戶在事件的分發,菜單的構建過程中進行攔截。其中dispatchTouchEvent正是用於攔截觸控事件,Activity實現瞭這個方法
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

從上面代碼可以看出,getWindow().superDispatchTouchEvent(ev)為false的時候,自身的onTouchEvent()函數才會被回調。這就是為什麼每次事件的都是從Activity的dispatchTouchEvent 開始,最終又會傳回Activity的onTouchEvent。另外這裡的getWindow()實際上返回的是PhoneWindow,下面再看看PhoneWindow相關的代碼:

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

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

從上面代碼可看出,事件最後傳到mDecor.superDispatchTouchEvent(),而mDecor.superDispatchTouchEvent()單單執行瞭超類的dispatchTouchEvent()也就是ViewGroup的dispatchTouchEvent(),之後事件被分發到佈局中的View中去。上述流程圖如下:
這裡寫圖片描述
二,ViewGroup內部消息派發過程
1.ViewGroup中的dispatchTouchEvent
ViewGroup中的dispatchTouchEvent()在View中的事件傳遞中承擔瞭比較重要的角色,也是精華部分。它主要承擔瞭是三個工作:1.攔截子View事件(調用自身的onInterceptTouchEvent()) 2.找出可以接受事件的子View,並把事件傳遞下去 2.把交由自身的TouchEvent()處理事件。下面貼出ViewGroup中的dispatchTouchEvent()的代碼:

@Override
    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.
               /*
                ACTION_DOWN是一些列事件的開端,因此需要做一些初始化工作。這裡主要實現瞭兩點:
                1.將mFirstTouchTarget設置為null
                2.清除FLAG_DISALLOW_INTERCEPT,讓事件可以被攔截
               */
            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);
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
               /*
               1.disallowIntercept 由FLAG_DISALLOW_INTERCEPT標志位所決定,應用可以通過調用View.requestDisallowInterceptTouchEvent(boolean disallowIntercept)設置。默認為false
               2.disallowIntercept為false時,onInterceptTouchEvent()會被調用,其返回值將決定事件是否會分發到其子view
               3.TouchTarget是ViewGroup的內部靜態類,鏈式結構,有相關的回收機制,用於記錄當前被點擊的View,以及點擊的Pointer, ViewGroup的mFirstTouchTarget總指向TouchTarget的最前一個。
               4.mFirstTouchTarget != null說明已經找到可以接受事件的View,通過onInterceptTouchEvent()同樣可以對其進行攔截
               */
                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;
            }

            // 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);
                    full_meizu6795_lwt_l1-userdebug
                    final int childrenCount = mChildrenCount;
                    /*
                      1.從這裡開始遍歷子View,遍歷能采取兩種方式:
                         ①當isChildrenDrawingOrderEnabled()返回true,根據getChildDrawingOrder(int childCount, int i)函數的順序遍歷
                            從這裡可以看出,應用可以通過重寫isChildrenDrawingOrderEnabled()以及getChildDrawingOrder(int childCount, int i)來指定子View的遍歷順序,isChildrenDrawingOrderEnabled()默認返回false
                         ②默認情況下,采用倒序遍歷子View
                      2.遍歷過程中,會調用dispatchTransformedTouchEvent(),把事件傳到當前子View的dispatchTouchEvent()中
                      3.遍歷過程中,假如對某個子view執行dispatchTransformedTouchEvent()返回true,遍歷被中斷。同時把當前的View以及當前的TouchEvent的pointerIndex記錄在mFirstTouchTarget 中
                    */
                    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--) {
                            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;
                            }

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

                            // 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,
            然後通過this.dispatchTransformedTouchEvent()——>View.dispatchTransformedTouchEvent()——>this.onTouchEvent()
            流程把事件分發到自身的onTouchEvent()
            */
            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.
                /*
                當mFirstTouchTarget !=null時可能是以下幾種情況:
                 (1).滿足條件(alreadyDispatchedToNewTouchTarget && target == newTouchTarget)也就是當前事件是Action_Down,並且通過上面的遍歷找到可以消費事件的子View,則直接返回true;
                 (2).不滿足1條件,但是事件通過需要被攔截,或者當前可消費事件的子View暫時不可見時,向該子View派發Action_Cancel事件,如上面的case5
                 (3).向當前可消費事件的子View分發事件。通常是這樣的情況:找到可消費事件的子View,當前又非Action_Down事件並且當前的ViewGroup沒有進行攔截。
                 */
                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;
                        }
                //cancelChild為true,說明child收到攔截,因此清理與該child相關的TouchTarget
                        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;
    }

其處理流程圖如下:
這裡寫圖片描述
2.ViewGroup的dispatchTransformedTouchEvent()
下面先對dispatchTransfZ喎?/kf/ware/vc/” target=”_blank” class=”keylink”>vcm1lZFRvdWNoRXZlbnSjqKOp1LTC67340NC31s72o7o8L3A+DQo8cHJlIGNsYXNzPQ==”brush:java;”>
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.
//上面分析提及到,在事件被攔截時,子View可能會接收到CANCEL事件,在這裡可以看到event被直接傳到子類的dispatchTouchEvent()沒有作坐標轉換,因此在ACTION_CANCEL事件裡 面我們不應該去event的相關坐標進行計算。
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;
// Motion事件沒有對應點,則丟棄這個Motion
if (newPointerIdBits == 0) {
return false;
}
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//這裡將當前的坐標重新轉換到child的坐標系中
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);
}
…..
}

從上述源碼可知,dispatchTransformedTouchEvent()主要有兩個作用:

1.當child為空時,把事件傳遞到超類也就是View的dispatchTouchEvent()中。
2.當child不為空時,事件傳遞到child的dispatchTouchEvent()中,但是傳遞之前首先把事件的對應的坐標重新轉化為child坐標系中的坐標,而轉換的的方法是:
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
為什麼要這樣轉換?這裡涉及兩種坐標的概念:
1.視圖坐標:視圖坐標沒有邊界,它取決於View本身的大小,而不受屏幕大小的限制。
2.佈局坐標:大小受限制,是指父視圖給子視圖分配的佈局(layout)大小,超過這個大小的區域將不能顯示到父視圖的區域中。
上面兩種坐標的關系如下圖:
這裡寫圖片描述
從上圖可知道佈局坐標轉化為視圖坐標隻需要加上mScrollY/mScrollX即可。
通常我們通過event.getX()/event.getX()獲取到的坐標是佈局坐標,而child.mLeft,child.mRight則是相對於視圖坐標而言的,因此在父視圖獲取獲取到event的坐標後必須先轉化為視圖坐標,再根據child在父視圖中的位置,進行坐標平移,即 event.offsetLocation(+mScrollX - offsetX, + mScrollY - offsetY).
3,View的dispatchTouchEvent()

 public boolean dispatchTouchEvent(MotionEvent event) {
....

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            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.dispatchTouchEvent()的主要代碼,從上面代碼可以看出,當mOnTouchListener不為空時會先執行mOnTouchListener.onTouch(this, event),而當mOnTouchListener沒有消耗事件時onTouchEvent()才會被執行,這說明在同一個View,mOnTouchListener優先級會比onTouchEvent要高。
4,View的onTouchEvent(MotionEvent event)

 public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
           //盡管當前View是disable狀態,但是隻要是CLICKABLE或者LONG_CLICKABLE仍然可以消費事件
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }
        //如果設置代理,則事件交由代理處理,如果代理消耗瞭事件則直接返回true
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
               ...

                        if (!mHasPerformedLongPress) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                              //假如對view設置瞭OnClickListener,在這裡會被回調。
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

              ...
                    break;

                case MotionEvent.ACTION_DOWN:
              ...
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        //檢查長按事件,假如設置瞭OnLongClickListener,在此有可能被回調
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                         //檢查長按事件,假如設置瞭OnLongClickListener,在此有可能被回調
                        checkForLongClick(0);
                    }
                 ...
                    break;

                case MotionEvent.ACTION_CANCEL:
                 ...
                    break;

                case MotionEvent.ACTION_MOVE:
                 ...
                    break;
            }

            return true;
        }

        return false;
    }

從上面代碼可以總結出一下幾點:
1.盡管當前View是disable狀態但仍然具有消費事件能力
2.假如設置瞭代理,事件將先交由代理處理,假如代理消費瞭事件,則直接返回true
3.在沒有設置代理的情況下,假如標記位CLICKABLE LONG_CLICKABLE不為0,則事件必然在此被消費
4.OnLongClickListener和OnClickListener的回調函數都在View的onTouchEvent()裡面執行,但執行時機有區別, OnClickListener.onClick()在ACTION_UP時被回調。OnLongClickListener.onLongClick()則在ACTION_DOWN的時候被執行(其實也不一定在這個時機被執行,因為長按需要作一定的時延檢測)。另外我們在定義View時經常需要監 聽點擊事件,這裡不推薦通過setOnClickListener()的方式實現,因為這樣相當於占用瞭應用的監聽權利,假如此時應用在不知道代碼邏輯的情況下,繼續setOnClickListener(),那麼默認的監聽器被覆蓋。做成意想不到的後果。其實替代方案可以是重寫performClick()。長按事件同理。

 

You May Also Like