Android事件分發(fā)機制源碼分析

一、事件定義

定義:當(dāng)用戶觸摸屏幕時,將產(chǎn)生的觸摸行為(Touch事件)

事件類型

  • MotionEvent.ACTION_DOWN 手指剛接觸屏幕
  • MotionEvent.ACTION_MOVE 手指在屏幕上滑動
  • MotionEvent.ACTION_UP 手指從屏幕上松開
  • MotionEvent.ACTION_CANCEL 非人為因素取消

二、事件序列

正常情況下一次手指觸摸屏幕的行為會觸發(fā)一系列事件

  1. 點擊屏幕后立即松開,事件序列為DOWN -> UP
  2. 點擊屏幕滑動一段距離后松開,事件序列為DOWN -> MOVE -> ... -> MOVE -> UP

事件序列流程圖如下

image.png

三、事件分發(fā)對象

  • Activity:控制生命周期&處理事件
  • ViewGroup:一組View的集合
  • View:所有UI組件的基類

四、事件分發(fā)主要方法

  • dispatchTouchEvent(MotionEvent ev):用來進行事件分發(fā)
  • onInterceptTouchEvent(MotionEvent ev):判斷是否攔截事件(ViewGroup)
  • onTouchEvent(MotionEvent ev)::處理觸摸事件

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

按照慣例,為了方便大家理解源碼,我們先來看看整個事件分發(fā)的流程圖


image.png

Activity的事件分發(fā)

Activity是最先得到用戶觸摸事件的,首先我們來看Activity#dispatchTouch

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

這里有個判斷這個觸摸事件是不是DOWN事件,如果是則調(diào)用onUserInteraction(),這個方法是一個空方法,我們繼續(xù)往下看,getWindow()會返回一個PhoneWindow對象,接著調(diào)用PhoneWindow#superDispatchTouchEvent,如果返回true則事件結(jié)束,否則表示事件沒有被消費,則 調(diào)用Activity#onTouchEvent來處理事件,繼續(xù)跟進PhoneWindow#superDispatchTouchEvent

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

mDecor是頂層布局容器DecorView,繼續(xù)跟進DecorView#superDispatchTouchEvent

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

可以看到DecorView中是直接調(diào)用了 super.dispatchTouchEvent(event),因為DecorView繼承自FrameLayout,所以最后會調(diào)用到ViewGroup#sdispatchTouchEvent(event),我們來總結(jié)下Activity的事件分發(fā):Activity->PhoneWindow->DecorView

ViewGroup的事件分發(fā)

現(xiàn)在事件已經(jīng)從Activity分發(fā)到了ViewGroup,我們接著來看ViewGroup的事件分發(fā)
ViewGroup#dispatchTouchEvent

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
        ...
        
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            // --------注釋1--------
            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();
            }

            // --------注釋2--------
            // 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;
            }
                        
                ...

                // --------注釋3 --------
                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;
                        
                        // 對子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.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            // --------注釋4 --------
                            if (!child.canReceivePointerEvents()
                                    || !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);
                            
                            // --------注釋5 --------
                            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();
                                
                                // --------注釋6 --------
                                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();
                    }

             ...
                    
            // --------注釋7 --------
            // 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);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    
                    // --------注釋8 --------
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        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.
            
            ...
            
        return handled;
    }

看到注釋1處,如果是一個DOWN事件,表明是一個新的事件的開始,會調(diào)用兩個方法進行一些標(biāo)記的清除,cancelAndClearTouchTargets(ev)主要的作用是將全局變量mTouchTarget置null,resetTouchState()主要作用是將全局變量mGroupFlags清除,接下來繼續(xù)往下看注釋2處的代碼,這塊代碼主要是用來檢測是否需要對事件進行攔截,必須是DOWNS事件或者mTouchTarget不為null才會可能去執(zhí)行onInterceptTouchEvent(ev),否則攔截標(biāo)記intercepted直接賦值為true,然后繼續(xù)根據(jù)mGroupFlags是不是設(shè)置為FLAG_DISALLOW_INTERCEPT,是的話表明不攔截該事件,攔截標(biāo)記intercepted賦值為false,否則執(zhí)行onInterceptTouchEvent(ev),當(dāng)子View調(diào)用requestDisallowInterceptTouchEvent()方法來請求父ViewGroup不攔截事件時會給mGroupFlags賦值為FLAG_DISALLOW_INTERCEPT

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

