View的事件分發機制,從dispatchTouchEvent說起(一)

在這裏插入圖片描述
事件分發機制是android中的核心知識點和難點。相信很多人也和我一樣對於這點感到非常困惑。我看了很多篇博客和書面資料。今天我們就聊聊事件的分發機制。

一、點擊事件的傳遞規則

1、什麼是點擊事件(MotionEvent)

在瞭解點擊事件的傳遞規則之前,我們首先要弄明白什麼事點擊事件(MotionEvent),所謂MotionEvent是指手指接觸屏幕後所產生的一系列事件。

ACTION_DOWN————手指剛接觸屏幕。
ACTION_MOVE————手指在屏幕上移動。
ACYION_UP————手指從屏幕上鬆開的一瞬間。

2、點擊事件分發過程

點擊事件的分發過程就是MotionEvent的分發過程,該過程主要由以下三個函數來完成:

public boolean dispatchTouchEvent(MotionEvent ev)

功能:用來進行事件的分發

public boolean onInterceptTouchEvent(MotionEvent ev)

功能:用來判斷是否攔截某個事件。

public boolean onTouchEvent(MotionEvent ev)

功能:處理點擊事件,在dispatchTouchEvent中調用。返回結果表示是否消耗當前點擊事件。

先不急我們從最簡單的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 事件傳遞算是入門了。
下面我們繼續接着往下看,我們通過源碼的角度來分析以下。

3、點擊事件遞

首先我們要知道,當我們手指觸摸屏幕上的控件後,接下來肯定會調用它的dispatchTouchEvent方法。我們根據下面一張圖來分析
在這裏插入圖片描述
當我們手指點擊屏幕上的 button 時,就會去調用 button 的dispatchTouchEvent方法,這時候會發現button 裏面沒有這個方法,那麼它就會繼續向上查找它的父類 TextView 的dispatchTouchEvent方法,如果沒有還是繼續向上查找,直到找到 View 中會發現這裏有dispatchTouchEvent方法。

二、源碼分析

下面我們根據源碼來看看,事件究竟是如何傳遞的?首先我們還是來看dispatchTouchEvent方法。

1.View.dispatchEvent(event)

    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)這個方法,這個方法主要作用就是判斷該觸摸事件要不要分發,我們下面來看下這個方法。

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)方法。

2、View.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。

I.MotionEvent.ACTION_DOWN

下面我們首先看 ACTION_DOWN ,如果是不可點擊的那麼就會執行checkForLongClick(0, x, y)判斷是不是長按。

a.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),這個方法就是判斷是不是鼠標右鍵,彈出菜單之類的,下面會判斷是不是滾動視圖之類的。我們這裏瞭解一下就好。

I.MotionEvent.ACTION_UP

下面我們看當我們擡起手指的時候,執行了那些操作呢?

 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(),我們接下來繼續看這個方法。

View.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源碼分析——View是如何被添加到屏幕的?
Android熱修復——深入剖析AndFix熱修復及自己動手實現
深入理解HashMap原理(一)——HashMap源碼解析(JDK 1.8)
深入理解HashMap原理(二)——手寫HashMap

歡迎在評論區留下你的觀點大家一起交流,一起成長。如果今天的這篇文章對你在工作和生活有所幫助,歡迎轉發分享給更多人。

同時歡迎大家掃描左側邊欄的二維碼關注我的公衆號和加入我組建的大前端學習交流羣,羣裏大家一起學習交流 Android、Flutter等知識。從這裏出發我們一起討論,一起交流,一起提升。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章