Android 事件分發機制原始碼解析

allenfeng發表於2017-03-01

觸控事件傳遞機制是Android中一塊比較重要的知識體系,瞭解並熟悉整套的傳遞機制有助於更好的分析各種滑動衝突、滑動失效問題,更好去擴充套件控制元件的事件功能和開發自定義控制元件。

預備知識

MotionEvent

在Android裝置中,觸控事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和雙擊,另外還包括單指操作和多指操作等。一個最簡單的使用者觸控事件一般經過以下幾個流程:

  • 手指按下
  • 手指滑動
  • 手指抬起

Android把這些事件的每一步抽象為MotionEvent這一概念,MotionEvent包含了觸控的座標位置,點按的數量(手指的數量),時間點等資訊,用於描述使用者當前的具體動作,常見的MotionEvent有下面幾種型別:

  • ACTION_DOWN
  • ACTION_UP
  • ACTION_MOVE
  • ACTION_CANCEL

其中,ACTION_DOWNACTION_MOVEACTION_UP就分別對應於上面的手指按下、手指滑動、手指抬起操作,即一個最簡單的使用者操作包含了一個ACTION_DOWN事件,若干個ACTION_MOVE事件和一個ACTION_UP事件。

幾個方法

事件分發過程中,涉及的主要方法有以下幾個:

  • dispatchTouchEvent: 用於事件的分發,所有的事件都要通過此方法進行分發,決定是自己對事件進行消費還是交由子View處理
  • onTouchEvent: 主要用於事件的處理,返回true表示消費當前事件
  • onInterceptTouchEvent: 是ViewGroup中獨有的方法,若返回true表示攔截當前事件,交由自己的onTouchEvent()進行處理,返回false表示不攔截

我們的原始碼分析也主要圍繞這幾個方法展開。

原始碼分析

Activity

我們從Activity的dispatchTouchEvent方法作為入口進行分析:

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

這個方法首先會判斷當前觸控事件的型別,如果是ACTION_DOWN事件,會觸發onUserInteraction方法。根據文件註釋,當有任意一個按鍵、觸屏或者軌跡球事件發生時,棧頂Activity的onUserInteraction會被觸發。如果我們需要知道使用者是不是正在和裝置互動,可以在子類中重寫這個方法,去獲取通知(比如取消屏保這個場景)。

然後是呼叫Activity內部mWindowsuperDispatchTouchEvent方法,mWindow其實是PhoneWindow的例項,我們看看這個方法做了什麼:

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ...
	@Override
	public boolean superDispatchTouchEvent(MotionEvent event) {
	    return mDecor.superDispatchTouchEvent(event);
	}
	private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

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

原來PhoneWindow內部呼叫了DecorView的同名方法,而DecorView其實是FrameLayout的子類,FrameLayout並沒有重寫dispatchTouchEvent方法,所以事件開始交由ViewGroup的dispatchTouchEvent開始分發了,這個方法將在下一節分析。

我們回到Activity的dispatchTouchEvent方法,注意當getWindow().superDispatchTouchEvent(ev)這一語句返回false時,即事件沒有被任何子View消費時,最終會執行Activity的onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

小結:
事件從Activity的dispatchTouchEvent開始,經由DecorView開始向下傳遞,交由子View處理,若事件未被任何Activity的子View處理,將由Activity自己處理。

ViewGroup

由上節分析可知,事件來到DecorView後,經過層層呼叫,來到了ViewGroup的dispatchTouchEvent方法中:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ... 
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        ...
        // 先檢驗事件是否需要被ViewGroup攔截
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            // 校驗是否給mGroupFlags設定了FLAG_DISALLOW_INTERCEPT標誌位
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
            	// 走onInterceptTouchEvent判斷是否攔截事件
                intercepted = onInterceptTouchEvent(ev);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        ...
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        if (!canceled && !intercepted) {
        	// 注意ACTION_DOWN等事件才會走遍歷所有子View的流程
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ...
                // 開始遍歷所有子View開始逐個分發事件
                final int childrenCount = mChildrenCount;
                if (childrenCount != 0) {
                    for (int i = childrenCount - 1; i >= 0; i--) {
                    	// 判斷觸控點是否在這個View的內部
                        final View child = children[i];
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }
                        ...
                        // 事件被子View消費,退出迴圈,不再繼續分發給其他子View
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

                            ...
                            // addTouchTarget內部將mFirstTouchTarget設定為child,即不為null
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
            }
        }
        // 事件未被任何子View消費,自己處理
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 將MotionEvent.ACTION_DOWN後續事件分發給mFirstTouchTarget指向的View
            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;
                    }
                    ...
                }
                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);
        }
    }
    return handled;
}
private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
private void clearTouchTargets() {
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        do {
            TouchTarget next = target.next;
            target.recycle();
            target = next;
        } while (target != null);
        mFirstTouchTarget = null;
    }
}
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...

        // 注意傳參child為null時,呼叫的是自己的dispatchTouchEvent
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 預設不攔截事件
    return false;
}

