View事件分發機制

View的事件分發機制

View的事件分發機制簡單來說就是將用戶與手機屏幕的交互事件交由正確的控件進行處理,從而可以對用戶事件作出相應,完成交互。這里主要涉及到兩個對象,一個是View, 一個是事件,即MotionEvent。

1. 事件MotionEvent

事件包括action code和axis value兩部分,前者指定該事件的類型,后者指定事件發生的位置,此外每個MotionEvent中還包含一個Pointer, 用于多點觸摸,每個Pointer表示一個觸摸點,每個Pointer都會有對應的index和id

事件類型 action code

事件類型有很多種,常見的包括以下幾種:

    public static final int ACTION_DOWN             = 0;
 
    public static final int ACTION_UP               = 1;
    
    public static final int ACTION_MOVE             = 2;
    
    public static final int ACTION_CANCEL           = 3;
    
    public static final int ACTION_OUTSIDE          = 4;

    public static final int ACTION_POINTER_DOWN     = 5;
    
    public static final int ACTION_POINTER_UP       = 6;

其中前三個類型最為常見,CANCEL則表示取消一個事件流,即當一個View接受到ACTION_CANCEL事件以后就不再處理該事件流的后續事件。最后兩個事件類型則是在多點觸摸的情況下,當已經有觸摸點按下屏幕,第二個觸摸點按下屏幕時,此時系統會發送一個ACTION_POINTER_DOWN類型的事件,同理,在有其他觸摸點并未離開觸摸屏,一個觸摸點離開觸摸點,系統發送一個ACTION_POINTER_UP事件。

事件位置 axis value

MotionEvent提供了四個方法獲取事件的位置,

event.getX(); //觸摸點相對于View左上角為原點坐標系的X坐標
event.getY(); //觸摸點相對于View左上角為原點坐標系的Y坐標
event.getRawX(); //觸摸點相對于屏幕左上角為原點坐標系的X坐標
event.getRawY(); //觸摸點相對于屏幕左上角為原點坐標系的Y坐標

事件流

在事件分發的過程中,經常會用到事件流的概念。用戶的一次操作都會觸發多個事件,它們組成事件流,通常為ACTION_DOWN,ACTION_UP或者ACTION_DOWN,ACTION_MOVE,......, ACTION_UP兩種類型,對于多點觸摸操作中間還會包含ACTION_POINTER_DOWN和ACTION_POINTER_UP。在事件分發的過程中,一個事件流通常是交由一個控件處理,因此ACTION_DOWN的處理邏輯通常會比較特殊,它代表著一個事件流的開始,此時會有清除之前的狀態等操作。一個事件流通常會以ACTION_UP或者ACTION_CANCEL結束。

多點觸摸操作

對于多點觸摸操作,MotionEvent中使用Pointer表示,每個Pointer都有一個Id和Index, 在一個事件流中,一個Pointer的id是保持不變的,可以通過id獲取Pointer的index,進而可以執行pointer的其他操作。(在有些資料中看到說一個MotionEvent中包括一系列的Pointer,表示多點觸摸,在后面的分析中感覺行不通,這里我的理解是每個MotionEvent只有一個Pointer,它有一個index和一個id, 比如第一個點觸摸時,系統發出一個ACTION_DOWN事件,對應有一個Pointer,第二個點觸摸時系統發出ACTION_POINTER_DOWN事件,此事件也對應一個Pointer,它有一個index和一個id, 然后兩個手指滑動過程中都會觸發ACTION_MOVE事件,而其Pointer對應的id保持不變,而index由可能因為滑動順序發生改變,通過id可以查找該pointer對應的index,進而執行其他的關于這個pointer對應MotionEvent的操作, 僅僅是個人理解,有待查證)
多點觸摸操作的常用方法:

int getPointerCount() //操作事件所包含的點的個數
int findPointerIndex(int pointerId) //根據pointerId找到pointer在MotionEvent中的index
int getPointerId(int pointerIndex) //根據MotionEvent中的index返回pointer的唯一標識
float getX(int pointerIndex) //返回操作點的x坐標
float getY(int pointerIndex) //返回操作點的y坐標
final int getActionMasked () //獲取action code
final int getActionIndex()// 獲取 pointer的index

