1. 程式人生 > >【Android】原始碼分析 - View事件分發機制

【Android】原始碼分析 - View事件分發機制

事件分發物件

(1)所有 Touch 事件都被封裝成了 MotionEvent 物件,包括 Touch 的位置、時間、歷史記錄以及第幾個手指(多指觸控)等。

(2)事件型別分為 ACTION_DOWNACTION_UPACTION_MOVEACTION_POINTER_DOWNACTION_POINTER_UPACTION_CANCEL,每個事件都是以 ACTION_DOWN 開始 ACTION_UP 結束。

主要發生的Touch事件有如下四種:

  • MotionEvent.ACTION_DOWN:按下View(所有事件的開始)
  • MotionEvent.ACTION_MOVE:滑動View
  • MotionEvent.ACTION_CANCEL:非人為原因結束本次事件
  • MotionEvent.ACTION_UP:擡起View(與DOWN對應)

事件列:從手指接觸螢幕至手指離開螢幕,這個過程產生的一系列事件
任何事件列都是以DOWN事件開始,UP事件結束,中間有無數的MOVE事件,如下圖:

即當一個點選事件發生後,系統需要將這個事件傳遞給一個具體的View去處理。這個事件傳遞的過程就是分發過程。

(3)對事件的處理包括三類,分別:

  • 傳遞——dispatchTouchEvent()函式;

  • 攔截——onInterceptTouchEvent()函式

  • 消費——onTouchEvent()函式和 OnTouchListener

原始碼跟蹤

觸控事件發生後,在Activity內最先接收到事件的是Activity自身的dispatchTouchEven接著Window傳遞給最頂端的View,也就是DecorView。接下來才是我們熟悉的觸控事件流程:首先是最頂端的ViewGroup(這邊便是DecorView)的dispatchTouchEvent接收到事件。並通過onInterceptTouchEvent判斷是否需要攔截。如果攔截則分配到ViewGroup自身的onTouchEvent,如果不攔截則查詢位於點選區域的子View(當事件是ACTION_DOWN的時候,會做一次查詢並根據查詢到的子View設定一個TouchTarget,有了TouchTarget以後,後續的對應id的事件如果不被攔截都會分發給這一個TouchTarget)。查詢到子View以後則呼叫dispatchTransformedTouchEvent把MotionEvent的座標轉換到子View的座標空間,這不僅僅是x,y的偏移,還包括根據子View自身矩陣的逆矩陣對座標進行變換(這就是使用setTranslationX,setScaleX等方法呼叫後,子View的點選區域還能保持和自身繪製內容一致的原因。使用Animation做變換點選區域不同步是因為Animation使用的是Canvas的矩陣而不是View自身的矩陣來做變換)。

事件分發的源頭

觸控事件發生後,在Activity內最先接收到事件的是Activity自身的dispatchTouchEvent(),然後Activity傳遞給Activity的Window:

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

其中的這個getWindow()得到的就是Activity的mWindow物件,它是在attach()方法中初始化的:

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, IVoiceInteractor voiceInteractor) {

    //建立一個Window物件        
    mWindow = PolicyManager.makeNewWindow(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);


    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    mWindowManager = mWindow.getWindowManager();

    //...省略其他程式碼...
}

呼叫了PolicyManager的makeNewWindow()方法建立的Window物件。我們跟進去PolicyManager這個類(這個類在Android 6.0之後原始碼中刪除了,下面是我找的5.1的原始碼):

public final class PolicyManager {
    private static final String POLICY_IMPL_CLASS_NAME =
        "com.android.internal.policy.impl.Policy";

    private static final IPolicy sPolicy;

    static {
        // Pull in the actual implementation of the policy at run-time
        try {
            Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
            sPolicy = (IPolicy)policyClass.newInstance();
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
        } catch (InstantiationException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
        }
    }

    // Cannot instantiate this class
    private PolicyManager() {}