這個方法比較長,只要把握住主要脈絡,修枝剪葉後還是非常清晰的:

(1) 判斷事件是夠需要被ViewGroup攔截

首先會根據mGroupFlags判斷是否可以執行onInterceptTouchEvent方法,它的值可以通過requestDisallowInterceptTouchEvent方法設定:

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) {
        // 層層向上傳遞,告知所有父View不攔截事件
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

所以我們在處理某些滑動衝突場景時,可以從子View中呼叫父View的requestDisallowInterceptTouchEvent方法,阻止父View攔截事件。

如果view沒有設定FLAG_DISALLOW_INTERCEPT,就可以進入onInterceptTouchEvent方法,判斷是否應該被自己攔截,
ViewGroup的onInterceptTouchEvent直接返回了false,即預設是不攔截事件的,ViewGroup的子類可以重寫這個方法,內部判斷攔截邏輯。

注意:只有當事件型別是ACTION_DOWN或者mFirstTouchTarget不為空時,才會走是否需要攔截事件這一判斷,如果事件是ACTION_DOWN的後續事件(如ACTION_MOVEACTION_UP等),且在傳遞ACTION_DOWN事件過程中沒有找到目標子View時,事件將會直接被攔截,交給ViewGroup自己處理。mFirstTouchTarget的賦值會在下一節提到。

(2) 遍歷所有子View,逐個分發事件:

執行遍歷分發的條件是:當前事件是ACTION_DOWNACTION_POINTER_DOWN或者ACTION_HOVER_MOVE三種型別中的一個(後兩種用的比較少,暫且忽略)。所以,如果事件是ACTION_DOWN的後續事件,如ACTION_UP事件,將不會進入遍歷流程!

進入遍歷流程後,拿到一個子View,首先會判斷觸控點是不是在子View範圍內,如果不是直接跳過該子View;
否則通過dispatchTransformedTouchEvent方法,間接呼叫child.dispatchTouchEvent達到傳遞的目的;

如果dispatchTransformedTouchEvent返回true,即事件被子View消費,就會把mFirstTouchTarget設定為child,即不為null,並將alreadyDispatchedToNewTouchTarget設定為true,然後跳出迴圈,事件不再繼續傳遞給其他子View。

可以理解為,這一步的主要作用是,在事件的開始,即傳遞ACTION_DOWN事件過程中,找到一個需要消費事件的子View,我們可以稱之為目標子View,執行第一次事件傳遞,並把mFirstTouchTarget設定為這個目標子View

(3) 將事件交給ViewGroup自己或者目標子View處理

經過上面一步後,如果mFirstTouchTarget仍然為空,說明沒有任何一個子View消費事件,將同樣會呼叫dispatchTransformedTouchEvent,但此時這個方法的View child引數為null,所以呼叫的其實是super.dispatchTouchEvent(event),即事件交給ViewGroup自己處理。ViewGroup是View的子View,所以事件將會使用View的dispatchTouchEvent(event)方法判斷是否消費事件。

反之,如果mFirstTouchTarget不為null,說明上一次事件傳遞時,找到了需要處理事件的目標子View,此時,ACTION_DOWN的後續事件,如ACTION_UP等事件,都會傳遞至mFirstTouchTarget中儲存的目標子View中。這裡面還有一個小細節,如果在上一節遍歷過程中已經把本次事件傳遞給子View,alreadyDispatchedToNewTouchTarget的值會被設定為true,程式碼會判斷alreadyDispatchedToNewTouchTarget的值,避免做重複分發。

小結:
dispatchTouchEvent方法首先判斷事件是否需要被攔截,如果需要攔截會呼叫onInterceptTouchEvent,若該方法返回true,事件由ViewGroup自己處理,不在繼續傳遞。
若事件未被攔截,將先遍歷找出一個目標子View,後續事件也將交由目標子View處理。
若沒有目標子View,事件由ViewGroup自己處理。

此外,如果一個子View沒有消費ACTION_DOWN型別的事件,那麼事件將會被另一個子View或者ViewGroup自己消費,之後的事件都只會傳遞給目標子View(mFirstTouchTarget)或者ViewGroup自身。簡單來說,就是如果一個View沒有消費ACTION_DOWN事件,後續事件也不會傳遞進來。

View

現在回頭看上一節的第2、3步,不管是對子View分發事件,還是將事件分發給ViewGroup自身,最後都殊途同歸,呼叫到了View的dispatchTouchEvent,這就是我們這一節分析的目標。

public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        if (onFilterTouchEventForSecurity(event)) {
        	// 判斷事件是否先交給ouTouch方法處理
            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                    mOnTouchListener.onTouch(this, event)) {
                return true;
            }
            // onTouch未消費事件,傳給onTouchEvent
            if (onTouchEvent(event)) {
                return true;
            }
        }
        ...
        return false;
    }