MotionEvent的方法getAction可以獲取action code, 對于單點的操作,即ACTION_DOWN,ACTION_UP等事件,getAction和getActionMasked二者是相同的,而對于多點操作,getAction可以獲取Pointer的index(8~15位)和action code(0~7位),而getActionMasked則是獲取action code

public static final int ACTION_MASK             = 0xff;
public static final int ACTION_POINTER_INDEX_MASK  = 0xff00;
public final int getAction() {
        return nativeGetAction(mNativePtr);
    }

public final int getActionMasked() {
        return nativeGetAction(mNativePtr) & ACTION_MASK;
    }

public final int getActionIndex() {
        return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
                >> ACTION_POINTER_INDEX_SHIFT;
    }

從代碼中可以清晰地看出三個方法作用。

2. View的事件分發

在系統產生一個事件流以后,事件通常交由某個合適的View處理,而找到合適的View則是事件分發機制的關鍵問題。而View有一個特殊的子類即ViewGroup,它主要負責布局它的子View, 因此展示內容的View和容器View(ViewGroup)對事件分發處理的邏輯有所不同,這里分兩部分講述。該小節首先說明內容View的事件分發機制。
內容View的事件分發處理邏輯較為簡單,只有一個問題,即我是否要處理并消費該事件,處理邏輯在內部實現,而對該事件是否完全處理,即是否消費該事件,需要通過返回值反饋給父控件,讓父控件針對處理結果做下一步處理。因此View的事件分發機制,只需要分析View#dispatchTouchEvent(MotionEvent event)的方法即可,該方法返回一個boolean值,true表示消費該事件,false則不消費。

事件分發的的方法View#dispatchTouchEvent(MotionEvent event)

 /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        
        ......

        boolean result = false;

        ......

        final int actionMasked = event.getActionMasked();
        ......

        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的事件分發邏輯很清晰,通過安全性檢查以后,首先是調用OnTouchListener的onTouch()方法(如果有),onTouch方法如果返回true(表示已經處理該事件),則事件分發結束,事件處理完畢,如果沒有OnTouchListener或者onTouch()方法返回false, 則調用View#onTouchEvent()方法,并將該方法的返回值作為dispatchTouchEvent()方法的返回值,表示該View是否消費該事件。

下面是onTouchEvent(MotionEvent event)負責處理事件,自定義View可以通過覆寫該方法自定義事件處理邏輯,下面為View的onTouchEvent方法的簡略化源碼
View#onTouch(MotionEvent event)

