理解 View 的事件分發(fā)機制

1、基礎(chǔ)認(rèn)知

事件
在我們通過屏幕與手機交互的時候,每一次點擊、長按、移動等都是一個個事件。按照面向?qū)ο蟮乃枷?,這些一個個事件都被封裝成了 MotionEvent 對象。
事件列
從手指解除屏幕到手指離開屏幕,這個過程產(chǎn)生了一系列的事件。一般情況下,事件列都是以 DOWN 事件開始,UP 事件結(jié)束,中間有無數(shù)的 MOVE 事件。這一個事件列中的所有事件,要么被忽略,要么就只能有一個事件能使用。要是同一個序列,比如從按下到移動這一系列的動作,不同的 View 都能接受的話,那整個界面就會非?;靵y,而且邏輯很復(fù)雜。

事件列.png

事件分發(fā)的本質(zhì)
所謂點擊事件的事件分發(fā),其實就是對 MotionEvent 事件的分發(fā)過程,即當(dāng)一個 MotionEvent 產(chǎn)生了以后,系統(tǒng)需要把這個事件傳遞給一個具體的 View 并且進行處理,而這個傳遞的過程就是分發(fā)過程。

事件在哪些對象之間進行傳遞?
Activity、ViewGroup、View

事件分發(fā)的順序
首先需要知道,Android 中的 View 是樹狀結(jié)構(gòu)。每一個 Activity 內(nèi)部都包含一個 Window 用來管理要顯示的視圖。而 Window 是一個抽象類,其具體實現(xiàn)是 PhoneWindow 類。DecorView 作為 PhoneWindow 的一個內(nèi)部類,實際管理著具體視圖的顯示。DecorView 是FrameLayout 的子類,盛放著我們的標(biāo)題欄和根視圖。我們自己寫的一些 View 和 ViewGroup 都是由他來管理的。事件分發(fā)的時候,頂層的這些“基礎(chǔ)View”們實際上是不會對事件有任何操作的,他們只是把事件不斷的向下遞交,直到我們可以使用這些事件。
所以事件自頂向下的分發(fā)順序是:
Activity(基本不處理) --> 根View --> 一層一層 ViewGroup(如果有的話) --> 子View

事件分發(fā)過程由哪些方法來協(xié)作完成?
dispatchTouchEvent : 分發(fā)事件
onInterceptTouchEvent : 攔截事件
onTouchEvent : 消費事件
1)三個方法的共同點:
他們具體是否執(zhí)行了自己的功能(分發(fā)、攔截、消費)完全由自己的返回值來確定,返回 true 就表示自己完成了自己的功能(分發(fā)、攔截、消費)。
2)不同點:
dispatchTouchEvent() 和 onTouchEvent() 這兩個方法,無論是 Activity、ViewGroup 還是 View,都會被用到。而 onInterceptTouchEvent() 方法因為只是為了攔截事件,那么 Activity 和 View 一個在最頂層,一個在最底層,也就沒必要使用了。因此在 Activity 和 View 中是沒有 onInterceptTouchEvent() 方法的。

接下來,會進入源碼來分析整個事件分發(fā)機制。

2、Activity 事件分發(fā)源碼分析

當(dāng)一個點擊事件發(fā)生時,事件最先傳到 Activity 的 dispatchTouchEvent() 進行事件分發(fā)。
下面是 Activity 中的 dispatchTouchEvent 方法的源碼。

