觸摸事件的處理流程

愿每次回憶,對(duì)生活都不感到負(fù)疚。 — 郭小川

寫(xiě)在前面

用戶(hù)通過(guò)觸摸屏幕與手機(jī)交互,所以觸摸事件成為了交互中相對(duì)重要的因素。比如我們想要點(diǎn)外賣(mài),就要通過(guò)手指觸摸屏幕打開(kāi)外賣(mài)軟件,選擇喜愛(ài)的菜品下單等待外賣(mài)小哥送餐,然而在等待過(guò)程中,我們還可以查看當(dāng)前外賣(mài)的狀態(tài),這些行為我們都需要觸摸屏幕來(lái)完成。那么問(wèn)題來(lái)了,應(yīng)用軟件是如何知道手指觸摸的是哪一塊區(qū)域呢?

接下來(lái)讓我們帶著問(wèn)題來(lái)分析觸摸事件是如何處理的。

觸摸事件

當(dāng)觸摸事件產(chǎn)生時(shí),最先傳遞給Activity,那么我們就從Activity開(kāi)始剖析。

1.Activity分發(fā)事件
public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback, WindowControllerCallback,
        AutofillManager.AutofillClient {

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) { // 1
            return true;
        }
        return onTouchEvent(ev); // 2
    }
}

從注釋1處可以看到getWindow().superDispatchTouchEvent(ev)如果返回true,dispatchTouchEvent(MotionEvent ev)就直接返回true,代表該事件已經(jīng)被處理,Activity不處理該事件,否則會(huì)調(diào)用注釋2處的onTouchEvent(ev)方法處理事件,繼承自Activity的子類(lèi)可以重寫(xiě)onTouchEvent(ev)方法處理事件。

下面我們來(lái)看一下getWindow():

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback, WindowControllerCallback,
        AutofillManager.AutofillClient {

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        .......
    }

    /**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
    public Window getWindow() {
        return mWindow;
    }
}

從以上代碼可以看出getWindow()返回的是mWindow,mWindow在attach()方法中通過(guò)new PhoneWindow(this, window, activityConfigCallback)創(chuàng)建的,Window是抽象類(lèi),PhoneWindow是Window的實(shí)現(xiàn)類(lèi),所以getWindow().superDispatchTouchEvent(ev)實(shí)則是調(diào)用的PhoneWindow的superDispatchTouchEvent(ev)方法。

2.PhoneWindow分發(fā)事件
public class PhoneWindow extends Window implements MenuBuilder.Callback {

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

    public PhoneWindow(Context context, Window preservedWindow,
            ActivityConfigCallback activityConfigCallback) {
        this(context);
        // Only main activity windows use decor context, all the other windows depend on whatever
        // context that was given to them.
        mUseDecorContext = true;
        if (preservedWindow != null) {
            mDecor = (DecorView) preservedWindow.getDecorView(); // 1
            mElevation = preservedWindow.getElevation();
            mLoadElevation = false;
            mForceDecorInstall = true;
            // If we're preserving window, carry over the app token from the preserved
            // window, as we'll be skipping the addView in handleResumeActivity(), and
            // the token will not be updated as for a new window.
            getAttributes().token = preservedWindow.getAttributes().token;
        }
        // Even though the device doesn't support picture-in-picture mode,
        // an user can force using it through developer options.
        boolean forceResizable = Settings.Global.getInt(context.getContentResolver(),
                DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
        mSupportsPictureInPicture = forceResizable || context.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_PICTURE_IN_PICTURE);
        mActivityConfigCallback = activityConfigCallback;
    }

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

從superDispatchTouchEvent(MotionEvent event)中可以看到,調(diào)用了mDecor.superDispatchTouchEvent(event),mDecor是在PhoneWindow的構(gòu)造函數(shù)中通過(guò)preservedWindow.getDecorView()初始化的,詳見(jiàn)注釋1。

3.DecorView分發(fā)事件
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

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

從superDispatchTouchEvent(MotionEvent event)中可以看到,調(diào)用了super.dispatchTouchEvent(event),DecorView繼承自FrameLayout,F(xiàn)rameLayout又繼承自ViewGroup,所以super.dispatchTouchEvent(event)實(shí)則是調(diào)用了ViewGroup的dispatchTouchEvent(event)方法。

4.ViewGroup分發(fā)事件
public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    @Override
    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) { // 1
                /**
                 * 當(dāng)手指按下時(shí)重置所有狀態(tài),為之后的除了down之外的一系列事件做準(zhǔn)備
                 */
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            /**
             * 當(dāng)觸摸事件為down或者觸摸事件不為down且mFirstTouchTarget不為空的情況下,
             * 去檢查是否需要攔截事件。
             */
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) { // 2
                /**
                 * 此處判斷當(dāng)前父控件是否不干預(yù)攔截事件,默認(rèn)為false,
                 * 會(huì)調(diào)用onInterceptTouchEvent(ev)判斷是否攔截事件。
                 * 一旦disallowIntercept等于true,就會(huì)直接走else,不攔截事件,
                 * 也就不會(huì)調(diào)用onInterceptTouchEvent(ev)判斷是否攔截事件。
                 *
                 * 子類(lèi)可以通過(guò)requestDisallowInterceptTouchEvent()方法設(shè)置mGroupFlags。
                 *
                 * onInterceptTouchEvent(ev)默認(rèn)返回false不攔截事件,
                 * 繼承自ViewGroup的子類(lèi)可以重寫(xiě)該方法決定是否攔截事件,
                 * 如果子控件調(diào)用了父控件的requestDisallowInterceptTouchEvent()方法,
                 * 父控件就不會(huì)調(diào)用onInterceptTouchEvent(ev)方法了,
                 * 此時(shí)的onInterceptTouchEvent(ev)是無(wú)效的。
                 *
                 * 如果攔截事件,intercepted會(huì)置為true,
                 * 否則不攔截事件,intercepted會(huì)置為false。
                 */
                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);
            }

            /**
             * 檢查事件是否已經(jīng)cancel
             */
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL; // 3

            // 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;
            /**
             * 事件沒(méi)有被取消也沒(méi)有被攔截
             */
            if (!canceled && !intercepted) { // 4

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

                    /**
                     * newTouchTarget為null并且有子控件就會(huì)去遍歷子控件
                     */
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) { // 5
                        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--) { // 6
                            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)) { // 7
                                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)) { // 8
                                // 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;
    }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
}