在DOWN事件時,已經(jīng)對mTuchTarget置null,mGroupFlags清除,導(dǎo)致在DOWN事件intercepted賦值為true,表示攔截該事件,所以requestDisallowInterceptTouchEvent只能作用DOWN事件之后的事件(MOVE、UP)。接著看注釋3處,在DOWN事件時,如果newTouchTarget == null && childrenCount != 0,則直接對子View進行倒敘遍歷,繼續(xù)往下看注釋4處child.canReceivePointerEvents()isTransformedTouchPointInView(),先看View#canReceivePointerEvents

protected boolean canReceivePointerEvents() {
        return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
    }

這個方法表示View可見或者正在執(zhí)行動畫,接著看
MotionEvent#isTransformedTouchPointInView

    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

這個方法主要作用是判斷觸摸區(qū)域是否在View的區(qū)域內(nèi) ,回到注釋4處,這個條件判斷表示如果當(dāng)前View不可見并且沒有在執(zhí)行動畫,或者觸摸區(qū)域不在View的區(qū)域內(nèi),則直接continue,進行下次遍歷。繼續(xù)看到注釋5處,如果找到了觸摸的View,調(diào)用dispatchTransformedTouchEvent()并將這個View傳進去,跟進ViewGroup#dispatchTransformedTouchEvent

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

        ...
         
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

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

如果child不為空,則調(diào)用child.dispatchTouchEvent(event)將事件傳遞給child,如果child為null,則直接調(diào)用super.dispatchTouchEvent(event),此時如果 child 為View,則直接調(diào)用View的dispatchTouchEvent(event)處理事件,如果child 為ViewGroup,則事件最終會來到ViewGroup的onTouchEvent()方法 ,回到注釋5處條件判斷,如果dispatchTransformedTouchEvent()返回true, 表示子View消費了事件,接著調(diào)用注釋6處的addTouchTarget(child, idBitsToAssign),ViewGroup#addTouchTarget

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

這里對mFirstTouchTarget賦值為消費了事件的子View的TouchTarget,回到調(diào)用該方法的注釋6處,將消費了事件的子View的TouchTarget賦值給newTouchTarget,并將alreadyDispatchedToNewTouchTarget設(shè)為true

我們先來總結(jié)下到目前為止ViewGroup#dispatchTouchEvent所做的事情:在DOWN事件時,遍歷所有子View,找到消費事件的子View,并將子View賦值給mFirstTouchTarget,即mFirstTouchTarget指向了消費事件的子View,如果沒有子View消費事件,則 mFirstTouchTarget依舊為null

繼續(xù)看到ViewGroup#dispatchTouchEvent中注釋7處,如果mFirstTouchTarget為null,表明沒有子View消費事件,則調(diào)用dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS),并將第三個參數(shù)child傳null,上面我們已經(jīng)分析了,如果child為null,則直接調(diào)用super.dispatchTouchEvent(event),此時如果 child 為View,則直接調(diào)用View的dispatchTouchEvent(event)處理事件,如果child 為ViewGroup,則事件最終會來到ViewGroup的onTouchEvent()方法

繼續(xù)看注釋8處,如果mFirstTouchTarget不為null,表示DOWN事件已經(jīng)找到了一個子View來消費事件,條件判斷(alreadyDispatchedToNewTouchTarget && target == newTouchTarget)當(dāng)找到消費事件的子View時值為true,然后直接給handles賦值為true,DOWN事件結(jié)束,如果上面條件判斷為false,則else里面的代碼是對MOVE和UP事件的處理,繼續(xù)調(diào)用dispatchTransformedTouchEvent()將MOVE和UP事件直接分發(fā)給消費了DOWN事件的 子View

