基於原始碼分析 Android View 事件分發機制

騎摩托馬斯發表於2019-01-17

基於 Android 28 原始碼分析

所謂點選事件的事件分發,其實就是對 MotionEvent 事件的分發過程,即當一個 MotionEvent 產生了以後,系統需要把這個事件傳遞給一個具體的 View,而這個傳遞的過程就是分發過程。

三個重要方法

首先我們需要介紹在點選事件分發過程中很重要的三個方法:

dispatchTouchEvent

用來進行事件的分發。如果事件能夠傳遞給當前 View,那麼此方法一定會被呼叫,返回結果受當前 ViewonTouchEvent 和 下級 ViewdispatchTouchEvent 方法的影響,表示是否消耗當前事件。

onInterceptTouchEvent

dispatchTouchEvent 內部呼叫,用來判斷是否攔截某個事件,如果當前 View 攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次呼叫,返回結果表示是否攔截當前事件。

onTouchEvent

dispatchTouchEvent 內部呼叫,用來處理點選事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一事件序列中,當前 View 無法再次接受到事件。

其實它們的關係可以用如下虛擬碼表示:

public boolean dispatchTouchEvent(MotionEvent ev) {

    if (onInterceptTouchEvent(ev)) {
        return onTouchEvent(ev);
    }
    
    return child.dispatchTouchEvent(ev);
}
複製程式碼

對於一個根 ViewGroup 來說,點選事件產生後,首先會傳遞給它的 dispatchTouchEvent 方法,如果這個 ViewGrouponInterceptTouchEvent 返回為 true, 就表示它要攔截當前事件,接著事件就會交給該 ViewGrouponTouchEvent 方法去處理。如果 onInterceptTouchEvent 返回為 false,就表示它不攔截當前事件,這是當前事件就會傳遞給它的子元素,接著由子元素的 dispatchTouchEvent 來處理點選事件,如此反覆直到事件被最終處理。

事件分發的原始碼分析

當一個點選事件發生後,它的傳遞過程遵循如下順序:Activity -> Window -> View, 即事件總是先傳遞給 ActivityActivity 再傳遞給 Window, 最後 Window 再傳遞給頂級 View。 頂級 View 接受到事件後,就會按照事件分發機制去分發事件。

Activity 對點選事件的分發過程

// Activity.java

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

分析上面的程式碼,點選事件用 MotionEvent 來表示,當一個點選操作發生時,由當前 ActivitydispatchTouchEvent 來進行事件分發,具體的工作由 Activity 內部的 Window 來完成的。如果返回 true,整個事件迴圈就結束了,返回 false 意味著事件沒人處理,所有 ViewonTouchEvent 都返回了 false, 那麼 ActivityonTouchEvent 就會被呼叫。

Window 對點選事件的分發過程

接下來看 Window 是如何將事件傳遞給 ViewGroup 的。看原始碼會發現,Window 是個抽象類,而 WindowsuperDispatchTouchEvent 方法也是個抽象方法,因此必須找到 Window 的實現類才行。通過註釋可以發現 Window 的唯一實現類是 PhoneWindow,因此接下來看一下 PhoneWindow 是如何處理點選事件的。

// PhoneWindow.java

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
複製程式碼

PhoneWindow 將事件直接傳遞給了 DecorView,我們知道通過 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0) 這種方式就可以獲取到 Activity 中所設定的 View, 這個 mDecor 顯然就是 getWindow().getDecorView() 返回的 View,而我們通過 setContentView 設定的 View 是它的一個子 View。由於 DecorView 繼承子 FrameLayout 且是 父 View,所以最終事件會傳遞給 View。從這裡開始,事件已經傳遞到頂級 View 了,即在 Activity 中通過 setContentView 所設定的 View頂級 View 一般來說都是 ViewGroup

頂級 View 對點選事件的分發過程

首先看 ViewGroup 對點選事件的分發過程,其主要實現在 ViewGroupdispatchTouchEvent 方法中,這個方法程式碼量很多,分段進行說明。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
        
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) { // 判斷是否要攔截當前事件
                    
                // 根據 FLAG_DISALLOW_INTERCEPT 標記位來判斷是否要進行攔截
                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;
            }
            
            ...
    }
複製程式碼

上面程式碼可以看出,當事件型別為 ACTION_DOWN 或者 mFirstTouchTarget != null 這兩種情況下來判斷是否要攔截當前事件。ACTION_DOWN 事件容易理解,那麼 mFirstTouchTarget != null 是什麼意思呢? 這個從後面的程式碼邏輯可以看出來,當事件由 ViewGroup 的子元素成功處理時,mFristTouchTarget 就會被賦值指向子元素,那也就是說當事件是被當前 ViewGroup 攔截來處理而不交給子元素處理時,mFristTouchTarget == null ,那麼當 ACTION_MOVEACTION_UP 事件到來時,由於 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 這個條件為 false ,將導致 ViewGrouponInterceptTouchEvent 不會再被呼叫,並且同一序列中的其他事件都會預設交給該 ViewGroup 來處理。