    // The static methods to spawn new policy-specific objects
    public static Window makeNewWindow(Context context) {
        return sPolicy.makeNewWindow(context);
    }

    public static LayoutInflater makeNewLayoutInflater(Context context) {
        return sPolicy.makeNewLayoutInflater(context);
    }

    public static WindowManagerPolicy makeNewWindowManager() {
        return sPolicy.makeNewWindowManager();
    }

    public static FallbackEventHandler makeNewFallbackEventHandler(Context context) {
        return sPolicy.makeNewFallbackEventHandler(context);
    }
}

可以看到實際上呼叫了Policy類的makeNewWindow()方法:

public class Policy implements IPolicy {
    private static final String TAG = "PhonePolicy";

    private static final String[] preload_classes = {
        "com.android.internal.policy.impl.PhoneLayoutInflater",
        "com.android.internal.policy.impl.PhoneWindow",
        "com.android.internal.policy.impl.PhoneWindow$1",
        "com.android.internal.policy.impl.PhoneWindow$DialogMenuCallback",
        "com.android.internal.policy.impl.PhoneWindow$DecorView",
        "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState",
        "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState$SavedState",
    };

    static {
        // For performance reasons, preload some policy specific classes when
        // the policy gets loaded.
        for (String s : preload_classes) {
            try {
                Class.forName(s);
            } catch (ClassNotFoundException ex) {
                Log.e(TAG, "Could not preload class for phone policy: " + s);
            }
        }
    }

    public Window makeNewWindow(Context context) {
        return new PhoneWindow(context);
    }

    public LayoutInflater makeNewLayoutInflater(Context context) {
        return new PhoneLayoutInflater(context);
    }

    public WindowManagerPolicy makeNewWindowManager() {
        return new PhoneWindowManager();
    }

    public FallbackEventHandler makeNewFallbackEventHandler(Context context) {
        return new PhoneFallbackEventHandler(context);
    }
}

原來是一個PhoneWindow物件,我們趕緊看看它的superDispatchTouchEvent方法,原來是繼續呼叫了DecorViewsuperDispatchTouchEvent()方法:

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


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

這個DecorViewPhoneWindow的一個內部類,它繼承了FrameLayout:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

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

    //...省略其他程式碼... 
}

而FrameLayout本身沒有實現dispatchTouchEvent()這個方法,它繼承了ViewGroup:

public class FrameLayout extends ViewGroup {...}

下面我們來看一下ViewGroup的dispatchTouchEvent()方法原始碼。

ViewGroup開始分發

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

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

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 觸控事件流開始,重置觸控相關的狀態
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 關鍵點1: 檢測當前是否需要攔截事件
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // 處理呼叫requestDisallowInterceptTouchEvent()來決定是否允許ViewGroup攔截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); 
            } else {
                intercepted = false;
            }
        } else {
            // 當前沒有TouchTarget也不是事件流的起始的話,則直接預設攔截,不通過onInterceptTouchEvent判斷。
            intercepted = true;
        }

        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // 檢測是否需要把多點觸控事件分配給不同的子View
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

        // 當前事件流對應的TouchTarget物件
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {

            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;

                // 當前事件是事件流的初始事件(包括多點觸控時第二、第三點燈的DOWN事件),清除之前相應的TouchTarget的狀態
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;

                    //通過for迴圈,遍歷了當前ViewGroup下的所有子View
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        // 關鍵點2: 判斷當前遍歷到的子View能否接受事件,如果不能則直接continue進入下一次迴圈
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // 當前子View能接收事件,為子View建立TouchTarget
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        // 呼叫dispatchTransformedTouchEvent把事件分配給子View
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();

                            // 把TouchTarget新增到TouchTarget列表的第一位
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        if (mFirstTouchTarget == null) {
            // 目前沒有任何TouchTarget,所以直接傳null給dispatchTransformedTouchEvent
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 把事件根據pointer id分發給TouchTarget列表內的所有TouchTarget,用來處理多點觸控的情況
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            // 遍歷TouchTarget列表
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;

                    // 根據TouchTarget的pointerIdBits來執行dispatchTransformedTouchEvent
                    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;
            }
        }

        // 處理CANCEL和UP事件的情況
        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;
}