我們來簡單總結(jié)下ViewGroup#dispatchTouchEvent所做的全部事情:ViewGroup中可以通過onInterceptTouchEvent()對事件進行攔截,返回true表示攔截,返回false表示不攔截,子View可以調(diào)用getParent().requestDisallowInterceptTouchEvent(true)來請求父ViewGroup不攔截事件(只對MOVE和UP事件生效),接著遍歷子View,找到消費了DOWN事件的子View,并將后續(xù)MOVE和UP事件直接分發(fā)給消費了DOWN事件的子View,若沒有子View消費DOWN事件,則會調(diào)用View的dispatchTouchEvent(),若返回true,則會調(diào)用此ViewGroup的onTouchEvent()將事件交給自己的onTouchEvent()處理,后續(xù)的MOVE和UP事件將不再向下分發(fā),直接交給ViewGroup#onTouchEvent處理

View的事件分發(fā)

現(xiàn)在事件已經(jīng)從ViewGroup分發(fā)到了View,我們接著來看View的事件分發(fā)
View#dispatchTouchEvent

    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;
            
            // --------注釋9 --------
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

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

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // 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
        ...

        return result;
    }

從代碼中可以看出,如果View正在被拖拽,則直接消費掉事件,mListenerInfo是ListenerInfo的對象,在給View設(shè)置一些監(jiān)聽的時候貴初始化mListenerInfo,看到注釋9處的條件判斷,View默認(rèn)就是enable的,所以只要設(shè)置了OnTouchListener,會調(diào)用OnTouchListener#onTouch方法,返回true,則事件被消費,返回false,則繼續(xù)執(zhí)行onTouchEvent(event)方法,onTouchEvent返回true,則事件被消費,繼續(xù)跟進View#onTouchEvent

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        // --------注釋10 --------
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return 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);
                        }


                        // --------注釋12 --------
                        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;

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

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        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();
                        
                        // --------注釋11 --------
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    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);
                    }

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

            // --------注釋13 --------
            return true;
        }

        return false;
    }

看到注釋10處,只要View是可點擊的或者可長按的clickable就為true,并且View即使設(shè)置為DISABLED,也不會對clickable產(chǎn)生影響 ,依舊返回clickable,如果clickable為true,則說明事件被消費了,只不過對這個事件沒有響應(yīng),看到注釋13處直接返回clickable,表明如果View是可以點擊的,那么直接消費掉事件,我們繼續(xù)往下看在DOWN事件中給mHasPerformedLongPress標(biāo)記位設(shè)為false,表示目前還沒有處理長按事件,接著繼續(xù)判斷當(dāng)View是在可滑動容器時,注釋11處發(fā)送一個延遲100ms的mPendingCheckForTap任務(wù)來檢查是否是長按事件,ViewConfiguration.getTapTimeout()返回的是100ms,CheckForTap代碼如下

    private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true, x, y);
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }

如果View不是在可滑動容器里面,則直接調(diào)用checkForLongClick()方法來檢查長按事件,跟進View#checkForLongClick

    private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }

注意這里mHasPerformedLongPress也被賦值false,ViewConfiguration.getLongPressTimeout()返回的是500ms,這段代碼又發(fā)送了一個500ms的延遲任務(wù)mPendingCheckForLongPress來表示長按事件,看到UP事件里面的注釋12處,大家應(yīng)該還記得之前在DOWN事件中和checkForLongClick()方法中我們將mHasPerformedLongPress賦值為false,如果在UP事件時mHasPerformedLongPress依然為false,則表明沒有長按事件,調(diào)用removeLongPressCallback()移除長按事件回調(diào),繼續(xù)回到checkForLongClick(),欄看下mPendingCheckForLongPress的run方法

    private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }

        public void setAnchor(float x, float y) {
            mX = x;
            mY = y;
        }

        public void rememberWindowAttachCount() {
            mOriginalWindowAttachCount = mWindowAttachCount;
        }

        public void rememberPressedState() {
            mOriginalPressedState = isPressed();
        }
    }

