View 體系之 View 事件分發源碼解析

View 體系之 View 事件分發源碼解析

本文原創,轉載請注明出處。
歡迎關注我的 簡書 ,關注我的專題 Android Class 我會長期堅持為大家收錄簡書上高質量的 Android 相關博文。

寫在前面:

前兩天我們分別總結了
View 的位置與事件:
View 的位置與事件

View 的滑動:
View 的滑動

今天我們來聊聊 View 的事件分發。相信每個人都知道 View 的事件分發實在是太重要了,它不僅僅是一個核心知識點,更是一個難點。在我初學 Android 時,View 的事件分發也前前后后看了好多次。雖然也能復述出一個大概,但是仍然有一些知識盲區。所以今天把 View 的事件分發總結出來,算是自己記下一篇學習筆記,未來復習鞏固使用。如果還能為大家解決一些困惑,那就更好了。

當然關于事件分發的文章,前輩們總結了很多,有一篇我認為非常出色:
圖解 Android 事件分發

這篇文章通過圖解的方式,清晰直觀的講明白了事件分發的原則,本文打算對這篇文章做一些補充,補充一下這篇文章的一些關鍵 log,和源碼分析。所以不理解事件分發的朋友可以閱讀下該文。

首先大家想一下,在 Android 中誰是事件分發的掌控者和消費者?沒錯,是 Activity、ViewGroup、View,一個正常的 Android 應用程序他們三個肯定是存在的。而分發的事件就是 MotionEvent 對象。

關于事件分發,有三個關鍵的方法

dispatchTouchEventonInterceptTouchEventonTouchEvent

onInterceptTouchEvent 方法是 ViewGroup 特有的。我們在 Activity、ViewGroup、View 中分別打印這幾個方法,來看看不同的返回值對事件分發的影響,來印證上文的觀點,并且分析出事件分發的傳遞規則。

當我們不修改任何返回值,全部為默認實現時:

這里寫圖片描述

可以看到 ACTION_DOWN 事件的傳遞原則為,U 型原則,ACTION_MOVE、ACTION_UP 傳遞原則為距離最短原則。

分別來改變 Activity 中dispatchTouchEventonTouchEvent 的返回值,來看看事件傳遞的 log:

首先分別將 dispatchTouchEvent 的返回值改為 false 或者 true:

這里寫圖片描述

可以看到我的這次點擊按鈕的事件在 Activity 中的 dispatchTouchEvent 中消費掉了。

當我改變 MainActivity 中 onTouchEvent 方法的返回值時:

這里寫圖片描述

可以看到打印的結果與最初所有方法的默認返回值相同,這也很好理解,因為 Activity 的 onTouchEvent 方法本身就是事件 U型 傳遞的最后一環,不管什么返回值,反正事件都會到這里。

Activity 的看完了,再來看看 ViewGroup 的:

這里寫圖片描述

可以看到將 ViewGroup 的 dispatchTouchEvent 返回值改為 false 時,事件就不會再下發了,而是直接傳遞給 Activity 的 onTouchEvent。當 dispatchTouchEvent 返回值改為 true 時,與默認實現相同。

這里寫圖片描述

onInterceptTouchEvent 的返回值改為 true 時,事件不會再傳遞給 View ,而是傳遞給當前 ViewGroup 的 onTouchEvent。當onInterceptTouchEvent返回值為 false 時,與默認相同。

這里寫圖片描述

首先明確一個概念:事件序列

就是當手指 按下-->滑動-->抬起 的這一完成過程產生的事件流為一個事件序列。

當我將 ViewGroup 的 onTouchEvent 方法的返回值改為 true 時,事件在 ViewGroup 就消費掉了,這里應該注意,onInterceptTouchEvent 如果發生了攔截,那么在一個事件序列中僅調用一次。

關于 View 這兩個方法的返回值就不貼圖了,與引用文章的結論一致。

一些細節

當給一個 View 設置 onTouchListener 時,它的 onTouch 方法就會回調,如果 onTouch 方法的返回值為 false,則該 View 的 onTouchEvent 方法會被調用,如果 onTouch 方法的返回值為 true,則該 View 的 onTouchEvent 方法就不會調用了,事件會直接在該 View 的 disPatchTouchEvent 中消費。另外 onClick 方法是在 onTouchEvent 方法中調用的。所以這幾個方法的優先級關系為:

onTouch>onTouchEvent>onClick