這裡還有一種特殊情況,那就是 FLAG_DISALLOW_INTERCEPT 標記位,這個標記位是通過 requestDisallowInterceptTouchEvent 方法來設定的,一般用於子 View 中。 FLAG_DISALLOW_INTERCEPT 一旦設定後,ViewGroup 將無法攔截除了 ACTION_DOWN 以外的其他點選事件。為什麼是除了 ACTION_DOWN 以外的事件呢? 這是因為 ViewGroup 在分發事件時,如果是 ACTION_DOWN 就會重置 FLAG_DISALLOW_INTERCEPT 這個標記位,將導致子 View 中設定的這個標記位無效。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
        
            // 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(); // 重置 FLAG_DISALLOW_INTERCEPT 標記位
            }

            // Check for interception.
            final boolean intercepted;
            
            ...
    }
複製程式碼

上面的程式碼中, ViewGroup 會在 ACTION_DOWN 事件到來時做重置狀態的操作,而在 resetTouchState 方法中會對 FLAG_DISALLOW_INTERCEPT 進行重置,因此子 View 呼叫 requestDisallowInterceptTouchEvent 方法並不會影響 ViewGroupACTION_DOWN 事件的處理。

通過上面可以得出結論:當 ViewGroup 決定攔截事件後,那麼後續的點選事件將會預設交給它處理並且不再呼叫它的 onInterceptTouchEvent 方法。所以 onIntecepterTouchEvent 不是每次事件都會被呼叫的,如果我們想提前處理所有的點選事件,要選擇 dispatchTouchEvent 方法,只有這個方法能保證每次都會被呼叫,當然前提是事件能夠傳遞到當前的 ViewGroup 中。

接著來看 ViewGroup 不攔截事件的時候,事件會向下分發交由它的子 View 進行處理

// ViewGroup.java

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
        
            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;

                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--) { // 遍歷 ViewGroup 的所有子元素 判斷子元素是否能夠接受到點選事件
                            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)) { // 實際呼叫的就是子元素的 dispatchTouchEvent 方法
                                // 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();
                                // mFirstTouchTarget 被賦值並且跳出 for 迴圈
                                newTouchTarget = addTouchTarget(child, idBitsToAssign); 
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
            ...
    }
複製程式碼

上面程式碼的邏輯是,首先遍歷 ViewGroup 的所有子元素,然後判斷子元素是否能夠接受到點選事件。是否能夠接受點選事件主要由兩點來衡量:

  • 子元素是否在播動畫
  • 點選事件的座標是否落在子元素的區域內

如果子元素滿足這兩個條件,那麼事件就會傳遞給它來處理。傳遞由 dispatchTransformedTouchEvent 方法來完成

// ViewGroup.java

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

        ...

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

        ...
        
        return handled
    }
複製程式碼

可以發現如果 child 傳遞的不是 null,它會直接呼叫子元素的 dispatchTouchEvent 方法,這樣事件就交由子元素處理了,從而完成了一輪事件的分發。

如果子元素的 dispatchTouchEvent 返回 true,那麼上文提到的 mFirstTouchTarget 就會被賦值同時跳出 for 迴圈,mFirstTouchTarget 真正的賦值過程是由 addTouchTarget 函式完成的。

// ViewGroup.java

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
複製程式碼

通過程式碼可以看出, mFirstTouchTarget 是一種單連結串列資料結構。mFirstTouchTarget 是否被賦值,將直接影響到 ViewGroup 對事件的攔截策略,如果 mFirstTouchTargetnull,那麼 ViewGroup 就預設攔截接下來同一序列中所有的點選事件,這點上文已經分析過。

如果遍歷所有的子元素後事件都沒有被合適的處理,這包含兩種情況:

  1. ViewGroup 沒有子元素
  2. 子元素處理了點選事件,但是在 dispatchTouchEvent 中返回了 false,這一般是因為子元素在 onTouchEvent 中返回了 false

在以上兩種情況下, ViewGroup 會自己處理點選事件

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
        
            // 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);
            }
            
            ...
    }
複製程式碼

上段程式碼中 dispatchTransformedTouchEvent 中傳入的 childnull,從簽名的分析可以知道,它會呼叫 super.dispatchTouchEvent(event),很顯然,這裡就轉到了 ViewdispatchTouchEvent 方法中,即點選事件開始交由 View 來處理。