上面的程式碼比較長,先不用細看。下面一張圖來簡化對照著理解一下:

  • 關鍵點1:只有ACTION_DOWN事件或者mFirstTouchTarget為空時,並且沒有呼叫過requestDisallowInterceptTouchEvent()去阻止該ViewGroup攔截事件的話,才可能執行攔截方法onInterceptTouchEvent()

  • 關鍵點2:判斷當前遍歷到的子View能否接受事件主要由兩點來衡量:子元素是否在播動畫(canViewReceivePointerEvents()方法);點選事件座標是否落在子元素區域內(“)。

//子元素是否在播動畫
private static boolean canViewReceivePointerEvents(View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}
//點選事件座標是否落在子元素區域內
protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
    float localX = x + mScrollX - child.mLeft;
    float localY = y + mScrollY - child.mTop;
    if (! child.hasIdentityMatrix() && mAttachInfo != null) {
        final float[] localXY = mAttachInfo.mTmpTransformLocation;
        localXY[0] = localX;
        localXY[1] = localY;
        child.getInverseMatrix().mapPoints(localXY);
        localX = localXY[0];
        localY = localXY[1];
    }

    //檢測座標是否在child區域內
    final boolean isInView = child.pointInView(localX, localY);
    if (isInView && outLocalPoint != null) {
        outLocalPoint.set(localX, localY);
    }
    return isInView;
}

當子View滿足這兩個條件之後,ViewGroup就會呼叫dispatchTransformedMotionEvent()方法去交給子元素處理:

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

    final int oldAction = event.getAction();
    // 處理CANCEL的情況,直接把MotionEvent的原始資料分發給子View或者自身的onTouchEvent
        // (這邊呼叫View.dispatchTouchEvent,而View.dispatchTouchEvent會再呼叫onTouchEvent方法,把MotionEvent傳入)
    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;
    }

    // 對MotionEvent自身的pointer id和當前我們需要處理的pointer id做按位與,得到共有的pointer id
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // 沒有pointer id需要處理,直接返回
    if (newPointerIdBits == 0) {
        return false;
    }

    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                // 關鍵點1: 子View為空,直接交還給自身的onTouchEvent處理
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                // 關鍵點2:交給子view的dispatchTouchEvent()方法去處理
                handled = child.dispatchTouchEvent(event);
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        // MotionEvent自身的pointer id和當前需要處理的pointer id不同,把不需要處理的pointer id相關的資訊剔除掉。
        transformedEvent = event.split(newPointerIdBits);
    }

    if (child == null) {
        // 子View為空,直接交還給自身的onTouchEvent處理
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        // 根據當前的scrollX、scrollY和子View的left、top對MotionEvent的觸控座標x、y進行偏移
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            // 獲取子View自身矩陣的逆矩陣,並對MotionEvent的座標相關資訊進行矩陣變換
            transformedEvent.transform(child.getInverseMatrix());
        }

        // 把經過偏移以及矩陣變換的事件傳遞給子View處理
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    transformedEvent.recycle();
    return handled;
}

子View消費事件

然後我們看看View的dispatchTouchEvent()方法:

public boolean dispatchTouchEvent(MotionEvent event) {
    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)) {
        // 如果存在mOnTouchListener,直接交給它消費Touch事件
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 交給onTouchEvent()方法消費Touch事件
        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;
}

注意這裡View的mOnTouchListener.onTouch(this, event)onTouchEvent(event)都是放在if判斷條件裡的,也就是說他們的返回值會影響事件是否繼續往下傳遞。如果mOnTouchListener.onTouch(this, event)返回true的話,就不會再執行此子View的onTouchEvent(event)方法了。