// Activity.java
/**
 * 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);
}

該方法被用來處理屏幕觸摸事件??梢灾貙懘朔椒▉韺崿F(xiàn)在事件在分發(fā)到 window 前攔截所有事件。當(dāng)事件被消費時,該方法返回 true。

當(dāng)為 ACTION_DOWN 事件時,調(diào)用 onUserInteraction() 方法,即一觸摸屏幕就會觸發(fā)此方法調(diào)用。onUserInteraction 在 Activity 中是一個空方法,可以通過重寫該方法來實現(xiàn)一觸摸便需要進行的邏輯,比如屏保功能。當(dāng)當(dāng)前 Activity 在棧頂時,觸屏點擊按 Home、Back、Menu 鍵等都會觸發(fā)此方法。

當(dāng) getWindow().superDispatchTouchEvent(ev) 方法返回 true 時,整個方法返回 true,即該點擊事件停止往下傳遞,事件傳遞過程結(jié)束。
getWindow(),獲取 Window 類對象,Window 類是抽象類,其唯一實現(xiàn)類是 PhoneWindow,下面為 PhoneWindow 的 superDispatchTouchEvent 方法。

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

可以看到,該方法調(diào)用了 DecorView 的 superDispatchTouchEvent 方法。

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

最終調(diào)用 ViewGroup 的 dispatchTouchEvent 方法,即 Activity 將事件傳遞給了 ViewGroup。
所以,getWindow().superDispatchTouchEvent(ev) 方法實現(xiàn)了將事件從 Activity 傳遞給 ViewGroup。

回到 Activity 的 dispatchTouchEvent 方法中,如果 getWindow().superDispatchTouchEvent(ev) 方法返回不為 true,即該事件未被 Activity 下任何一個 View 接收或處理時,則調(diào)用并返回 Activity 的 onTouchEvent(ev) 方法。

// Activity.java
/**
 * Called when a touch screen event was not handled by any of the views
 * under it.  This is most useful to process touch events that happen
 * outside of your window bounds, where there is no view to receive it.
 *
 * @param event The touch screen event being processed.
 *
 * @return Return true if you have consumed the event, false if you haven't.
 * The default implementation always returns false.
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

如果 mWindow.shouldCloseOnTouch(this, event) 方法返回 ture,則 finish 掉 Activity 并返回 ture,其他情況均返回 false。看一下 Window 的 shouldCloseOnTouch 方法:

// Window.java
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}

Window 的 shouldCloseOnTouch 方法主要是對于處理邊界外點擊事件的判斷:是否是 ACTION_UP 事件,event 的坐標(biāo)是否在邊界內(nèi),或是否為 ACTION_OUTSIDE 事件等。

3、ViewGroup 事件分發(fā)源碼分析

從 Activity 的事件分發(fā)源碼可知,ViewGroup 事件分發(fā)過程從 dispatchTouchEvent() 開始。下面便是 ViewGroup 的 dispatchTouchEvent 的源碼。

// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    // handled 標(biāo)志保存的是最終返回的結(jié)果,表示是否被消費,不繼續(xù)分發(fā),默認(rèn)為否,繼續(xù)分發(fā)。
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 判斷是否為 ACTION_DOWN 事件,如果是的話,代表一個新的事件序列開始。
        // 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.
            // 取消并清除所有觸摸 target,將 mFirstTouchTarget 設(shè)置為 null
            cancelAndClearTouchTargets(ev);
            // 重置所有觸摸狀態(tài),為新的事件序列做準(zhǔn)備
            resetTouchState();
        }

        // Check for interception.
        // intercepted 標(biāo)志保存的是是否進行攔截,下面就是做攔截檢查
        final boolean intercepted;
        // 1、只有當(dāng)前未 ACTION_DOWN 事件
        // 2、當(dāng) ACTION_DOWN 事件被子 View 消費后處理其他事件時會調(diào)用該代碼
        // 因為此時 mFirstTouchTarget!=null(mFirstTouchTarget 為 TouchTarget 序列中的第一個對象)
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            // 可以調(diào)用方法 requestDisallowInterceptTouchEvent 來禁止 ViewGroup 的事件攔截
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 如果沒有禁止 ViewGroup 事件攔截
                // 那么就會調(diào)用 ViewGroup 的 onInterceptTouchEvent 方法來確認(rèn)是否進行攔截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                // 如果禁止了 ViewGroup 事件攔截,那么 intercepted 標(biāo)志直接設(shè)為 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.
            // 當(dāng)不存在消耗 ACTION_DOWN 事件的目標(biāo)控件時
            // 后續(xù)事件的 intercepted 都會被設(shè)為 true
            // 此時可以理解為 ViewGroup 退化成 View,事件處理將交給 super.dispatchTouchEvent() 進行
            // 
            // 所以當(dāng)我們當(dāng)點擊 ViewGroup 的空白位置時
            // 由于不存在消耗 ACTION_DOWN 事件的子控件,導(dǎo)致 mFirstTouchTarget 為空
            // 任何后續(xù)的事件到來時,intercepted 都會被設(shè)為 true 而被 ViewGroup 攔截
            // 包括多點觸控 ACTION_POINTER_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;

        // 事件分發(fā)
        // 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;
        // 沒有被取消或者當(dāng)前 ViewGoup 不進行攔截
        if (!canceled && !intercepted) {

            // If the event is targeting accessibility 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;
                    
            // 這里開始對事件類型進行區(qū)分了,如果是 ACTION_DOWN,ACTION_POINTER_DOWN
            // ACTION_DOWN 為第一個手指初次觸摸到屏幕時觸發(fā)
            // ACTION_POINTER_DOWN 為有非主要的手指按下時觸發(fā),即按下之前已經(jīng)有手指在屏幕上,即多點觸控
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // 觸控點下標(biāo)
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                // 位分配 ID,通過觸控點的 PointerId 計算
                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.
                // 檢查是否有記錄的 PointID
                // 假如 mFirstTouchTarget 不為空,檢查 TouchTarget 序列,檢索是否存在記錄了該觸控點的 TouchTarget
                // 如果存在,則移除該觸控點記錄
                // 移除后,如果 TouchTarget 不存在其他的觸控點記錄,則從序列中移除
                removePointersFromTouchTargets(idBitsToAssign);

                // 接下來開始遍歷自己的子 View 們
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    // 獲取到點擊的坐標(biāo),用來從子 View 中篩選出點擊到的 View
                    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.
                    // 按從前往后的順序開始遍歷子 View
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    // 遍歷子 View 判斷哪個子 View 接受事件
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        // If there is a view that has accessibility focus we want it
                        // to get the event first and if not handled we will perform a
                        // normal dispatch. We may do a double iteration but this is
                        // safer given the timeframe.
                        // 篩選到不合適的子 View 時直接 continue
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }
                        // 同樣也是做篩選操作
                        if (!child.canReceivePointerEvents()  // 判斷控件是否可以接受事件,當(dāng)控件可見性為 VISIBLE 或者正在執(zhí)行動畫時,返回 true
                                || !isTransformedTouchPointInView(x, y, child, null)) {  // 判斷該子 View 是否包含事件的坐標(biāo)
                            // 不可接受事件,或點擊坐標(biāo)不在其中,則跳過該子 View
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        
                        // 當(dāng)找到了合適的子 View 時,通過 getTouchTarget 方法獲取 TouchTarget 序列中是否包含該子 View 的 touch target
                        // 未找到對應(yīng)的 target 時返回空
                        newTouchTarget = getTouchTarget(child);
                        // 要是返回的結(jié)果不為空,即找到了可以向其分發(fā)事件的子 View,就跳出循環(huán)
                        if (newTouchTarget != null) {
                            // Child is already receiving touch within its bounds.
                            // Give it the new pointer in addition to the ones it is handling.
                            // 如果 TouchTarget 序列中已經(jīng)存在對應(yīng) View 的 TouchTarget
                            // 就直接把 idBitsToAssign 添加到 TouchTarget 中
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        // 就算沒有在 TouchTarget 序列中找到該 View 的 TaouchTarget 結(jié)果也沒關(guān)系
                        // 在這里繼續(xù)調(diào)用 dispatchTransformedTouchEvent() 方法,并將該子 View 作為參數(shù)傳遞進去
                        // 該方法返回 true 表示事件分發(fā)給該子 View,此時將 alreadyDispatchedToNewTouchTarget 置為了 ture。
                        // 
                        // 而代碼塊中的 addTouchTarget 方法中,
                        // 生成一個新的 TouchTarget(包裹著消化事件的 View)
                        // 并添加到了 TouchTarget 序列的頭部,此時 mFirstTouchTarget 為這個新的 TouchTarget
                        // 返回的 newTouchTarget 此時和 mFirstTouchTarget 相同
                        // 且 newTouchTarget.next 為舊的 mFirstTouchTarget 值
                        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();
                }

                // 經(jīng)過前面的 for 循環(huán)沒有找到消耗 ACTION_POINTER_DOWN 事件的 View
                // 但是 mFirstTouchTarget 不為空
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    // 把 newTouchTarget 指向 TouchTarget 序列的最后的元素(一般即為消耗 ACTION_DOWN 事件的控件)
                    // 并把當(dāng)次 ACTION_POINTER_DOWN 事件的 PointID 記錄到該元素。

                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // 接下來就是對于其他事件的分發(fā)了
        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            // 沒有找到要接受事件的 View,或者被攔截了,這里調(diào)用了 dispatchTransformedTouchEvent() 并且傳了一個 null 的 View 參數(shù)
            // 即分配給 ViewGroup 自身
            // 也就是說,如果子 View 沒有消費事件,那么子 View 的上層 ViewGroup 會調(diào)用其 onTouchEvent() 處理Touch事件
            // 此時 ViewGroup 像一個普通的 View 那樣調(diào)用 dispatchTouchEvent() 方法,并且在 dispatchTouchEvent() 方法中調(diào)用 onTouchEvent() 方法
            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.
            // 有 View 接受了 ACTION_DOWN 事件,那么這個 View 也將接受其余的事件
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    // alreadyDispatchedToNewTouchTarget 變量在將 ACTION_DOWN,ACTION_POINTER_DOWN 事件傳遞給子 View 時設(shè)為了 true
                    // 同時 target 為這個子 View 的 target
                    // 所以這里直接標(biāo)志 handled = true,避免重復(fù)分發(fā)事件
                    handled = true;
                } else {
                    // 否則依然是遞歸調(diào)用 dispatchTransformedTouchEvent 方法
                    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.
        // 處理 ACTION_UP 和 ACTION_CANCEL
        // 在此主要的操作是還原狀態(tài)
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            // 這里說明這是最后一個觸控點抬起,通過 resetTouchState 方法進行清理和還原狀態(tài)。
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            // 如果是 ACTION_POINTER_UP 事件
            // 那就移除觸控點對應(yīng)的 TouchTarget 內(nèi)的 pointerIdBits 記錄
            // 移除后如果 pointerIdBits = 0(即沒有其他觸控點記錄)
            // 則把該 TouchTarget從 TouchTarget 序列中移除
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

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

整個 dispatchTouchEvent 源碼我是這么理解的:

首先對于 TouchEvent 來說,其成員變量中包含:
1、View child:被點擊的子 View,即消耗事件的目標(biāo) View;
2、int pointerIdBits:為了區(qū)分多點觸控時不同的觸控點,每一個觸控點都會攜帶一個 pointerId。而 pointerIdBits 即是所有被當(dāng)前 View 消耗的觸控點的 pointerId 的組合。即 pointerIdBits 包含一個或以上的 pointerId 數(shù)據(jù);
3、TouchTarget next:記錄的下一個 TouchTarget 對象,由此組成了 TouchTarget 序列。
所以, TouchTarget 的作用是記錄一個 View 及其對應(yīng)分發(fā)的觸控點列表 pointerIdBits,且可以通過 next 與其他實例形成序列。TouchTarget 把消耗事件的 View 以序列方式保存,且記錄各個 View 對應(yīng)的觸控點列表,以實現(xiàn)后續(xù)的事件分發(fā)。
接下來,可以具體分析源碼邏輯了。

handled 標(biāo)志是返回的結(jié)果,表示是否被消費,不繼續(xù)分發(fā),默認(rèn)為否,繼續(xù)分發(fā)。

actionMasked == MotionEvent.ACTION_DOWN 判斷是否為 ACTION_DOWN 事件,如果是的話,代表一個新的事件序列開始。然后調(diào)用 cancelAndClearTouchTargets 方法取消并清除所有觸摸 target,調(diào)用 resetTouchState 方法重置所有觸摸狀態(tài),為新的事件序列做準(zhǔn)備。

intercepted 用來標(biāo)志是否被攔截。接著判斷是否為 ACTION_DOWN 事件,或 mFirstTouchTarget 不為空。即兩種情況:
1、只有當(dāng)前為 ACTION_DOWN 事件。
2、或者當(dāng) ACTION_DOWN 事件被子 View 消費后處理其他時會調(diào)用該代碼,因為此時 mFirstTouchTarget!=null(mFirstTouchTarget 為 TouchTarget 序列中的第一個對象,mFirstTouchTarget 為 null 表示 TouchTarget 序列是空的)。
如果滿足條件,就根據(jù) disallowIntercept(表示是否不允許攔截),如果允許攔截,那么調(diào)用 onInterceptTouchEvent(ev) 方法進行事件攔截的判斷,否則直接將 intercepted 設(shè)為 false,表示不被攔截;如果不滿足條件,即不存在消耗 ACTION_DOWN 事件的目標(biāo)控件時,那么后續(xù)的事件就直接將 intercepted 設(shè)為 true,表示進行攔截。從這里可以知道,如果子 View 沒有消費 Touch 事件,那么當(dāng)后續(xù)的事件到來時在這里直接返回 true。所以當(dāng)我們當(dāng)點擊 ViewGroup 的空白位置時,由于不存在消耗 ACTION_DOWN 事件的子控件,導(dǎo)致 mFirstTouchTarget 為空,任何后續(xù)的事件到來時(包括多點觸控 ACTION_POINTER_DOWN 事件),intercepted 都會被設(shè)為 true 而被 ViewGroup 攔截。所以,當(dāng) ViewGroup 決定攔截事件后,那么后續(xù)的點擊事件也將會默認(rèn)交給它處理,不再調(diào)用 onInterceptTouchEvent() 判斷是否需要攔截。

之后在 if (!canceled && !intercepted) 判斷的代碼塊中,如果當(dāng)前事件沒有被取消或攔截的情況,那就要開始處理事件分發(fā)了。
1、首先對事件類型進行區(qū)分,先處理 ACTION_DOWN 或者 ACTION_POINTER_DOWN 事件。
先記錄觸控點下標(biāo)和 PointerId,然后檢查是否有記錄過 PointId 的 TouchTarget 存在(存在則移除),接下再開始遍歷子 View。
通過一系列過濾找到合適的子 View 。
找到子 View 后,先通過 getTouchTarget 方法獲取 TouchTarget 序列中是否已經(jīng)存在包含該子 View 的 TouchTarget,如果已經(jīng)存在,就直接把 idBitsToAssign 添加到該 TouchTarget 中。
之后調(diào)用 dispatchTransformedTouchEvent 方法并將子 View 作為參數(shù)傳入,如果返回 true,表示事件傳遞給子 View,子 View 需要消費該事件,此時先通過 addTouchTarget 方法生成一個新的 TouchTarget(包含該需要消費事件的 View),并添加到了 TouchTarget 序列的頭部,同時賦值給 newTouchTarget,然后將 alreadyDispatchedToNewTouchTarget 標(biāo)志設(shè)為 true。
如果經(jīng)過輪詢子 View 的循環(huán)后沒有找到消耗 ACTION_POINTER_DOWN 事件的 View,那就把 newTouchTarget 指向 TouchTarget 序列的最后的元素(即為消耗 ACTION_DOWN 事件的控件),并把當(dāng)次 ACTION_POINTER_DOWN 事件的 PointID 記錄到該元素。
2、接下來處理其他事件,當(dāng)沒有找到要接受事件的子 View 時,或者被攔截了,這里調(diào)用了 dispatchTransformedTouchEvent 方法并且傳了一個 null 的 View 參數(shù),即分發(fā)給 ViewGroup 自身;當(dāng)有 View 接受了 ACTION_DOWN 事件,那么這個 View 也將接受其余的事件,其余事件也是通過遞歸調(diào)用 dispatchTransformedTouchEvent 方法分發(fā)。
3、如果是 ACTION_UP 和 ACTION_CANCEL 事件,通過 resetTouchState 方法進行清理和還原狀態(tài)。如果是 ACTION_POINTER_UP 事件,那就移除觸控點對應(yīng)的 TouchTarget 內(nèi)的 pointerIdBits 記錄,移除后如果 pointerIdBits = 0(即沒有其他觸控點記錄),則把該 TouchTarget從 TouchTarget 序列中移除。

可以發(fā)現(xiàn),dispatchTouchEvent 方法中通過調(diào)用 dispatchTransformedTouchEvent 方法,將子 View 作為參數(shù)傳入,下面就來看一看這個方法。

// ViewGroup.java
/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
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.
    // 處理 ACTION_CANCEL
    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) {
                // 如果傳來的參數(shù) child 為空時,調(diào)用自身 dispatchTouchEvent() 方法
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                // 如果傳來的參數(shù) child 不為空,那么就調(diào)用該 child 的 dispatchTouchEvent() 方法
                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) {
        // 如果傳來的參數(shù) child 為空時,調(diào)用自身 dispatchTouchEvent() 方法
        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());
        }
        // 如果傳來的參數(shù) child 不為空,那么就調(diào)用該 child 的 dispatchTouchEvent() 方法
        handled = child.dispatchTouchEvent(transformedEvent);
    }

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

當(dāng)方法中的 child 參數(shù)為空時,都會調(diào)用 super.dispatchTouchEvent(event) 方法,也就是將事件交給 ViewGroup 自己處理;否則調(diào)用 child.dispatchTouchEvent(event) 方法,即將事件分發(fā)給子 View。
綜合上面的分析,所以當(dāng) child 為空時,即沒有子 View 或子 View 不消費事件,ViewGroup 可以處理該事件,而當(dāng)子 View 消費該事件時,ViewGroup 就無法調(diào)用 super.dispatchTouchEvent 方法處理事件。也就是說,子 View 消費事件,父 View 就無法消費該事件了。

一些小結(jié)
1、ViewGroup 的事件分發(fā)從 dispatchTouchEvent 方法開始。
2、ViewGroup 先調(diào)用 onInterceptTouchEvent 方法判斷自己是否攔截事件。
1)如果 intercepted 為 true,自己攔截事件,則最后會調(diào)用自身的 super.dispatchTouchEvent 方法,最后自己處理該事件。
2)如果 intercepted 為 false,但是沒有找到被點擊的相應(yīng)的子 View,那么最后會調(diào)用自身的 super.dispatchTouchEvent 方法,最后自己處理該事件。。
3)如果 intercepted 為 false,如果找到被點擊的相應(yīng)的子 View,那么會調(diào)用 child.dispatchTouchEvent 方法將事件傳遞給子 View。
3、當(dāng) ViewGroup 決定攔截事件后,后續(xù)的事件將會默認(rèn)交給它處理,不再調(diào)用 onInterceptTouchEvent() 判斷是否需要攔截。
4、ViewGroup 在遇到一個新的事件序列時,開始遍歷自己的所有子 View,找到需要接收到事件的子 View。無論是否找到,最后都會調(diào)用 dispatchTransformedTouchEvent() 方法,區(qū)別在于如果找到了,那么在這個方法中傳入的是那個子 View 對象,否則就為空。
5、沒有子 View 或子 View 不消費事件,ViewGroup 可以處理該事件,而當(dāng)子 View 存在且子 View 消費該事件時,ViewGroup 就無法調(diào)用 super.dispatchTouchEvent 方法處理該事件。
6、多點觸摸同一個 View 時,釋放后該 View 的點擊事件只會觸發(fā)一次。

4、View 事件分發(fā)源碼分析

由上文可知,ViewGroup 自己處理事件,會調(diào)用 super.dispatchTouchEvent 方法;分發(fā)給子 View 處理事件,會調(diào)用 child.dispatchTouchEvent 方法,他們最終走到的都是 View 的 dispatchTouchEvent 方法。下面就來看看 View 的 dispatchTouchEvent 方法的源碼。

// View.java
/**
 * 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) {
    ...
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        // 停止正在進行的嵌套滑動
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
        // 過濾出應(yīng)用安全策略的 Touch 事件
        // 當(dāng) View 處于 enabled 狀態(tài),并且處理了通過鼠標(biāo)輸入拖動滾動條,就將 result 設(shè)為 true,表示消費該事件
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        // ListenerInfo 中封裝了各種 Listener 監(jiān)聽,比如熟悉的 OnClickListener、OnLongClickListener 等
        // 這里判斷設(shè)置了 OnTouchListener 監(jiān)聽,且 onTouch 方法返回 true
        // 則將 result 設(shè)為 ture,表示事件被消費
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 最后如果經(jīng)過上面的過程 result 仍為 false,那么再去調(diào)用 onTouchEvent(event) 方法來消費該事件
        // onTouchEvent 方法中如果消費該事件,則返回 true,此時 result 將會被設(shè)為 true
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    // 最后如果為 ACTION_UP、ACTION_CANCEL 事件或者為 ACTION_DOWN 事件但未消費事件,也停止正在進行的嵌套滑動
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

首先判斷如果為 ACTION_DOWN 事件,那么調(diào)用 stopNestedScroll 方法停止正在進行的嵌套滑動。

然后過濾出應(yīng)用安全策略的 Touch 事件后。
先判斷為 enabled 狀態(tài),并且處理了鼠標(biāo)輸入拖動滾動條事件,那么 result 為 true。
接下來判斷設(shè)置了 OnTouchListener 監(jiān)聽,且監(jiān)聽回調(diào)方法 onTouch 返回 true,即事件被消費,那么也將 result 設(shè)為 true。
最后如果經(jīng)過上面的過程 result 仍為 false,那么再去調(diào)用 onTouchEvent(event) 方法來消費該事件,事件被消費的話 result 就會被設(shè)為 true。

接下來如果為 ACTION_UP、ACTION_CANCEL 事件或者為 ACTION_DOWN 事件但未消費事件,也停止正在進行的嵌套滑動。

最后返回結(jié)果 result。

可以知道,事件在 onTouchEvent(event) 方法中確定是否被消費。下面來看看 onTouchEvent 的源碼。

// View.java
/**
 * 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();

    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    // 判斷如果 View 被設(shè)置為 disabled,
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        // clickable 的 View 雖然被設(shè)為了 disabled,但是仍然消耗觸摸事件,只是不響應(yīng)它們
        // 所以這里返回 clickable
        return clickable;
    }
    // 如果設(shè)置有 mTouchDelegate,則交由 TouchDelegate 處理。
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    // 兩種情況進入 if 代碼塊
    // 1、當(dāng) View 為 clickable 時
    // 2、或者 View 有 TOOLTIP,表示此 View 可以在懸?;蜷L按時顯示工具提示。
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        // 下面便是針對不同的事件進行相應(yīng)的處理
        switch (action) {
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                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 (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    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
                        // 當(dāng)前為點擊事件的情況
                        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)) {
                                performClickInternal();
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }

                    removeTapCallback();
                }
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_DOWN:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    break;
                }

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // 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(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                if (clickable) {
                    setPressed(false);
                }
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                break;

            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                final int motionClassification = event.getClassification();
                final boolean ambiguousGesture =
                        motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                int touchSlop = mTouchSlop;
                if (ambiguousGesture && hasPendingLongPressCallback()) {
                    final float ambiguousMultiplier =
                            ViewConfiguration.getAmbiguousGestureMultiplier();
                    if (!pointInView(x, y, touchSlop)) {
                        // The default action here is to cancel long press. But instead, we
                        // just extend the timeout here, in case the classification
                        // stays ambiguous.
                        removeLongPressCallback();
                        long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                * ambiguousMultiplier);
                        // Subtract the time already spent
                        delay -= event.getEventTime() - event.getDownTime();
                        checkForLongClick(
                                delay,
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    touchSlop *= ambiguousMultiplier;
                }

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

                final boolean deepPress =
                        motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                if (deepPress && hasPendingLongPressCallback()) {
                    // process the long click action immediately
                    removeLongPressCallback();
                    checkForLongClick(
                            0 /* send immediately */,
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                }

                break;
        }

        return true;
    }

    return false;
}