從注釋1處可以看到當(dāng)觸摸事件為down(手指按下)時(shí)會(huì)重置所有狀態(tài),為之后的一系列事件做準(zhǔn)備。在注釋2處可以看到當(dāng)觸摸事件為down或者觸摸事件不為down且mFirstTouchTarget不為null的情況下去檢查是否需要攔截事件。注釋3處檢查事件是否已經(jīng)cancel。注釋4處可以看到,事件沒(méi)有被取消也沒(méi)有被攔截就會(huì)走到注釋5處。注釋5處newTouchTarget為null并且有子控件就會(huì)去遍歷子控件分發(fā)事件。注釋6處可以看到for循環(huán)遍歷子控件,判斷子控件是否能夠接收到觸摸事件,如果子控件能夠接收到觸摸事件,則交由子控件來(lái)處理。注釋7處會(huì)判斷當(dāng)前觸摸事件是否在子控件范圍內(nèi)或者子控件是否在播放動(dòng)畫(huà),如果觸摸事件沒(méi)有在子控件范圍內(nèi)或者子控件正在播放動(dòng)畫(huà),則會(huì)開(kāi)始遍歷下一個(gè)子控件,否則會(huì)調(diào)用注釋8處的dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)方法,該方法做了什么之后再講。最后來(lái)看返回值handled,如果handled返回true表示該事件已經(jīng)得到處理。

下面看一下dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)方法:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    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) { // 1
                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;
    }
}

這段代碼我們只看第一個(gè)if,從注釋1處可以看到如果子控件為null,則會(huì)調(diào)用super.dispatchTouchEvent(event),否則會(huì)調(diào)用child.dispatchTouchEvent(event),也就是向子控件分發(fā)事件,最后將結(jié)果handled返回。因?yàn)閂iewGroup繼承自View,
所以super.dispatchTouchEvent(event)實(shí)則是調(diào)用的View的dispatchTouchEvent(event)。