/**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == 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.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        ......
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                ......
            }

            return true;
        }

        return false;
    }

這里將代碼簡化,只看最外層的邏輯,根據View的enable和disable兩種狀態分為兩種情況,首先disable狀態下,View為clickable時,該view消費該事件但并未做任何處理(這一點符合邏輯,即該view為clickable,則可以處理單擊等事件,只不過現在處于disable狀態,可以消費,但不做邏輯處理)。其次在enable狀態下,如果View為clickable時,該view消費該事件(即返回true)并處理該事件,根據事件類型不同處理邏輯不同(switch內的分支代碼在下一段)。最后就是view的所有clickable均為false時則該view不處理該事件,也不消費該事件,交由父控件處理。

下面為switch的分支語句,是不同事件的處理邏輯

case MotionEvent.ACTION_UP:
    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
        // take focus if we don't have it already and we should in
        // touch mode.
        boolean focusTaken = false;
        
        ......

        if (prepressed) {
            // The button is being released before we actually
            // showed it as pressed.  Make it show the pressed
            // state now (before scheduling the click) to ensure
            // the user sees it.
            setPressed(true, x, y);
       }

        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
            // 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();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
            }
        }

        ......

        removeTapCallback();
    }
    mIgnoreNextUpEvent = false;
    break;

case MotionEvent.ACTION_DOWN:
    mHasPerformedLongPress = false;

    ......

    // Walk up the hierarchy to determine if we're inside a scrolling container.
    boolean isInScrollingContainer = isInScrollingContainer();

    // For views inside a scrolling container, delay the pressed feedback for
    // a short period in case this is a scroll.
    if (isInScrollingContainer) {
        mPrivateFlags |= PFLAG_PREPRESSED;
        if (mPendingCheckForTap == null) {
            mPendingCheckForTap = new CheckForTap();
        }
        mPendingCheckForTap.x = event.getX();
        mPendingCheckForTap.y = event.getY();
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    } else {
        // Not inside a scrolling container, so show the feedback right away
        setPressed(true, x, y);
        checkForLongClick(0, x, y);
    }
    break;

case MotionEvent.ACTION_CANCEL:
    setPressed(false);
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    break;

case MotionEvent.ACTION_MOVE:
    ......

    // Be lenient about moving outside of buttons
    if (!pointInView(x, y, mTouchSlop)) {
        // Outside button
        removeTapCallback();
        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
            // Remove any future long press/tap checks
            removeLongPressCallback();

            setPressed(false);
        }
    }
    break;

首先看ACTION_DOWN, 根據View是否處于可以滾動的容器中分為兩種情況,如果在可滑動容器中,則設置PFLAG_PREPRESSED,并啟動單擊檢測(從代碼中可以看出CheckForTap是一個Runnable類型,后面再做分析), 如果不在可滑動容器中則直接將設置FLAG_PRESSED標志位,并啟動長按檢測,這是因為在可滑動容器中需要區分是單擊還是滑動事件,所以啟動了單擊檢測。其實在CheckForTap中所做的工作也是設置FLAG_PRESSED標志位,并啟動長按檢測,源碼如下:

private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;   //情況PFLAG_PREPRESSED
            setPressed(true, x, y);
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }

這里可以看出其操作基本相同,繼續看checkForLongClick()方法的源碼

private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }

這里是做長按檢測,其邏輯執行與在可滑動容器中做單擊檢測比較相似,同樣CheckForLongPress也是一個Runnable, 其源碼:

private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;

        @Override
        public void run() {
            if (isPressed() && ......) {
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }

        .......
    }

(這里在條件判斷中加入省略號是我還沒有理解的條件,暫時不影響分析),可以看出在長按檢測中是在一定時間之后如果是Pressed狀態(因為此時單擊檢測時間已過,如果此時沒有ACTION_UP或者ACTION_CANCEL事件,則setPressed方法一定被調用)則執行長按邏輯,并置位mHasPerformedLongPress。所以單擊檢測和長按檢測都交由View的attachInfo中有一個mHandler來處理,在指定時間之后設置標志位并執行相應邏輯。
接著是ACTION_MOVE和ACTION_CANCEL,這兩個都較為簡單,CANCEL則是表示該view不再處理該事件流的后續事件,清空所有標志位,并移除單擊和長按檢測。MOVE則考慮手指劃出View的范圍時,在合適條件下移除單擊和長按檢測以及清除標志位。
最后再看ACTION_UP就比較容易理解,這個是一個事件流的結束,prepressed為true說明單擊檢測還沒結束,pressed為true則說明單擊檢測已經結束,此時直接看if (!mHasPerformedLongPress && !mIgnoreNextUpEvent)這里的條件判斷,如果沒有執行長按邏輯或者長按事件處理邏輯返回了false, mHasPerformedLongPress則為false, 如果在ACTION_UP事件之前沒有ACTION_CANCEL事件,則mIgnoreNextUpEvent則為false, 那么此時發生ACTION_UP事件則是一次單擊事件,此時觸發單擊事件處理邏輯,單擊處理邏輯較為簡單,與PerformeLongClick一樣,也是通過post交由mHandler調用performClick方法,在該方法中會調用設置的ClickListener的onClick()方法,也是我們最為熟悉的方法。(這里也就解釋了,如果沒有設置長按監聽器,手指長按在離開時也可以觸發單擊事件,而且即使設置了長按監聽器,如果返回false,在手指離開時也會觸發單擊事件,而長按事件是ACTION_DOWN加一定時間觸發的,而單擊則一定要等到ACTION_UP時才能觸發)。最后在ACTION_UP中關于focusTaken還不明白什么意思,后續會繼續學習。

View事件分發的總結

至此,View的事件分發就結束了,主要流程就是調用dispatchTouchEvent()方法,然后是OnTouchListener的onTouch()方法,最后是調用View的OnTouchEvent()方法,是否調用則根據事件處理結果的返回值決定。在onTouchEvent()方法中,根據enable和各種clickable的狀態分為四種情況,其中disable且clickable(這里的clickable包括多種,有一個為true就可以)時沒有處理邏輯但是該view會消費該事件,clickable為false的時候,不管是否enable,都不處理且不消費該事件,只有在enable且clickable狀態下才執行處理邏輯,此時根據不同的事件類型有不同邏輯,重點是ACTION_DOWN和ACTION_UP,前者開始一個事件流,主要是進行單擊檢測和長按檢測并處罰長按事件處理邏輯(如果有),后者則是根據一個事件流中之前所設置的標志位判斷是否發生了單擊事件并執行單擊邏輯。而其他兩種事件類型處理較為簡單,只是設置一些標志位。

View的事件分發是從調用View#dispatchTouchEvent(MotionEvent event)開始,那么誰來調用該方法,很自然應該是該view所屬的ViewGroup調用,所以下一部分分析ViewGroup的事件分發,看ViewGroup如何調用View的dispatchTouchEvent

3. ViewGroup事件分發

在MotionEvent部分提及過事件流的概念,通常事件處理都是針對事件流,而一個事件流一般都是交由一個View處理,而當一個事件交由一個ViewGroup時,ViewGroup面臨三個問題:

    1. ViewGroup是否攔截該事件
    1. 如果不攔截該事件,應該將該事件交由哪個子View處理
    1. 如果攔截如何攔截一個事件流,如果不攔截,如何將一個事件流的后續事件交由同一個View處理

由于ViewGroup是繼承View,所以ViewGroup的事件分發的分析也是從dispatchTouchEvent()方法開始:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        ......

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
/*****************************************************判斷********************************************************/
            //1. 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);
                resetTouchState();
            }

            //2. 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;
            }

            ......

            //3. 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 (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 = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        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);

                            .......

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

                            ......
                        }
                        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.
            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;
                }
            }
