Android事件分發機制源碼解析

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

預備知識

MotionEvent

在Android設備中,觸摸事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和雙擊,另外還包括單指操做和多指操做等。一個最簡單的用戶觸摸事件通常通過如下幾個流程:ide

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

Android把這些事件的每一步抽象爲MotionEvent這一律念,MotionEvent包含了觸摸的座標位置,點按的數量(手指的數量),時間點等信息,用於描述用戶當前的具體動做,常見的MotionEvent有下面幾種類型:源碼分析

  • ACTION_DOWN
  • ACTION_UP
  • ACTION_MOVE
  • ACTION_CANCEL

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

幾個方法

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

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

咱們的源碼分析也主要圍繞這幾個方法展開。this

源碼分析

Activity

咱們從Activity的dispatchTouchEvent方法做爲入口進行分析:spa

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會被觸發。若是咱們須要知道用戶是否是正在和設備交互,能夠在子類中重寫這個方法,去獲取通知(好比取消屏保這個場景)。code

而後是調用Activity內部mWindowsuperDispatchTouchEvent方法,mWindow實際上是PhoneWindow的實例,咱們看看這個方法作了什麼:orm

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開始分發了,這個方法將在下一節分析。cdn

咱們回到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

如今回頭看上一節的第二、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事件,後續事件也不會傳遞進來。

最後

若是你看到了這裏,以爲文章寫得不錯就給個讚唄?若是你以爲那裏值得改進的,請給我留言。必定會認真查詢,修正不足。謝謝。

相關文章
相關標籤/搜索