程式碼量不多,主要做了三件事:

  1. 若View設定了OnTouchListener,且處於enable狀態時,會先呼叫mOnTouchListener的onTouch方法
  2. 若onTouch返回false,事件傳遞給onTouchEvent方法繼續處理
  3. 若最後onTouchEvent也沒有消費這個事件,將返回false,告知上層parent將事件給其他兄弟View

這樣,我們的分析轉到了View的onTouchEvent方法:

public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
            mPrivateFlags &= ~PRESSED;
            refreshDrawableState();
        }
        // 如果一個View處於DISABLED狀態,但是CLICKABLE或者LONG_CLICKABLE的話,這個View仍然能消費事件
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    ...
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {

                    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.
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                   }
                    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 |= PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    mPrivateFlags |= PRESSED;
                    refreshDrawableState();
                    checkForLongClick(0);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
                removeTapCallback();
                break;
            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();
                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();
                        // Need to switch from pressed to not pressed
                        mPrivateFlags &= ~PRESSED;
                        refreshDrawableState();
                    }
                }
                break;
        }
        return true;
    }
    return false;
}
public final boolean isFocusable() {
    return FOCUSABLE == (mViewFlags & FOCUSABLE_MASK);
}
public final boolean isFocusableInTouchMode() {
    return FOCUSABLE_IN_TOUCH_MODE == (mViewFlags & FOCUSABLE_IN_TOUCH_MODE);
}

onTouchEvent方法的主要流程如下:

  1. 如果一個View處於DISABLED狀態,但是CLICKABLE或者LONG_CLICKABLE的話,這個View仍然能消費事件,只是不會再走下面的流程;
  2. 如果View是enable的且處於可點選狀態,事件將被這個View消費:
    在方法返回前,onTouchEvent會根據MotionEvent的不同型別做出不同響應,如呼叫refreshDrawableState()去設定View的按下效果和抬起效果等。
    這裡我們主要關注ACTION_UP分支,這個分支內部經過重重判斷之後,會呼叫到performClick方法:
public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    if (mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        mOnClickListener.onClick(this);
        return true;
    }
    return false;
}

可以看到,如果設定了OnClickListener,就會回撥我們的onClick方法,最終消費事件

總結

通過上面的原始碼解析,我們可以總結出事件分發的整體流程:

事件傳遞流程

下面做一個總體概括:

事件由Activity的dispatchTouchEvent()開始,將事件傳遞給當前Activity的根ViewGroup:mDecorView,事件開始自上而下進行傳遞,直至被消費。

事件傳遞至ViewGroup時,呼叫dispatchTouchEvent()進行分發處理:

  1. 檢查送否應該對事件進行攔截:onInterceptTouchEvent(),若為true,跳過2步驟;
  2. 將事件依次分發給子View,若事件被某個View消費了,將不再繼續分發;
  3. 如果2中沒有子View對事件進行消費或者子View的數量為零,事件將由ViewGroup自己處理,處理流程和View的處理流程一致;

事件傳遞至ViewdispatchTouchEvent()時, 首先會判斷OnTouchListener是否存在,倘若存在,則執行onTouch(),若onTouch()未對事件進行消費,事件將繼續交由onTouchEvent處理,根據上面分析可知,View的onClick事件是在onTouchEventACTION_UP中觸發的,因此,onTouch事件優先於onClick事件。

若事件在自上而下的傳遞過程中一直沒有被消費,而且最底層的子View也沒有對其進行消費,事件會反向向上傳遞,此時,父ViewGroup可以對事件進行消費,若仍然沒有被消費的話,最後會回到Activity的onTouchEvent

如果一個子View沒有消費ACTION_DOWN型別的事件,那麼事件將會被另一個子View或者ViewGroup自己消費,之後的事件都只會傳遞給目標子View(mFirstTouchTarget)或者ViewGroup自身。簡單來說,就是如果一個View沒有消費ACTION_DOWN事件,後續事件也不會傳遞進來。

相關文章