android 事件分發機制詳解(二)

Activity的Touch事件事實上是調用它內部的ViewGroup的Touch事件,可以直接當成ViewGroup處理。View在ViewGroup內,ViewGroup也可以在其他ViewGroup內,這時候把內部的ViewGroup當成View來分析。ViewGroup的相關事件有三個:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。View的相關事件只有兩個:dispatchTouchEvent、onTouchEvent。

前面一篇文章中我們分析了App返回按鍵的分發流程,從Native層到ViewRootImpl層到DocorView層到Activity層,以及在Activity中的dispatchKeyEvent方法中分發事件,最終調用了Activity的finish方法,即銷毀Activity,所以一般情況下假如我們不重寫Activity的onBackPress方法或者是onKeyDown方法,當我們按下并抬起返回按鍵的時候默認都是銷毀當前Activity。而本文中我們主要介紹觸摸事件的分發流程,從Native層到Activity層觸摸事件的分發了流程和按鍵的分發事件都是類似的,這里我們可以根據異常堆棧信息看一下。

at com.example.aaron.helloworld.MainActivity.dispatchTouchEvent(MainActivity.java:103)
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2359)
at android.view.View.dispatchPointerEvent(View.java:8698)
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4530)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4388)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3924)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3977)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3943)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4053)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3951)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4110)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3924)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3977)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3943)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3951)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3924)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6345)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6301)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6254)
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6507)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)

這樣經過一系列的方法調用之后最終調用了Activity的dispatchTouchEvent方法,而我們也是從Activiyt的dispatchTouchEvent方法開始對觸摸事件的分發進行分析。

在具體查看Activity的dispatchTouchEvent方法之前我們先簡單介紹一下觸摸事件,觸摸事件是由一個觸摸按下事件、N個觸摸滑動事件和一個觸摸抬起事件組成的,通常的一個觸摸事件中只能存在一個觸摸按下和一個觸摸抬起事件,但是觸摸滑動事件可以有零個或者多個。好了,知道這個概念以后,下面我們就具體看一下Activity中的dispatchTouchEvent的實現邏輯。

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

在看一下dispatchTouchEvent方法之前我們首先需要解釋一下MotionEvent的概念。MotionEvent是一個觸摸動作的封裝,里面包含了觸摸動作的類型,以及操作等屬性,我們具體的可以看一下MotionEvent的說明:

Object used to report movement (mouse, pen, finger, trackball) events. Motion events may hold either absolute or relative movements and other data, depending on the type of device.

然后在dispatchTouchEvent方法中,會首先判斷MotionEvent的動作類型,也就是我們的觸目動作的類型,判斷其是否是“按下”操作,若是的湖澤,則執行onUserInteraction方法,這個方法又是實現了什么邏輯呢?

public void onUserInteraction() {
    }

可以發現其在Activity中只是一個簡單的空實現方法,同樣的我們可以看一下該方法的介紹:

Called whenever a key, touch, or trackball event is dispatched to the activity. Implement this method if you wish to know that the user has interacted with the device in some way while your activity is running. This callback and {@link #onUserLeaveHint} are intended to help activities manage status bar notifications intelligently; specifically, for helping activities determine the proper time to cancel a notfication.

理解上就是用戶在觸屏點擊,按home,back,menu鍵都會觸發此方法。

回到Activity的dispatchTouchEvent方法中,我們調用了getWindow().suerDispatchTouchEvent()方法,我們分析過Activity的加載繪制流程,而這里的getWindow()就是返回Activity中的mWindow對象,而我們知道Activity中的mWindow對象就是一個PhoneWindow的實例。并且這里的window.superDispatchTouchEvent若返回值為ture,則直接返回true,否則的話會執行Activity的onTouchEvent方法,繼續我們看一下PhoneWindow的superDispatchTouchEvent方法。

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

可以看到在PhoneWindow中的superDispatchTouchEvent方法中調用的是mDecor.superDispatchTouchEvent方法,而這里的mDecor是我們Activity顯示的ViewTree的根View,并且mDecor是一個FrameLayout的子類,所以這里我們看一下mDecor的superDispatchTouchEvent方法。

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    ...
    public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
    ...
}

