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

基於 Android 28 源碼分析java

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

三個重要方法

首先咱們須要介紹在點擊事件分發過程當中很重要的三個方法:數據結構

dispatchTouchEvent

用來進行事件的分發。若是事件可以傳遞給當前 View,那麼此方法必定會被調用,返回結果受當前 ViewonTouchEvent 和 下級 ViewdispatchTouchEvent 方法的影響,表示是否消耗當前事件。app

onInterceptTouchEvent

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

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 來處理點擊事件,如此反覆直到事件被最終處理。post

事件分發的源碼分析

當一個點擊事件發生後,它的傳遞過程遵循以下順序: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 就會被調用。ui

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 方法被回調。

參考

相關文章
相關標籤/搜索