View 對點選事件的處理過程

// View.java

    public boolean dispatchTouchEvent(MotionEvent event) {
       ...
       
        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 (不包含 ViewGroup)是一個單獨的元素,它沒有子元素因此無法向下傳遞事件,所以只能自己處理事件。上面的原始碼可以看出 View 首先會判斷有沒有設定 onTouchListener,如果 onTouchListener 中的 onTouchListener 方法返回 true ,那麼 onTouchEvent 就不會被呼叫,可見 onTouchListener 的優先順序高於 onTouchEvent,這樣做的好處是方便在外界處理點選事件。

// View.java

    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) { // 不可用狀態下的 View 照樣會消耗點選事件
            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.
            return clickable;
        }
        
        ...
        
        // 只要 View 的 CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE 和 TOOLTIP 有一個為 true 就會消耗這個事件
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            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
                            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;

                ...
            }

            return true;
        }

        return false;
    }
複製程式碼

上面程式碼中,只要 ViewCLICKABLELONG_CLICKABLECONTEXT_CLICKABLETOOLTIP 有一個為 true 就會消耗這個事件。 即 onTouchEvent 方法返回 true,不管它是不是 DISABLE 狀態。然後就是當 ACTION_UP 事件發生時,會觸發 performClickInternal 方法,最終呼叫 performClick 方法。

// View.java

    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;
    }
複製程式碼

上述程式碼可知,如果 View 設定了 OnClickListener 那麼 performClick 方法內部就會呼叫它的 onClick 方法

總結

  1. 同一個事件序列是指從手指接觸螢幕的那一刻起,到手指離開螢幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序列以 down 事件開始,中間含有數量不定的 move 事件,最終以 up 事件結束
  2. 某個 View 一旦決定攔截,那麼這一個事件序列都只能又它來處理(如果事件序列可以傳遞給它的話),並且它的 onIntercepetTouchEvent 不會再被呼叫。這條也很好理解,就是說當一個 View 決定攔截一個事件後,那麼系統會把同一個事件序列內的其他方法都直接交給它來處理,因此就不用再呼叫這個 ViewonIntercepterTouchEvent 去詢問它是否要攔截了
  3. 正常情況下,一個事件序列只能被一個 View 攔截且消耗。這一條的原因可以參考上一條,因為一旦一個元素攔截了此事件,那麼同一個事件序列內的其他事件都會交由它來處理,因此同一個事件序列不可能交由兩個 View 同時來處理,但是通過特殊手段可以做到,比如一個 View 將本該自己處理的事件通過 onTouchEvent 強行傳遞給其他 View 處理。
  4. 某個 View 一旦開始處理事件,如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交由它的父元素去處理,即父元素的 onTouchEvent 會被呼叫。意思就是事件一旦交給一個 View 處理,那麼它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它來處理了。
  5. 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那麼這個點選事件會消失,此時父元素的 onTouchEvent 並不會被呼叫,並且當前 View 可以持續收到後續的事件,最終這些消失的點選事件會傳遞給 Activity 處理
  6. ViewGroup 預設不攔截任何事件,Android 原始碼中 ViewGrouponInterceptTouchEvent 方法預設返回 false
  7. View 沒有 onInterceptTouchEvent 方法,一旦有點選事件傳遞給它,那麼它的 onTouchEvent 方法就會被呼叫
  8. ViewonTouchEvent 預設都會消耗事件(返回 true),除非它是不可點選的(clickable 和 longClickable 同時為 false)。ViewlongClickable 屬性預設都為 falseclickable 屬性要分情況,比如 Buttonclickable 屬性預設為 true,而 TextViewclickable 屬性預設為 false
  9. Viewenable 屬性不影響 onTouchEvent 的預設返回值。哪怕一個 Viewdisable 狀態的,只要它的 clickable 或者 longClickable 有一個為 true,那麼它的 onTouchEvent 就返回 true
  10. onClick 會發生的前提是當前 View 是可點選的,並且它收到了 downup 的事件
  11. 事件傳遞過程是由外向內的,即事件總是先傳遞給父元素,然後再由父元素髮給子 View, 通過 requestDisallowInteceptTouchEvent 方法可以在子元素中干預父元素的事件分發過程,但是 ACTION_DOWN 事件除外
  12. View 設定的 OnTouchListener,其優先順序比 onTouchEvent 要高,如果 OnTouchListeneronTouch 方法的回撥返回 true 那麼 onTouchEvent 方法將不會被呼叫。如果返回 false,則當前 ViewonTouchEvent 方法被回撥。

參考

相關文章