事件分發機制是android中的核心知識點和難點。相信不少人也和我同樣對於這點感到很是困惑。我看了不少篇博客和書面資料。今天咱們就聊聊事件的分發機制。前端
在瞭解點擊事件的傳遞規則以前,咱們首先要弄明白什麼事點擊事件(MotionEvent),所謂MotionEvent是指手指接觸屏幕後所產生的一系列事件。android
ACTION_DOWN————手指剛接觸屏幕。
ACTION_MOVE————手指在屏幕上移動。
ACYION_UP————手指從屏幕上鬆開的一瞬間。ios
點擊事件的分發過程就是MotionEvent
的分發過程,該過程主要由如下三個函數來完成:app
public boolean dispatchTouchEvent(MotionEvent ev)
ide
功能:用來進行事件的分發函數
public boolean onInterceptTouchEvent(MotionEvent ev)
源碼分析
功能:用來判斷是否攔截某個事件。post
public boolean onTouchEvent(MotionEvent ev)
學習
功能:處理點擊事件,在dispatchTouchEvent
中調用。返回結果表示是否消耗當前點擊事件。this
先不急咱們從最簡單的OnClickListener
來看,OnClickListener
的優先級最低,處於事件傳遞的尾端。
咱們首先簡單建立一個Android 項目,只有一個 Activity ,而且 Activity 中有一個按鈕。若是咱們想要給這個按鈕註冊一個點擊事件,只須要調用以下的代碼:
button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.e("TAG_紫霧凌寒","執行了onClick"); } });
這樣在onClick()
方法裏面寫咱們須要處理的業務邏輯,就能夠在按鈕被點擊的時候執行。再若是想給這個按鈕再添加一個 touch 事件,只須要調用以下所示的代碼:
button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.e("TAG_紫霧凌寒","執行了onTouch==Action="+event.getAction()); return false; } });
咱們僅僅憑 Touch[觸摸] 和 Click[點擊] 就可以猜測到onTouch()
方法裏能作的事情比onClick()
要多一些,好比判斷手指按下、擡起、移動等事件。那麼我同時給 button 兩個事件都註冊了,哪個會先執行呢?咱們用事實說話,運行程序點擊按鈕,咱們會發現打印結果以下:
這裏咱們能夠看到,onTouch()
是優先於onClick()
執行的,而且根據日誌能夠看到onTouch()
執行了兩次,一次是 ACTION_DOWN ,一次是 ACTION_UP (當你手指按下屏幕並在屏幕上滑動時,還會有屢次 ACTION_MOVE 的執行)。所以事件傳遞的順序是先通過onTouch()
,再傳遞到onClick()
。
有些同窗可能已經注意到,onTouch()
方法是有返回值的,這裏咱們返回的是 false 。若是咱們嘗試把onTouch()
方法裏的返回值改爲 true ,再運行一次,結果以下:
咱們發現,onClick()
方法再也不執行了!那爲何會這樣呢?具體的緣由看完這篇文章你們就明白了,這裏咱們能夠先理解成onTouch()
方法返回 true 就認爲這個事件被onTouch()
消費了,於是不會再繼續向下傳遞。
若是讀到這裏,以上全部的知識點你都清楚,那麼說明你對 Android 事件傳遞算是入門了。
下面咱們繼續接着往下看,咱們經過源碼的角度來分析如下。
首先咱們要知道,當咱們手指觸摸屏幕上的控件後,接下來確定會調用它的dispatchTouchEvent
方法。咱們根據下面一張圖來分析
當咱們手指點擊屏幕上的 button 時,就會去調用 button 的dispatchTouchEvent
方法,這時候會發現button 裏面沒有這個方法,那麼它就會繼續向上查找它的父類 TextView 的dispatchTouchEvent
方法,若是沒有仍是繼續向上查找,直到找到 View 中會發現這裏有dispatchTouchEvent
方法。
下面咱們根據源碼來看看,事件到底是如何傳遞的?首先咱們仍是來看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) { // 若是是Down中止滾動 stopNestedScroll(); } //重要的代碼就是這裏 if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } /***********省略部分代碼******************/ return result; }
這裏咱們首先看到它定義了一個變量 result
,它的默認值是false,僅接着就去調用了onFilterTouchEventForSecurity(event)
這個方法,這個方法主要做用就是判斷該觸摸事件要不要分發,咱們下面來看下這個方法。
public boolean onFilterTouchEventForSecurity(MotionEvent event) { if (// 先檢查View有沒有設置被遮擋時不處理觸摸事件的flag (mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 // 再檢查受到該事件的窗口是否被其它窗口遮擋 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) { // Window is obscured, drop this touch. return false; } return true; }
這個方法的代碼很少幾行就是判斷當前 View 有沒有被遮擋,還有 View 對應的窗口有沒有被遮擋。
Tips:既然判斷事件要不要被分發,有一條是根據mViewFlags
標誌的,那咱們徹底能夠經過設置或是清楚FILTER_TOUCHES_WHEN_OBSCURED
標誌位,這樣就能夠控制觸摸事件在彈出窗口後,後續的事件可否繼續處理。
看完onFilterTouchEventForSecurity
方法咱們繼續回到前面的dispatchTouchEvent
中。咱們看到若是前面是true
,那麼接下來會判斷 view 的mOnTouchListener
是否是空,而且這個View是否是能夠點擊的,若是能夠點擊而且mOnTouchListener
不爲空的話,就會繼續調用mOnTouchListener.onTouch(this.event)
,它若是也是 true 的話,就給result
賦值爲 true ,後面就再也不調用view的點擊事件了。這就是咱們前面說的onTouch()
的方法改成 true 後就不會再執行onClik
的緣由。
Tips:也就是說咱們調用setOnTouchListener
設置的 OnTouchListener 的onTouch()
優先級比onTouchEvent(event)
高。
若是前面不知足result
爲false,那麼就會繼續調用onTouchEvent(event)
方法。
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //判斷View是否是可點擊 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; /***********省略部分代碼******************/ if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } /***********省略部分代碼******************/ // 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(); } } } /***********省略部分代碼******************/ 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; } /***********省略部分代碼******************/ 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); } /***********省略部分代碼******************/ break; } return true; } return false; }
咱們看到這個方法很是的長,咱們注意下面幾點就OK。
1.clickable
判斷 View 是否是可點擊的。
2.若是是的話會根據手勢的 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL。來執行不一樣的代碼。
3.咱們主要看按下手勢 ACTION_DOWN 和擡起手勢 ACTION_UP。
下面咱們首先看 ACTION_DOWN ,若是是不可點擊的那麼就會執行checkForLongClick(0, x, y)
判斷是否是長按。
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); } }
咱們看到這裏主要是若是是長按的話會,延遲發送消息執行一個Runable-CheckForLongPress
,下面咱們看下,這個 Runable 的run()
方法:
@Override public void run() { if ((mOriginalPressedState == isPressed()) && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick(mX, mY)) { mHasPerformedLongPress = true; } } }
這裏咱們看到其實就作了一件事調用了(performLongClick(mX, mY)
咱們繼續跟這個方法,發現它最後調用performLongClickInternal
執行了長按的操做。這裏就很少作深刻了。
咱們回到 ACTION_DOWN ,繼續往下看,咱們會發現緊接着就調用了performButtonActionOnTouchDown(event)
,這個方法就是判斷是否是鼠標右鍵,彈出菜單之類的,下面會判斷是否是滾動視圖之類的。咱們這裏瞭解一下就好。
下面咱們看當咱們擡起手指的時候,執行了那些操做呢?
case MotionEvent.ACTION_UP: /***********省略部分代碼******************/ // 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(); } } /***********省略部分代碼******************/
這裏咱們主要看核心代碼,那就這裏執行了 performClickInternal()
,咱們來看看它作了哪些?
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(); }
咱們看到這個方法很簡單直接 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; }
在這個方法中咱們會看到,這裏它仍是獲取了mListenerInfo
,而且判斷了它的OnClickListener
是否是爲空,若是不爲空則執行mOclickListener.onClick()
方法。
看到這裏,你們是否是明白爲何,View 的onClick
方法會在最後執行了。
這一篇文章咱們首先介紹了事件的傳遞機制,再經過源碼分析了 View 的onTouch
方法爲何比onClick
方法優先執行。咱們學習了 View,那咱們還知道 Activity 是一個 ViewGroup ,下篇文章咱們來分析下手指從觸摸屏幕到 Activity 再到 ViewGroup 的傳遞。
下面咱們經過一張圖來總結如下dispatchTouchEvent
方法
歡迎在評論區留下你的觀點你們一塊兒交流,一塊兒成長。若是今天的這篇文章對你在工做和生活有所幫助,歡迎 轉發分享給更多人。同時歡迎你們加入我組建的大前端學習交流羣,羣裏你們一塊兒學習交流 Android、Flutter等知識。從這裏出發咱們一塊兒討論,一塊兒交流,一塊兒提高。
羣號:872749114