View的事件分發(二)源碼分析(dispatchTouchEvent)

目錄

如下源碼基於版本27,併爲了方便閱讀有所刪減。

Activity ---> ViewGroup流程分析

View的事件分發(一)分發流程文章得知,從用戶點擊屏幕到屏幕中的控件響應操做的大體流程是 Activity-> ViewGroup-> View。那麼 Activity 是怎麼把事件傳遞給咱們在 xml 中寫的根視圖呢?就是說 Activity-> ViewGroup 具體是一個怎樣的流程?java

當一個點擊操做發生時,最早傳遞給 當前 Activity,由 Activity 的 dispatchTouchEvent 方法分發。咱們看下 Activity 的 dispatchTouchEvent 的具體源碼:android

//Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //調用 window 的 superDispatchTouchEvent 方法進行事件的分發
    //若是返回 true,就表明事件被消費, Activity 的 dispatchTouchEvent 就直接返回 true,再也不往下執行。
    //若是返回 false,就表明事件沒有被消費,就調用 Activity 的 onTouchEvent 方法
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
複製代碼

繼續查看 window 的 superDispatchTouchEvent 源碼,window 是一個抽象類,有一個惟一的實現類 PhoneWindow(這一點能夠在 window 的類註釋中查看到),那麼咱們就找 PhoneWindow 的 superDispatchTouchEvent 方法。ide

//PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    //mDecor 是 DecorView
    //這裏調用 DecorView 的 superDispatchTouchEvent 方法
    return mDecor.superDispatchTouchEvent(event);
}
複製代碼

繼續查看 DecorView 的 superDispatchTouchEvent 方法源碼分析

//DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    //調用父類的 dispatchTouchEvent 方法進行分發.
    return super.dispatchTouchEvent(event);
}
複製代碼

咱們知道能夠經過 ((ViewGroup) getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0); 來找到 Activity 所設置的 View,PhoneWindow 中調用的 mDecor 就是 getWindow().getDecorView() 獲取的 DecorView,咱們經過 Activity 所設置的 View 就是它的子 View。DecorView 是繼承的 FrameLayout,固然 FrameLayout 是沒有重寫 dispatchTouchEvent 方法的,因此最終就是經過 FrameLayout 的父類 ViewGroup 的 dispatchTouchEvent 方法來將事件分發給咱們開發的頁面。post

總結

完整的事件分發流程:Activity ---> PhoneWindow ---> DecorView ---> ViewGroup ---> ··· --->View學習

ViewGroup ---> View流程分析

在分析 ViewGroup 的 dispatchTouchEvent 方法以前,先分析一個關鍵的類:TouchTargetui

TouchTarget 是 ViewGroup 中的一個靜態內部類。記錄了響應 Touch 事件 View 的鏈表。在 ViewGroup 中,有一個成員變量 private TouchTarget mFirstTouchTarget,這個變量在 dispatchTouchEvent 分發 Touch 事件中起了重要做用。this

private static final class TouchTarget {
    //鏈表的最大長度
    private static final int MAX_RECYCLED = 32;
    //用於控制同步的鎖鎖
    private static final Object sRecycleLock = new Object[0];
    //內部用於複用的表頭
    private static TouchTarget sRecycleBin;
    //可複用的實例鏈表的長度
    private static int sRecycledCount;

    public static final int ALL_POINTER_IDS = -1; // all ones

    // The touched child view.
    //記錄響應 Touch 事件的 View
    public View child;

    // The combined bit mask of pointer ids for all pointers captured by the target.
    // 對目標捕獲的全部指針的指針id的組合位掩碼,和多點觸控有關。
    public int pointerIdBits;

    // The next target in the target list.
    //鏈表中的下一個記錄觸摸 View 的 TouchTarget 對象
    public TouchTarget next;

    private TouchTarget() {
    }

    public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
        if (child == null) {
            throw new IllegalArgumentException("child must be non-null");
        }