最後我們再看下View的onTouchEvent()方法是如何消費事件的呢?

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

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == 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));
    }

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

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            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) {
                        // 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();
                }
                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();
                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;
}

我們這裡只注意一下在這個View接收到ACTION_UP事件之後,會呼叫到performClick()方法:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        //通知回撥mOnClickListener的onClick方法
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

這裡能說明View的OnClickListeneronClick()事件的執行時機是在整個TouchEvent事件列的最後才會執行。

Touch案例分析

問題:當ViewGroup的onInterceptTouchEvent()函式分別返回true和false時,這個ViewGroup和View1分別能接收到DOWN、MOVE、UP中的什麼事件?

ViewGroup的onInterceptTouchEvent()方法 ViewGroup View1
return true 僅能接收到DOWN事件 什麼都接收不到
return false 三種都能接收到 三種都能接收到

另一個案例可以參考這篇文章:Android 程式設計下 Touch 事件的分發和消費機制

總結

  • (1)Touch事件分發中只有兩個主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三個相關事件。View包含dispatchTouchEvent、onTouchEvent兩個相關事件。其中ViewGroup又繼承於View。

  • (2)事件從 Activity.dispatchTouchEvent()開始傳遞,只要沒有被停止或攔截,從最上層的 ViewGroup開始一直往下(子View)傳遞。子View可以通過 onTouchEvent()對事件進行處理。

  • (3)事件由ViewGroup傳遞給子 View,ViewGroup 可以通過 onInterceptTouchEvent()對事件做攔截,停止其往下傳遞。

  • (4)如果事件從上往下傳遞過程中一直沒有被停止,且最底層子 View 沒有消費事件,事件會反向往上傳遞,這時父 View(ViewGroup)可以進行消費,如果還是沒有被消費的話,最後會到 Activity 的 onTouchEvent()函式。

  • (5) 如果 View 沒有對 ACTION_DOWN 進行消費,之後的其他事件不會傳遞過來。

  • (6)OnTouchListener 優先於 onTouchEvent()對事件進行消費。

  • (7)當Acitivty接收到Touch事件時,將遍歷子View進行Down事件的分發。ViewGroup的遍歷可以看成是遞迴的。分發的目的是為了找到第一個真正要處理本次完整觸控事件的View,這個View會在onTouchuEvent結果返回true。

  • (8)當某個子View返回true時,會中止Down事件的分發,同時在ViewGroup中記錄該子View。接下去的Move和Up事件將由該子View直接進行處理。由於子View是儲存在ViewGroup中的,多層ViewGroup的節點結構時,上級ViewGroup儲存的會是真實處理事件的View所在的ViewGroup物件:如ViewGroup0-ViewGroup1-TextView的結構中,TextView返回了true,它將被儲存在ViewGroup1中,而ViewGroup1也會返回true,被儲存在ViewGroup0中。當Move和UP事件來時,會先從ViewGroup0傳遞至ViewGroup1,再由ViewGroup1傳遞至TextView。

  • (9)當ViewGroup中所有子View都不捕獲Down事件時,將觸發ViewGroup自身的onTouch事件。觸發的方式是呼叫super.dispatchTouchEvent函式,即父類View的dispatchTouchEvent方法。在所有子View都不處理的情況下,觸發Acitivity的onTouchEvent方法。

  • (10)ViewGroup預設不攔截任何事件。原始碼中的ViewGroup的onInterceptTouchEvent()方法預設返回false。

  • (11)View沒有onInterceptTouchEvent()方法。一旦點選事件傳遞給它,就會呼叫它的onTouchEvent方法

  • (12)我們可以發現ViewGroup沒有onTouchEvent事件,說明他的處理邏輯和View是一樣的。

  • (13)子view如果消耗了事件,那麼ViewGroup就不會在接受到事件了。

參考資料