在DecorView的superDispatchTouchEvent方法中我們調用了super.dispatchTouchEvent方法,而我們的DecorView繼承于FrameLayout,但是經過查看之后我們知道FrameLayout中并沒有實現dispatchTouchEvent方法,而由于我們的FrameLayout繼承于ViewGroup,所以這里的dispatchTouchEvent方法應該就是ViewGroup的dispatchTouchEvent方法。

好了,這里先暫時說一下Acitivty中的事件分發流程

  • ViewRootImpl層的事件分發會首先調用Activity的dispatchTouchEvent方法;

  • Activity的dispatchTouchEvent方法中會通過Window.superDispatchTouchEvent方法將事件傳遞給DecorView即ViewGroup。

  • 若window的superDispatchTouchEvent方法返回true,則事件分發完成,Activity的dispatchTouchEvent直接返回為true,否則的話調用Activity的onTouchEvent方法,并且Acitivty的dispatchTouchEvent返回值與Activity的onTouchEvent返回值一致。

下面我們在繼續看一下ViewGroup的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

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

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

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

            // 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.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // 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;
            if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity 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 = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(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;
                            }

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

            // 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;
                    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.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

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

前面我們知道觸摸事件是由一個觸摸按下事件,一個觸摸抬起事件和N個觸摸滑動事件組成的,而這里的觸摸按下事件就是這里的ACTION_DOWN,同時友誼ACTION_DOWN是一系列事件的開端,所以我們在ACTION_DOWN時進行一些初始化操作,從上面源碼中注釋也可以看出來,清除以往的Touch狀態然后開始新的手勢。并在在cancelAndClearTouchTargets(ev)方法中將mFirstTouchTarget設置為了null,接著在resetTouchState()方法中重置Touch狀態標識。

然后標記ViewGroup是否攔截Touch事件的傳遞,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)這一條判斷語句說明當事件為ACTION_DOWN或者mFirstTouchTarget不為null(即已經找到能夠接收touch事件的目標組件)時if成立,否則if不成立,然后將intercepted設置為true,也即攔截事件。這里說明一下ViewGroup中的onInterceptTouchEvent方法是ViewGroup中特有的方法用于表示是否攔截觸摸事件,返回為true的話則表示攔截事件,事件不在向子View中分發,若范圍為false的話,則表示不攔截事件,繼續分發事件。

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

一般的我們可以在自定義的ViewGroup中重寫該方法,用于攔截事件的分發。而當我們在父ViewGroup重寫該方法返回為true執行事件攔截的邏輯的時候,可以在子View中通過調用requestDisallowInterceptTouchEvent方法,重新設置父ViewGroup的onInterceptTouchEvent方法為false,不攔截對事件的分發邏輯。

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

比如常見的向我們的ViewPager中由于需要處理左右滑動事件從而在其onInterceptTouchEvent方法中重寫了返回值,返回為true,攔截對事件的處理邏輯,但是若這時候ViewPager中嵌套了ListView,則listView也需要處理觸摸事件的邏輯,但是ViewPager中已經重寫了onInterceptTouchEvent方法,這時候怎么辦呢?幸運的是ListView也在內部的實現中調用了requestDisallowInterceptTouchEvent方法,保證自身獲得對觸摸事件的處理。

然后在代碼中我們判斷childrenCount個數是否不為0,繼續我們獲取子View的list集合preorderedList;最后通過一個for循環倒序遍歷所有的子view,這是因為preorderedList中的順序是按照addView或者XML布局文件中的順序來的,后addView添加的子View,會因為Android的UI后刷新機制顯示在上層;假如點擊的地方有兩個子View都包含的點擊的坐標,那么后被添加到布局中的那個子view會先響應事件;也就是說后被添加的子view會浮在上層,點擊的時候最上層的那個組件先去響應事件。

然后代碼通過調用getTouchTarget去查找當前子View是否在mFirstTouchTarget.next這條target鏈中的某一個targe中,如果在則返回這個target,否則返回null。在這段代碼的if判斷通過說明找到了接收Touch事件的子View,即newTouchTarget,那么,既然已經找到了,所以執行break跳出for循環。如果沒有break則繼續向下執行,這里你可以看見一段if判斷的代碼if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)),那么這個方法又是執行什么邏輯的呢?