5.View分發(fā)事件
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }

            /**
             * ListenrInfo是一個(gè)用來(lái)存放事件監(jiān)聽(tīng)器的靜態(tài)類(lèi)。
             * mOnTouchLisenter就是我們調(diào)用View的setOnTouchListener(listener)方法設(shè)置的監(jiān)聽(tīng)器。
             * 如果mOnTouchListener不為null并且當(dāng)前控件是enabled狀態(tài),
             * 就會(huì)調(diào)用mOnTouchListener.onTouch(this, event)方法,如果返回true,
             * 表示這個(gè)事件已經(jīng)被處理了,就不會(huì)調(diào)用onTouchEvent(event)去處理事件,
             * 由此證明了OnTouchListener的onTouch()比onTouchEvent(event)優(yōu)先級(jí)高。
             */
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) { // 1
                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
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }
}

從注釋1處可以看到,如果mOnTouchListener不為null并且當(dāng)前控件是enabled狀態(tài),就會(huì)調(diào)用mOnTouchListener.onTouch(this, event)方法,如果結(jié)果返回true,表示這個(gè)事件已經(jīng)被處理了,就不會(huì)調(diào)用onTouchEvent(event)去處理事件,由此證明了OnTouchListener的onTouch()比onTouchEvent(event)優(yōu)先級(jí)高。如果mOnTouchListener.onTouch(this, event)方法結(jié)果返回false就會(huì)調(diào)用onTouchEvent(evnet)方法處理事件。

6.View處理事件
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {

    /**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        /**
         * CLICKABLE代表點(diǎn)擊
         * LONG_CLICKABLE代表長(zhǎng)按點(diǎn)擊
         * 可以通過(guò)setClickable()和setLongClickable()設(shè)置,
         * 在調(diào)用setOnClickListener()和setOnLongClickListener()時(shí)會(huì)自動(dòng)設(shè)置為CLICKABLE和LONG_CLICKABLE
         * 只要CLICKABLE和LONG_CLICKABLE有一個(gè)成立,也就是setOnClickListener或者setOnLongClickListener了,
         * clickable就會(huì)為true。
         */
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 1

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

        /**
         * clickable為true就會(huì)處理點(diǎn)擊事件或者長(zhǎng)按事件
         */
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { // 2
            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);
                        }

                       /**
                         * 在up事件發(fā)生時(shí),也就是手指抬起時(shí),
                         * 如果mHasPerformedLongPress的值為false并且mIgnoreNextUpEvent的值也為false,
                         * 就會(huì)調(diào)用performClickInternal()處理點(diǎn)擊事件。
                         */
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 3
                            // 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(); // 4
                                }
                            }
                        }

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

            return true;
        }

        return false;
    }

    private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

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

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

    public boolean performLongClick(float x, float y) {
        mLongClickX = x;
        mLongClickY = y;
        final boolean handled = performLongClick();
        mLongClickX = Float.NaN;
        mLongClickY = Float.NaN;
        return handled;
    }

    public boolean performLongClick() {
        return performLongClickInternal(mLongClickX, mLongClickY);
    }

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

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

}

注釋1,2,3處都加了解釋?zhuān)梢栽诖a中直接看。

這里主要介紹點(diǎn)擊事件和長(zhǎng)按事件是如何處理的:

首先我們來(lái)看注解5處,當(dāng)down事件產(chǎn)生時(shí),就會(huì)去檢查長(zhǎng)按事件調(diào)用checkForLongClick(0, x, y),傳入?yún)?shù)x,y的坐標(biāo),在該方法內(nèi)部會(huì)調(diào)用postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset)方法,mPendingCheckForLongPress是Runnable的實(shí)現(xiàn)類(lèi),ViewConfiguration.getLongPressTimeout() - delayOffset表示長(zhǎng)按事件觸發(fā)的時(shí)間。接著來(lái)看mPendingCheckForLongPress內(nèi)部run()方法中調(diào)用了performLongClick(x,y)方法,performLongClick(x,y)內(nèi)部調(diào)用了無(wú)參數(shù)的performLongClick()方法,無(wú)參數(shù)的performLongClick()內(nèi)部又調(diào)用了performLongClickInternal(x, y)方法,performLongClickInternal(x, y)內(nèi)部就會(huì)去判斷是否設(shè)置了長(zhǎng)按事件監(jiān)聽(tīng)器,如果設(shè)置了長(zhǎng)按事件監(jiān)聽(tīng)器,則調(diào)用mOnLongClickListener.onLongClick(View.this)并返回結(jié)果賦值給handled,performLongClickInternal(x, y)方法最終會(huì)將handled的值返回給performLongClick(x,y)方法,如果返回結(jié)果為true,表示該事件已經(jīng)被長(zhǎng)按處理,mHasPerformedLongPress會(huì)賦值為true,這個(gè)值在up事件產(chǎn)生時(shí)會(huì)用到。