一個 View 的 onTouchEvent 的返回值是與這個 View 本身的 onClick 和 onLongClick 屬性相關的,只有這兩個屬性同時為 false 則 onTouchEvent 才會為 false,View 的 onLongClick 默認都為 false,而 onClick 屬性不同,比如 button 的為 true,textview 的為 false。

事件分發的源碼解析

在這部分內容中,我們看看事件分發在源碼上的處理,事件最初都是在 Acitivity 中產生,然后分發給根 ViewGroup,最后再發給相應的 View。那先來看看 Activity 的 dispatchTouchEvent 方法。

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

這段代碼很簡單,當 ACTION_DOWN 來了的時候,回調 onUserInteraction 方法作為事件起始的回調。

然后來看看 getWindow().superDispatchTouchEvent(ev) 方法的返回值是如何的。

setContentView

首先關于 Window 和 PhoneWindow 類的關系可以上面這篇我曾經總結的文章。

PhoneWindow:

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

跟進,看看 DectorView 的 superDispatchTouchEvent(event) 方法:

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

DectorView 繼承自 FrameLayout,所以這里也是調用到了 ViewGroup 的 dispatchTouchEvent,事件順利傳到了 ViewGroup

來看看 ViewGroup 對事件的分發
代碼比較多,我們分段來看:

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

這段代碼的意義是,判斷是否要調用 onInterceptTouchEvent 方法,可以看到 if 判斷的條件語句為:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
先看第一個,當事件為 ACTION_DOWN 時,肯定會調用 onInterceptTouchEvent,那 mFirstTouchTarget 是什么呢?由后面的代碼可知,當事件不被攔截并且交給子元素處理時,mFirstTouchTarget != null。所以當被當前 View 攔截的時候,mFirstTouchTarget == null,ACTION_DOWN、ACTION_MOVE 事件來的時候,條件就不成立了,所以 onInterceptTouchEvent方法也不會再次調用,這也就是為什么之前說,當此 ViewGroup 確定攔截事件的時候,onInterceptTouchEvent之后在事件為 ACITON_DOWN 的時候調用一次。

這有一個 flag 比較重要,FLAG_DISALLOW_INTERCEPT,它的值由子 View 的 requestDisallowInterceptTouchEvent 決定,由子 View 請求父 View 不要攔截事件。當然此屬性對 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.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

dispatchTouchEvent 的開頭就重置了 FLAG 的狀態。

這里我們會有兩個結論:

  1. onInterceptTouchEvent 方法并不是每次都調用,而如果事件傳遞進來,dispatchTouchEvent 才是每次都會調用的。
  2. requestDisallowInterceptTouchEvent 可以干預父 View 的事件分發過程,有助于我們解決滑動沖突。
                    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);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

這段代碼首先判斷所有的子 View 是否具有接受事件的能力,1 沒有進行動畫 2 點擊的位置在 View 的坐標范圍內。dispatchTransformedTouchEvent 中有這樣一行代碼:

handled = child.dispatchTouchEvent(event);

所以到這里,就調用到了子 View 的 dispatchTouchEvent 方法。

在 addTouchTarget 方法中:

 mFirstTouchTarget = target;

mFirstTouchTarget 被賦值,也就是當子 View 處理事件時,mFirstTouchTarget 不為 null.

看完了 ViewGroup 對事件分發的處理,我們來看看 View 對事件的處理吧。

    /**
     * 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;

        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)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        return result;
    }

因為 View 不會再有子 View 了,所以他的 dispatchTouchEvent 方法比較簡單,首先如果這個 View 設置了 onTouchListener,并且 onTouch 方法返回值為 true 時,會進入判斷條件,方法直接返回 true,就不會走到 onTouchEvent 方法里面了。所以這里也印證了我們之前的觀點,也就是 onTouch 方法優先級大于 onTouchEvent。

再來看看 View 的 onTouchEvent 方法的源碼:

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

可以看到這里即使 View 是 disable 的,依然可以消耗事件。

if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
     switch (action) {
                case MotionEvent.ACTION_UP:
                        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();
                                }
                            }
                        }
                    break;

可以看到當這個 View 的 LongClick 或者 Clickable 屬性有一個為 true,就可以消耗這個事件,并且在 ACTION_UP 調用 performClick():

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

所以到這里這個 View 的點擊事件也響應了。

到這里,我們事件分發的源碼就分析完畢了。

寫在后面:

本文更多的是對上面那篇引用文章的源碼補充,兩篇結合起來看,對事件分發的理解就應該足夠了。這幾天看源碼看得頭疼。。。關于本文的結論總結,我準備過一陣回頭溫習的時候補充下,希望大家喜歡。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容