在該方法中為一個遞歸調用,會遞歸調用dispatchTouchEvent()方法。在dispatchTouchEvent()中如果子View為ViewGroup并且Touch沒有被攔截那么遞歸調用dispatchTouchEvent(),如果子View為View那么就會調用其onTouchEvent()。dispatchTransformedTouchEvent方法如果返回true則表示子View消費掉該事件。

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

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

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

然后在在ViewGroup的dispatchTransformedTouchEvent方法中,調用了該ViewGroup的child View的dispatchTouchEvent方法,若其子View也是ViewGroup,則重復執行ViewGroup的dispatchTouchEvent方法,若其子View是View,則執行View的dispatchTouchEvent方法。

但這里大概分析了一下ViewGroup的事件分發流程

  • 首先在android的事件分發流程中,通過調用Activity的dispatchTouchEvent,事件會首先被派發是先傳遞到最頂級的DecorView也就是ViewGroup,再由ViewGroup遞歸傳遞到View的。

  • 在ViewGroup中可以通過設置onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,返回false代表不對事件進行攔截,默認返回false。

下面我們繼續看一下View的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        if (onFilterTouchEventForSecurity(event)) {
            //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的dispatchTouchEvent方法的內容比較長,我們重點看一下View對觸摸事件的處理邏輯,首先調用了onFilterTouchEventForSecurity(event)方法判斷當前的View是否被遮蓋,若沒有的話,則判斷View的mListenerInfo城邊變量是否為空,而這里的mListenerInfo又是什么呢?通過分析源碼我們知道這里的mListenerInfo是通過setOnClickListener方法設置的。

public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

可以當前View一旦執行了setOnClickListener方法改View的mListenerInfo就不為空,若后有判斷了該View是否可點擊,最后是判斷View的onTouchListener的onTouch方法的返回值。

所以當我們為當前View設置了OnTouchListener并且返回值為true的話,則直接執行其onTouch方法,若onTouch方法返回為true的話,則直接返回不在執行后續的View的onTouchEvent方法,否則繼續執行View的onTouchEvent方法,而我們繼續看一下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();

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

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

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

                        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:
                    mHasPerformedLongPress = false;

                    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(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

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

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

在ACTION為MotionEvent.ACTION_UP時,我們經過層層調用最終執行了performClick,方法而這個方法中我們回調了View的OnClickListener的onClick方法。。。

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

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

總結:

? 事件分發:public boolean dispatchTouchEvent(MotionEvent ev)

Touch 事件發生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會以隧道方式(從根元素依次往下傳遞直到最內層子元素或在中間某一元素中由于某一條件停止傳遞)將事件傳遞給最外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由該 View 的 dispatchTouchEvent(MotionEvent ev) 方法對事件進行分發。dispatchTouchEvent 的事件分發邏輯如下:

如果 return true,事件會分發給當前 View 并由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;
如果 return false,事件分發分為兩種情況:
如果當前 View 獲取的事件直接來自 Activity,則會將事件返回給 Activity 的 onTouchEvent 進行消費;
如果當前 View 獲取的事件來自外層父控件,則會將事件返回給父 View 的 onTouchEvent 進行消費。
如果返回系統默認的 super.dispatchTouchEvent(ev),事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。
? 事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)

在外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系統默認的 super.dispatchTouchEvent(ev) 情況下,事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件攔截邏輯如下:

如果 onInterceptTouchEvent 返回 true,則表示將事件進行攔截,并將攔截到的事件交由當前 View 的 onTouchEvent 進行處理;
如果 onInterceptTouchEvent 返回 false,則表示將事件放行,當前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發;
如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),事件默認會被攔截,并將攔截到的事件交由當前 View 的 onTouchEvent 進行處理。
? 事件響應:public boolean onTouchEvent(MotionEvent ev)

在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情況下 onTouchEvent 會被調用。onTouchEvent 的事件響應邏輯如下:

如果事件傳遞到當前 View 的 onTouchEvent 方法,而該方法返回了 false,那么這個事件會從當前 View 向上傳遞,并且都是由上層 View 的 onTouchEvent 來接收,如果傳遞到上面的 onTouchEvent 也返回 false,這個事件就會“消失”,而且接收不到下一次事件。
如果返回了 true 則會接收并消費該事件。
如果返回 super.onTouchEvent(ev) 默認處理事件的邏輯和返回 false 時相同。

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

推薦閱讀更多精彩內容