長(zhǎng)按事件調(diào)用流程:checkForLongClick(0,x,y) >>> postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset) >>> performLongClick(x,y) >>> performLongClickInternal(x, y) >>> mOnLongClickListener.onLongClick(View.this)

現(xiàn)在來(lái)看注解4處,當(dāng)up事件產(chǎn)生時(shí),如果mHasPerformedLongPress的值為false并且mIgnoreNextUpEvent的值也為false, 就會(huì)調(diào)用performClickInternal()處理點(diǎn)擊事件。performClickInternal()內(nèi)部會(huì)調(diào)用performClick()方法,performClick()內(nèi)部回去判斷是否設(shè)置了點(diǎn)擊事件監(jiān)聽(tīng)器,如果設(shè)置了點(diǎn)擊事件監(jiān)聽(tīng)器,則調(diào)用mOnClickListener.onClick(this)方法處理,并將處理結(jié)果返回。

短按事件調(diào)用流程:performClickInternal() >>> performClick() >>> mOnClickListener.onClick(this)

總結(jié)

方法 Activity ViewGroup View
dispatchTouchEvent()
onInterceptTouchEvnet() 無(wú) 無(wú)
onTouchEvnet()

onInterceptTouchEvent()和onTouchEvnet()都在dispatchTouchEvent()中調(diào)用。

這里使用一個(gè)例子來(lái)了解觸摸事件的處理流程:我所在的項(xiàng)目來(lái)了一個(gè)需求,項(xiàng)目經(jīng)理肯定不會(huì)去動(dòng)代碼呀,所以項(xiàng)目經(jīng)理把這個(gè)需求分發(fā)給了我的Leader,而我的Leader還有別的事情要忙,所以這個(gè)需求又給了我,我看了一眼這個(gè)需求不合理,我又把這個(gè)需求還給了Leader,Leader聽(tīng)我說(shuō)完后也認(rèn)同需求不合理,所以這個(gè)需求又回到了項(xiàng)目經(jīng)理手里,最終這個(gè)需求沒(méi)有處理。

在這個(gè)例子里,項(xiàng)目經(jīng)理就是Activity,Leader手底下不止我一個(gè)開(kāi)發(fā),所以Leader就是ViewGroup,我就是View。從項(xiàng)目經(jīng)理分發(fā)需求給Leader,Leader將需求又分發(fā)給我,我又將需求打回給Leader,Leader將需求打回給項(xiàng)目經(jīng)理,中間傳遞的過(guò)程就是dispatchTouchEvent(),誰(shuí)都不處理就會(huì)返回false;如果需求在分發(fā)給Leader時(shí),Leader覺(jué)得他就可以處理,就會(huì)在dispatchTouchEvnet()中調(diào)用onInterceptToucheEvent()攔截事件,調(diào)用Leader的onTouchEvnet()處理事件,并在dispatchTouchEvent()返回true告訴項(xiàng)目經(jīng)理可以做;如果我處理了這個(gè)需求,就會(huì)調(diào)用我的onTouchEvent()處理事件,在dispatchTouchEvent()返回true告訴Leader我可以做。

通俗點(diǎn)說(shuō)吧,觸摸事件產(chǎn)生會(huì)一層一層的向下分發(fā),誰(shuí)有能力誰(shuí)就攔截該事件并處理,如果都不能處理,觸摸事件還會(huì)一層一層的向上傳遞,誰(shuí)有能力誰(shuí)就攔截該事件并處理,否則該事件就不會(huì)得到處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容