可以看到run方法里面調(diào)用了performLongClick(mX, mY),當(dāng)返回值為true時,將mHasPerformedLongPress標(biāo)記設(shè)為true,所以若從DOWN事件到UP時間超過了500ms,則認(rèn)為事件是長按事件,否則是點擊事件,繼續(xù)跟進performLongClick方法發(fā)現(xiàn)最終會調(diào)用performLongClickInternal(float x, float y),并且返回performLongClickInternal(float x, float y)的返回值,繼續(xù)跟進
View#performLongClickInternal

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

可以看到這里調(diào)用了mOnLongClickListener.onLongClick回調(diào),就是我們代碼中設(shè)置長按事件的回調(diào),如果我們在onLongClick回調(diào)中返回true,則mHasPerformedLongPress標(biāo)記設(shè)為true,在onTouchEvent()方法的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)) {
                                    performClickInternal();
                                }
                            }
                        }

如果mHasPerformedLongPress標(biāo)記為true,則不會執(zhí)行條件里面的
post(mPerformClick),跟進PerformClick

private final class PerformClick implements Runnable {
        @Override
        public void run() {
            performClickInternal();
        }
    }

run方法中調(diào)用了performClickInternal()方法,跟進performClickInternal()發(fā)現(xiàn)繼續(xù)調(diào)用了performClick(),跟進View#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;
    }

可以看到其中調(diào)用了mOnClickListener.onClick(this),就是我們在代碼中設(shè)置的點擊監(jiān)聽回調(diào),到此事件分發(fā)就分析完畢了。

六、事件分發(fā)總結(jié)

Activity事件分發(fā)

  1. Activity#dispatchTouchEvent
  2. PhoneWindow#superDispatchTouchEvent
  3. DecorView#superDispatchTouchEvent
  4. ViewGroup#dispatchTouchEvent
  5. 若事件沒有被消費,則最終調(diào)用Activity#onTouchEvent

ViewGroup事件分發(fā)

  1. ViewGroup#dispatchTouchEvent(對down事件特殊處理:cancelAndClearTouchTargets(),resetTouchState(),清除標(biāo)記后down事件一定會執(zhí)行onInterceptTouchEvent(),onInterceptTouchEvent()默認(rèn)返回false,不攔截)
  2. 若沒有攔截,ViewGroup#dispatchTansformedTouchEvent,for循環(huán)遍歷子View,調(diào)用子view的dispatchTouchEvent,若沒有子view消費事件,則直接調(diào)用super.dispatchTouchEvent(event) ,此時如果 child 為View,則直接調(diào)用View的dispatchTouchEvent(event)處理事件,如果child 為ViewGroup,則事件最終會來到ViewGroup的onTouchEvent()方法
  3. 若有子view消費事件,則繼續(xù)將down事件后續(xù)的move和up事件分發(fā)給子view處理,ViewGroup不做處理
  4. 若沒有view消費事件,則down事件后續(xù)的move和up事件不再向下分發(fā),直接交給ViewGroup#onTouchEvent處理
  5. 子view可以調(diào)用getParent().requestDisalowInterceptTouchEvent()請求ViewGroup不攔截事件(只對MOVE和UP事件有效)

View事件分發(fā)

  1. View#dispatchTouchEvent(若view被設(shè)置了監(jiān)聽器,則會先調(diào)用監(jiān)聽器的 onTouch()方法,若onTouch()方法返回true,則事件被消費,不會調(diào)用onTouchEvent(), 若返回false,則會繼續(xù)調(diào)用onTouchEvent() )
  2. onTouch()返回false,View#onTouchEvent(若view是可以被點擊的(clickable == true),則直接返回true消費掉事件),在down事件里面會做一個長按事件的檢測 ,在up事件中檢測沒有長按事件則移除長按事件回調(diào)并響應(yīng)點擊事件,如果設(shè)置了長按事件并且長按事件的onLongClick()返回為true,則不執(zhí)行onClick(),若onLongClick()返回為false,則會繼續(xù)執(zhí)行onClick()事件
  3. 行先后順序:dispatchTouchEvent-> onTouch -> onTouchEvent -> onLongClick() -> onClick()
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容