        final TouchTarget target;
        synchronized (sRecycleLock) {
            if (sRecycleBin == null) {
                //應用中,首次被觸摸的時候 sRecycleBin 是爲 null 的。建立一個新對象
                target = new TouchTarget();
            } else {
                //將鏈表的表首的對象賦值爲 target,sRecycleBin 指向下一個對象等待下一次複用。
                target = sRecycleBin;
                //表頭 sRecycleBin 往下移一位,指向 next 對象。
                sRecycleBin = target.next;
                //可複用的實例鏈表的長度減一
                sRecycledCount--;
                //將 target 的 next 賦值爲 null
                target.next = null;
            }
        }
        //記錄響應 Touch 事件的 View。
        target.child = child;
        target.pointerIdBits = pointerIdBits;
        return target;
    }
    //回收 TouchTarget 對象,後面複用
    public void recycle() {
        if (child == null) {
            throw new IllegalStateException("already recycled once");
        }

        synchronized (sRecycleLock) {
            //複用鏈表的長度最大不超過 MAX_RECYCLED
            if (sRecycledCount < MAX_RECYCLED) {
                //在鏈表的最前面加一個可複用的對象做爲表頭。sRecycleBin 指向這個對象。
                next = sRecycleBin;
                sRecycleBin = this;
                sRecycledCount += 1;
            } else {
                next = null;
            }
            child = null;
        }
    }
}
複製代碼

mFirstTouchTarget 是經過 TouchTarget.obtain 方法賦值的(這個後面會說),應用首次被觸摸的時候是經過 new 一個對象來賦值的,後面觸摸的時候,是複用了鏈表首部對象。最後經過 recycle() 方法用來回收本身,用來之後複用。spa

首先看 ViewGroup 的 dispatchTouchEvent 方法:

分析以前,咱們先肯定兩個疑問:.net

  • 問題一:ViewGroup 是怎麼分發給子 View 的?
  • 問題二:關於 requestDisallowInterceptTouchEvent
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) {
            //取消並清除觸摸目標,裏面將 mFirstTouchTarget 置爲 null。
            // mFirstTouchTarget 是記錄響應觸摸事件 View 的鏈表。
            cancelAndClearTouchTargets(ev);
            //若是是 DOWN 事件,就重置觸摸狀態。
            //將 mGroupFlags 重置 FLAG_DISALLOW_INTERCEPT 標誌位,這個是和 requestDisallowInterceptTouchEvent 相關的,後面再進行解釋。
            resetTouchState();
            //這兩個方法內部都調用了 clearTouchTargets() 方法,回收 mFirstTouchTarget 指向的鏈表上的對象,將 mFirstTouchTarget 置爲null。
        }