onTouchEvent 方法主要負(fù)責(zé)處理 touch 事件的。如果使用此方法檢測點擊動作,則建議通過實現(xiàn)并調(diào)用 performClick() 方法,這樣能確保系統(tǒng)行為的一致性。

首先獲取 touch 事件的坐標(biāo)、action、clickable 等信息。

然后處理 View 被設(shè)置為 disabled 的情況。clickable 的 View 雖然被設(shè)為了 disabled,但是仍然消耗觸摸事件,只是不響應(yīng)它們,所以直接返回 clickable。如果設(shè)置有 mTouchDelegate,則交由 TouchDelegate 處理。

接下來只要 View 為 clickable 或有 TOOLTIP 標(biāo)簽(表示此 View 可以在懸停或長按時顯示工具提示),便開始針對不同事件類型進行處理,主要分為 ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE 四種情況。

在處理 ACTION_UP 事件時,會調(diào)用到 performClick 方法;在處理 ACTION_DOWN 事件時,會依次調(diào)用到 checkForLongClick -> checkForLongPress -> performLongClick(mX, mY) -> performLongClick() -> performLongClickInternal(mLongClickX, mLongClickY) 方法。
最后可以看一下 performClick 方法和 performLongClickInternal 方法的源碼。

// View.java
/**
 * Call this view's OnClickListener, if it is defined.  Performs all normal
 * actions associated with clicking: reporting accessibility event, playing
 * a sound, etc.
 *
 * @return True there was an assigned OnClickListener that was called, false
 *         otherwise is returned.
 */