/**************************************************清理***********************************************/
            // 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);
            }
        }

        ......

        return handled;
    }   

這里的代碼將關于accessibilityfocus部分現在不理解,暫時先略去,只分析剩余部分(可能會有錯誤之處,后續繼續學習時再作改正),另外由于該方法的代碼較長,添加了幾行分割線。下面分步驟分析:
在判斷部分主要有個三個if語句塊
第一步是當事件為ACTION_DOWN時,清空所有的狀態。
第二步是檢查是否攔截該事件,ACTION_DOWN或者一個事件流的后續事件(且該事件流的ACTION_DOWN已經交由某個子View處理,此時mFirstTouchTargent不為空),此時進入if語句塊判斷是否攔截。而如果某個ACTION_DOWN沒能夠交由ViewGroup的某個View處理,其后續事件都交由該ViewGroup處理,此時mFirstTouchTarget為空,不再判斷直接設置為攔截,其他情況下均需要做判斷。對于進入if語句塊還需要判斷disallowIntercept, 這個可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,從而禁止執行是否需要攔截的判斷,而當disallowIntercept為false時,是否攔截事件則是由onInterceptTouchEvent(ev)方法決定,自定義ViewGroup可以通過覆寫該方法定義攔截某些事件。
第三步檢查是不是要取消這一個事件流
在查找部分主要是查找可以處理該事件的子View,這里涉及到一個概念就是TouchTarget,用來描述一個觸摸目標View以及觸摸到該View上的pointers。這里不再貼出源碼,重點關注它的三個public Field,

// The touched child view.
public View child;

// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;

// The next target in the target list.
public TouchTarget next;