①------------        
        //檢查是否攔截
        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //是否不容許攔截
            //(requestDisallowInterceptTouchEvent 就是經過給 mGroupFlags 添加FLAG_DISALLOW_INTERCEPT 標誌位來告訴父控件不要攔截的)
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //若是容許攔截,那就調用 onInterceptTouchEvent 方法查詢是否攔截,記錄返回值
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                //若是不容許攔截,直接將 intercepted 賦值爲 false
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            //若是不是DOWN 事件,而且 mFirstTouchTarget 爲 null,這個 ViewGroup 就繼續攔截事件
            //若是可以執行到這裏,說明 DOWN 後面的事件傳遞到了這裏,
            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.
        //是否本身被攔截,或者傳遞的是 CANCEL 事件
        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;
            //若是是 DOWN 事件。
            //(ACTION_POINTER_DOWN 是一個手指按下的後,另外一個手指也按下傳遞的 DOWN 事件)
            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--) {
②------------            
                    //遍歷子 View 
                    //注意 這裏是倒序遍歷的,先看最上面的 View 是否響應操做事件,其實這也是符合咱們的直覺的.
                        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 是否在當前點擊區域,若是不在就跳過此次循環
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        //判斷這個 View 是否已經在相應事件的鏈表中。若是在,就跳出循環。
                        //例如:一個手指按在一個按鈕上後,另外一個手指也按在這個按鈕上,這個時候不響應事件。
                        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 方法,
                        //這個方法裏面調用子 View 的 dispatchTouchEvent。(後面說明)
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            //若是 dispatchTransformedTouchEvent 返回了 true,就表明這個 child 消費了這個事件
                            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();
⑤------------                 
                            //調用 addTouchTarget 方法,爲 mFirstTouchTarget 進行賦值
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                //沒有找到響應控件的 View,可是 mFirstTouchTarget 不爲null(以前存在響應事件的 View)
                //例如:一個手指按在一個按鈕上,另個一手指按在其餘控件上(沒有消費事件)
                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;
                    //遍歷鏈表,將 newTouchTarget 指向鏈表的尾部的對象。
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
⑥------------
            //若是 mFirstTouchTarget 爲null,沒有子 View 消費事件
            //就經過 dispatchTransformedTouchEvent 調用自身的 onTouchEvent.
            // 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) {
                    //DOWN 事件傳遞過來的時候,是走到這裏的。這一點在前面的流程中有體現。
                    handled = true;
                } else {
                    // target.child 是否被設置了 CANCEL 事件,或者傳遞過來的是不是 CANCEL 事件。
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
⑦------------       //對於非 DOWN 事件,也是在這裏調用 dispatchTransformedTouchEvent 方法進行分發。 
                    /* *若是父控件攔截事件 intercepted 爲true,則 cancelChild 爲 true。 *在調用 dispatchTransformedTouchEvent 進行分發事件的時候,傳遞了一個 CANCEL 事件給子 View(後面說明) */
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
⑧------------                        
                            //若是表頭被回收, mFirstTouchTarget 指向下一個對象。
                            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) {
            // UP 事件的時候,重置觸摸狀態
            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;
}
--------------------------------------------------------------------- 
//ViewGroup 類
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    //建立一個 TouchTarget 對象
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    //將 mFirstTouchTarget 賦值給 target 的 next。第一次賦值的話,mFirstTouchTarget確定是 null了。
    target.next = mFirstTouchTarget ;
    //mFirstTouchTarget 賦值爲 target
    mFirstTouchTarget = target;
    return target;
}
---------------------------------------------------------------------
//ViewGroup 類
private TouchTarget getTouchTarget(@NonNull View child) {
    for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
        //遍歷鏈表,找到該 child 所在的 TouchTarget 對象。
        if (target.child == child) {
            return target;
        }
    }
    return null;
}
---------------------------------------------------------------------
private void clearTouchTargets() {
    TouchTarget target = mFirstTouchTarget;
    //遍歷鏈表,依次調用鏈表上TouchTarget對象的 recycle 方法。
    if (target != null) {
        do {
            TouchTarget next = target.next;
            target.recycle();
            target = next;
        } while (target != null);
        //將 mFirstTouchTarget 置爲null。
        mFirstTouchTarget = null;
    }
}
複製代碼