// NOTE: other methods on View should not call this method directly, but performClickInternal()
// instead, to guarantee that the autofill manager is notified when necessary (as subclasses
// could extend this method without calling super.performClick()).
public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

/**
 * Calls this view's OnLongClickListener, if it is defined. Invokes the
 * context menu if the OnLongClickListener did not consume the event,
 * optionally anchoring it to an (x,y) coordinate.
 *
 * @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
 *          to disable anchoring
 * @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
 *          to disable anchoring
 * @return {@code true} if one of the above receivers consumed the event,
 *         {@code false} otherwise
 */
private boolean performLongClickInternal(float x, float y) {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

    boolean handled = false;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnLongClickListener != null) {
        handled = li.mOnLongClickListener.onLongClick(View.this);
    }
    if (!handled) {
        final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
        handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
    }
    if ((mViewFlags & TOOLTIP) == TOOLTIP) {
        if (!handled) {
            handled = showLongClickTooltip((int) x, (int) y);
        }
    }
    if (handled) {
        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    }
    return handled;
}

performClick 方法中,先檢查 ListenerInfo 中是否設(shè)置 OnClickListener 回調(diào),然后調(diào)用 onClick 回調(diào)方法。performLongClickInternal(float x, float y) 方法中,會先檢查 ListenerInfo 中是否設(shè)置 OnLongClickListener 方法,然后調(diào)用 onLongClick 回調(diào)方法。所以,平時常用的 onClick 、onLongClick 回調(diào)方法是在 View 的 onTouchEvent 方法中處理的。

一些小結(jié)
1、View 的事件分發(fā)過程從 View 的 dispatchTouchEvent 方法開始。
2、View 的事件分發(fā)過程沒有 onInterceptTouchEvent 方法參與,所以 onInterceptTouchEvent 方法只是 ViewGroup 事件分發(fā)過程中存在的方法。因為 View 沒有子 View,所以不存在需要攔截事件的需求。
3、View 的 enable 屬性不影響 onTouchEvent 的默認(rèn)返回值。
4、平時常用的 onClick 、onLongClick 回調(diào)方法是在 View 的 onTouchEvent 方法中處理的。
5、View 的 onTouchEvent 方法默認(rèn)都會消耗事件(返回 true),除非它是不可點擊的(clickable 和 longClickable 同時為false)。

5、View 的事件分發(fā)機制簡易流程圖

View 的事件分發(fā)流程.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容