child表示它代表的View,pointerIdBits表示觸摸點,整型中置一的位代表著該View捕獲了該pointer的事件,next則用于構建鏈表。在ViewGroup中,mFirstTouchTarg就是一個這樣類型的域,它是一個鏈表,在當ACTION_DOWN事件發生時用于記錄處理這個事件流的View以及Pointer,也就是通過TouchTarget表示。

另外有一個方法在多個地方被調用,這里首先簡要介紹,即dispatchTransformedTouchEvent,這個方法也挺長,不過簡化一些細節,我們暫時只需要明白以下邏輯:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
}

即根據child不同做不同的事件分發邏輯,具體細節可以查看具體源碼。

下面開始分析子View查找部分,根據之前多次提到事件流的概念,所以ViewGroup只有在ACTION_DOWN事件或者ACTION_POINTER_DOWN事件發生時才查找相應的子View, idBitsToAssign表示該事件的Pointer對應的id(在該整型的某一位置一),首先清除pointer對應的TouchTarget,然后遍歷所有的子View,調用dispatchTransformedTouchEvent,該方法的返回值表示該子view是否處理了該事件,如果處理了則進入if語句塊,記錄一些信息,并將該View以及對應Pointer組合成TouchTarget添加到mFirstTouchTarget的鏈表中,此時newTouchTarget==mFirstTouchTarget, 并且alreadyDispatchedToNewTouchTarget置位true,跳出循環。最后如果mFirstTouchTarget不為空,且沒有找到合適子View,那么叫交由mFirstTouchTarget鏈表的最后一個TouchTarget來處理該事件。

接下來是事件分發部分,查找部分已經將TouchTarget添加到mFirstTouchTarget鏈表中,接下來就是典型的鏈表操作,遍歷鏈表并調用dispatchTransformedTouchEvent,根據View是否為空決定是ViewGroup處理還是某個查找到的View處理。

最后是清除工作,即ACTION_UP或者ACTION_POINTER_UP事件時以及取消事件時,需要清理一些狀態。

總結

ViewGroup的事件分發只在ACTION_DOWN或者ACTION_POINTER_DOWN時才做攔截或查找判斷,保證攔截一個事件流或者將一個事件流交由同一個子View處理,ViewGroup通過onIntercepte()方法決定是否攔截一個事件流,通過調用dispatchTransformedTouchEvent,根據這個方法的返回值查找目標子View,通過mFirstTouchTarget這樣一個TouchTarget的鏈表記錄一個事件流的目標View以及Pointer,并且通過變量這個鏈表以及調用dispatchTransformedTouchEvent完成事件分發。
這一部分省略了所有關于TargetAccessibilityFocus的源碼,對這部分目前還不了解,所以分析過程可能存在錯誤,另外由于多點觸摸,涉及到TouchTarget以及Pointer的概念,這部分理解還不夠,所以分析有所欠缺,這方面在網上沒有找到多少資料,有比較了解的歡迎賜教!

至此分析完了ViewGroup的事件分發,同樣也是dispatchTouchEvent方法,同樣的問題,ViewGroup的該方法由誰調用,下一步則需要分析Activity的事件分發。

4. Activity的事件分發。

Activity的事件分發同樣從Activity#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();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

首先是在ACTION_DOWN事件時,調用onUserInteraction(),在該回調方法中可以處理事件。接著調用Window#superDispatchTouchEvent().
我們都知道每個Activity都有一個Window對象,Window是一個抽象類,它有唯一的一個實現類即PhoneWindow。它的superDispatchTouchEvent方法如下:

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

mDecor就是ContentView的父View,所以這樣就將事件傳遞到了頂層的ViewGroup,所以事件就可以逐層傳遞下去。

如果事件傳遞結束,頂層View的dispatchTouchEvent()方法返回了false, 則事件交由Activity的onTouchEvent處理。

所以Activity的事件分發較為簡單,至此就分析完了事件分發的過程,至于Activity的dispatchTouchEvent()方法由誰調用,還在進一步學習中。。。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容