dispatchTransformedTouchEvent 方法:

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

    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        //cancel爲 true,或者傳遞的是一個 CANCEL 事件,設置 cancel 事件
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            //若是 child 爲空,就調用父類(View)的 dispatchTouchEvent 方法來分發
            //(裏面調用了 onTouchEvent 方法,後面進行分析)
            handled = super.dispatchTouchEvent(event);
        } else {
            //child 不爲 null,就調用子類的分發
            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 (newPointerIdBits == 0) {
        return false;
    }

    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);
                //將事件分發給子 View
                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;
}
複製代碼
  • 問題一:ViewGroup 是怎麼分發給子 View 的?

    DOWN 事件: 經過上述源碼分析,ViewGroup 在進行事件分發的時候,先在 DOWN 事件的時候重置一些狀態,而後在 代碼① 處進行詢問是否進行攔截,由於首次傳入的事件是 ACTION_DOWN 事件,而且 mFirstTouchTarget 被重置爲 null,因此就調用 onInterceptTouchEvent 詢問是否攔截。若是 不攔截 就執行到 代碼② ,進行遍歷子 View,尋找在點擊區域的子類(代碼③) ,而後經過(代碼④) dispatchTransformedTouchEvent 方法調用該 child 的 dispatchTouchEvent 方法進行分發。如此事件就分發到了子 View。

    此時,若是子 View 消費了 DOWN 事件( child 的 dispatchTouchEvent 返回 true ---> dispatchTransformedTouchEvent 方法返回 true)。就執行到了 代碼⑤ ,經過 addTouchTarget 將該 View 綁定到 TouchTarget 鏈表中,而且是在表頭位置,而且將 newTouchTarget 和 mFristTouchTarget 都賦值指向表頭。若是不消費,就進行下一次循環,尋找消費該事件的子 View。

    那麼,若是 ViewGroup 進行事件攔截呢?事件分發該怎麼執行?分爲兩種狀況:1. 直接在 DOWN 事件的時候進行攔截;2. 攔截 MOVE、UP 事件。

    1. 若是直接攔截 DOWN 事件,mFirstTouchTarget 爲 null,intercepted 爲 true。那麼直接執行到 代碼⑥ ,經過 dispatchTransformedTouchEvent 源碼可知,直接執行了super.dispatchTouchEvent(event)。View (ViewGroup 父類是 View)的 dispatchTouchEvent 方法內部調用了 onTouchEvent 方法,從而事件傳遞到了 ViewGroup 的 onTouchEvent 方法。
    2. MOVE、UP 事件到來的時候開始攔截,此時會產生 CANCEL 事件。具體分析在 延伸知識一:CANCEL 事件產生的內部實現。

    MOVE、UP 事件: MOVE、UP 事件可以傳遞過來,那就說明有子 View 消費了 DOWN 事件。mFristTouchTarget 不爲 null。代碼執行到 代碼⑦ ,經過 dispatchTransformedTouchEvent 方法將事件分發給消費了 DOWN 事件的子 View。

  • 延伸知識一:CANCEL 事件產生的內部實現。

    CANCEL 事件產生的條件就是,子 View 消費了 DOWN 事件,在 MOVE、UP 事件中,父View 攔截了事件。在 View的事件分發(一)分發流程 中的 ==CANCEL 事件模擬==中看到的日誌是:在第一個 MOVE 事件中,子 View 收到了 CANCEL 事件,第二個 MOVE 事件的時候,ViewGroup 的 onTouchEvent 才接收收到事件。 那麼咱們分析一下流程:

    因爲子 View 消費了 DOWN 事件,因此 mFirstTouchTarget 不爲 null 。在第一個 MOVE 事件來臨的時候,代碼執行到了 代碼① ,此時 onInterceptTouchEvent 返回 true,因此 intercepted 被賦值爲 true。而後代碼執行到了 代碼⑦ 。intercepted 爲true, cancelChild 天然也就爲 true。經過 dispatchTransformedTouchEvent 源碼可知,cancel 爲 true 時,就設置了一個 CANCEL 事件給 child。在 代碼⑧ 中,因爲此時是單點觸摸,mFirstTouchTarget 指向的鏈表只有綁定這個 View 的TouchTarget對象,next 爲 null,因此 mFirstTouchTarget 賦值爲 null。如此,child 在父控件攔截的時候就收到了一個 CANCEL 事件。

    第二個 MOVE 事件:因爲此時 mFirstTouchTarget 爲null,就直接調用了 代碼⑥ ,經過 dispatchTransformedTouchEvent 調用了自身的 onTouchEvent 方法。

    也能夠看出,一旦 ViewGroup 攔截以後, mFirstTouchTarget 重置爲 null,後續事件來的時候直接將 intercepted 設置爲 true,而再也不調用 onInterceptTouchEvent 方法。

  • 問題二:關於 requestDisallowInterceptTouchEvent

    requestDisallowInterceptTouchEvent 自己是 ViewParent 中的方法,ViewParent 是一個接口,ViewGroup 實現了它。咱們子類 View 中經過調用getParent().requestDisallowInterceptTouchEvent(true);來達到請求父控件不攔截。

    //ViewGroup 類
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }
        //給 mGroupFlags 設置標識。
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
    
        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
    複製代碼

    代碼塊① 中,能夠看到先經過 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;來進行判斷子類 View 是否設置了不容許攔截標識,而後再對自身是否攔截進行判斷。若是不容許攔截將 intercepted 設置爲 false。

    在 DOWN 事件中,ViewGroup 會先重置 mGroupFlags 標識。因此並非說子 View 在請求一次不攔截後,父控件就永遠不攔截。

    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        //重置 mGroupFlags
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    複製代碼

View 的 dispatchTouchEvent 方法

//View 類
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            //若是設置了 mOnTouchListener ,而且 onTouch 返回 true
            //就直接 返回結果 true
            result = true;
        }
        //不然,調用 View 的 onTouchEvent 方法
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    return result;
}
複製代碼

View 的 onTouchEvent 方法。

public boolean onTouchEvent(MotionEvent event) {
    //------代碼省略-------
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
                
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        //一個不可用的 View 仍然是能夠消費這個事件的,只是沒有響應而已。
        //根據 clickable 的值來決定是否消費這個事件。
        return clickable;
    }
    //是否設置了觸摸代理 (若是嘗試擴大 View 的 touch 範圍,可使用觸摸代理)
    if (mTouchDelegate != null) {
        //若是設置了觸摸代理,就將事件 event 傳遞個觸摸代理的 onTouchEvent 方法來處理。
        
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    //若是 clickable 爲 true ,就執行下面的代碼,並返回 true 消費事件。
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                //------代碼省略-------
                break;
            case MotionEvent.ACTION_DOWN:
                //------代碼省略-------
                break;
            case MotionEvent.ACTION_CANCEL:
                //------代碼省略-------
                break;
            case MotionEvent.ACTION_MOVE:
                //------代碼省略-------
                break;
                
        }
        return true;
    }
    return false;
}
複製代碼

能夠看出,View 的 dispatchTouchEvent 中,首先進行判斷是否設置了 mOnTouchListener ,若是設置了 mOnTouchListener 而且 mOnTouchListener.onTouch 返回 true,則 View 的 dispatchTouchEvent 方法直接返回 true, 不會調用 View 的 onTouchEvent 方法。不然調用 View 的 onTouchEvent 方法。

在 View 的 onTouchEvent 方法中,咱們能夠看到若是設置了點擊事件和長按事件,那麼 onTouchEvent 必定會返回爲 true了,也就是消費了事件。最終,事件走到這裏就算結束了。

因爲 OnLongClickListener 和 OnClickListener 的回調在 onTouchEvent 方法中調用,因此 當咱們設置了 setOnTouchListener 而且 onTouch 返回 true 的時候,setOnLongClickListener 和 setOnClickListener 是不會被回調的。

View 的 TouchEvent 詳細流程:View的事件分發(三)源碼分析

總結

  1. 完整的事件分發流程爲:Activity ---> PhoneWindow ---> DecorView ---> ViewGroup ---> View。
  2. ViewGroup 經過倒序遍歷子 View 來尋找處理 DOWN 事件的子 View,經過 dispatchTransformedTouchEvent 方法調用 child.dispatchTouchEvent 將事件分發給childView。而後將消費 DOWN 事件的 View 綁定到觸摸的鏈表中。mFirstTouchTarget 指向表頭。
  3. ViewGroup 一旦開始攔截事件,若是先前有子 View 處理事件,就分發一個 CANCEL 事件給以前處理事件的 View。以後的事件不在分發給子 View,也不會再調用 onInterceptTouchEvent 方法。
  4. 每一次 DOWN 事件都會重置 mGroupFlags 和 mFirstTouchTarget。子 View 調用 requestDisallowInterceptTouchEvent 方法,做用範圍僅限於當前事件流。
  5. 若是設置了 OnTouchListener 而且 onTouch 返回 true,則 OnLongClickListener 和 OnClickListener 的回調不起做用。

本篇文章用於記錄學習過程當中的理解和筆記,若有錯誤,請批評指正,萬分感謝!

參考文檔

《Android開發藝術探索》第三章

Android ViewGroup事件分發機制

View—事件分發

相關文章
相關標籤/搜索