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

ostracod發表於2019-02-25

本篇文章我們專門來研究一下view層的事件分發機制,我們在學習過程中總會碰到關於事件分發的各種問題,如onTouch和onTouchEvent的關係,setOnTouchListener和setOnClickListener的關係等等,類似這樣的問題很多,結論我們都知道,有的時候是死記硬背的,記不長久,本篇文章我們來從原始碼的角度來分析總結一下各種關係,這樣才能理解,便於記憶。

分析工具

//Android原始碼環境
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"    
}

//分析工具
Android Studio 2.2.3
Build #AI-145.3537739, built on December 2, 2016
JRE: 1.8.0_112-release-b05 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o複製程式碼

接下來我們正式分析一下view層的事件分發的原始碼。首先要知道一點,對於view層次的,事件分發主要有兩個方法,dispatchTouchEve和onTouchEvent,我們主要對這兩種方法進行分析。

一、例項引入

我們先通過自定義一個button來進行分析。自定義的button很簡單,就是重寫了一下dispatchTouchEve和onTouchEvent兩個方法。

public class MyButton extends Button {

    protected static final String TAG = "liji-view-test";

    public MyButton(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event)
    {
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "dispatchTouchEvent ACTION_UP");
                break;

            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

}複製程式碼

自定義的MyButton很簡單,就是重寫了view的兩個方法,我們在這兩個方法中只進行一些log操作,其他不改變。接著我們在activity中使用這個自定義的MyButton。

        mMyButton = (MyButton) findViewById(R.id.myButton);
        mMyButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG,"onClick button click");
            }
        });

        mMyButton.setOnTouchListener(new View.OnTouchListener()
        {
            @Override
            public boolean onTouch(View v, MotionEvent event)
            {
                int action = event.getAction();

                switch (action)
                {
                    case MotionEvent.ACTION_DOWN:
                        Log.d(TAG, "onTouch ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d(TAG, "onTouch ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG, "onTouch ACTION_UP");
                        break;
                    default:
                        break;
                }

                return false;
            }
        });複製程式碼

可以看到,在activity中我們也處理了兩個方法,一個是setOnTouchListener、一個是setOnClickListener,然後執行一下,我們可以看看log結果是什麼。

D/liji-view-test: dispatchTouchEvent ACTION_DOWN
D/liji-view-test: onTouch ACTION_DOWN
D/liji-view-test: onTouchEvent ACTION_DOWN
D/liji-view-test: dispatchTouchEvent ACTION_MOVE
D/liji-view-test: onTouch ACTION_MOVE
D/liji-view-test: onTouchEvent ACTION_MOVE
D/liji-view-test: dispatchTouchEvent ACTION_MOVE
D/liji-view-test: onTouch ACTION_MOVE
D/liji-view-test: onTouchEvent ACTION_MOVE
D/liji-view-test: dispatchTouchEvent ACTION_MOVE
D/liji-view-test: onTouch ACTION_MOVE
D/liji-view-test: onTouchEvent ACTION_MOVE
D/liji-view-test: dispatchTouchEvent ACTION_MOVE
D/liji-view-test: onTouch ACTION_MOVE
D/liji-view-test: onTouchEvent ACTION_MOVE
D/liji-view-test: dispatchTouchEvent ACTION_UP
D/liji-view-test: onTouch ACTION_UP
D/liji-view-test: onTouchEvent ACTION_UP
D/liji-view-test: onClick button click複製程式碼

可以大概看出來事件響應的順序是:

dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick複製程式碼

從上面的log可以看出來,onTouch是優先於onClick執行的,並且onTouch執行了多次,一次是ACTION_DOWN,一次是ACTION_UP,還有幾次是ACTION_MOVE。因此事件傳遞的順序是先經過onTouch,再傳遞到onClick。

onTouch方法是有返回值的,如果我們嘗試把onTouch方法裡的返回值改成true,再執行一次就會發現onClick方法不再執行了,這是因為onTouch方法返回true就認為這個事件被onTouch消費掉了,因而不會再繼續向下傳遞。

這其中的緣由究竟是怎麼樣的?我們通過原始碼來一探究竟。view事件分發的順序是從dispatchTouchEvent開始的,所以我們就從它開始分析:

二、原始碼探究

首先我們進入view的dispatchTouchEvent方法中檢視。

    //view.java
    public boolean dispatchTouchEvent(MotionEvent event) {

        //...

        boolean result = false;

        //...


        if (onFilterTouchEventForSecurity(event)) {

            //...

            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;
    }複製程式碼

我們省略了其中無關的程式碼,只看對分析有用的程式碼,我們進入到if中去,首先看到一個物件ListenerInfo的li物件指的是什麼,

    static class ListenerInfo {    
        protected OnFocusChangeListener mOnFocusChangeListener;
        protected OnScrollChangeListener mOnScrollChangeListener;
        public OnClickListener mOnClickListener;
        protected OnLongClickListener mOnLongClickListener;
        private OnKeyListener mOnKeyListener;
        private OnTouchListener mOnTouchListener;
        //...     
    }複製程式碼

看到沒有,其實這個li指的就是我們設定的一些監聽器,包括onTouchListener、onClickListener等等,我們接著分析if中的條件

            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }複製程式碼

可以確認這裡面的li!=null,所以第一個條件為true,第二個條件我們因為設定了onTouchListener事件監聽,所以這裡面的li.mOnTouchListener != null也是為true,再看第三個條件(mViewFlags & ENABLED_MASK) == ENABLED,因為我們的button是可以點選的,所以這裡面也是為true,如果碰到不可點選的,如ImageView,這裡面就是false了,我們到時候另外再談,我們接著看下面一句程式碼。

li.mOnTouchListener.onTouch(this, event))複製程式碼

這句程式碼說明什麼?如果我們在setOnTouchListener裡面返回true的話,那麼我們將直接返回result=true了,如果返回了false的話,那麼這個if條件就不成立,所以它將會執行下一行程式碼if語句端判斷-即它將會執行onTouchEvent事件

            if (!result && onTouchEvent(event)) {
                result = true;
            }複製程式碼

因為我們都是設定的預設返回值,所以在一開始的時候我們的log日誌顯示的順序是:

dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick複製程式碼

這個時候就看看onTouch返回結果了,返回的結果不同導致的順序也不同。我們接著看看onTouchEvent的原始碼,分析一下里面藏了什麼東西。

    public boolean onTouchEvent(MotionEvent event) {
        //...

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:

                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;

                      //...


                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {

                            if (!focusTaken) {

                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                    //...

                    break;

                case MotionEvent.ACTION_DOWN:
                    //...
                    break;

                case MotionEvent.ACTION_CANCEL:
                       //...
                    break;

                case MotionEvent.ACTION_MOVE:
                   //...

                    break;
            }

            return true;
        }

        return false;
    }複製程式碼

我們在onTouchEvent方法中檢視一下,省略一些無關的程式碼,我們發現了其中有一個方法就是在手指鬆開的時候action=MotionEvent.ACTION_UP的時候,會呼叫這個performClick方法。我們進入performClick方法中繼續檢視

    public boolean performClick() {
        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);
        return result;
    }複製程式碼

看到沒?這裡面就涉及到了onClick事件了,這也間接的證明了,onTouch的事件優先順序高於onClick的優先順序。

到了這裡,我們就可以總結一下關於一開始提出來的幾個問題:

1、onTouch和onTouchEvent有什麼區別,又該如何使用?

從原始碼中可以看出,這兩個方法都是在View的dispatchTouchEvent中呼叫的,onTouch優先於onTouchEvent執行。如果在onTouch方法中通過返回true將事件消費掉,onTouchEvent將不會再執行。

另外需要注意的是,onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二當前點選的控制元件必須是enable的。因此如果你有一個控制元件是非enable的,那麼給它註冊onTouch事件將永遠得不到執行(&&操作符,如果前面的判斷為false的話,後面就不判斷了)。對於這一類控制元件,如果我們想要監聽它的touch事件,就必須通過在該控制元件中重寫onTouchEvent方法來實現。

2、onTouch和onClick優先順序

我們從原始碼中也可以分析得到:onTouch的優先順序高於onClick的優先順序,其中onClick的事件是在onTouchEvent中產生的。

判斷是否發生onTouchEvent事件的條件有三個。(1)設定OnTouchListener監聽,(2)該view是否是enable的,(3)在onTouch方法中返回true

如果上述三個條件有一個沒有滿足即為FALSE的話,那麼它將執行onTouchEvent事件同時將產生onClick事件。

3、touch事件的層級傳遞

我們都知道如果給一個控制元件註冊了touch事件,每次點選它的時候都會觸發一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這裡需要注意,如果你在執行ACTION_DOWN的時候返回了false,後面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發後一個action。

說到這裡,很多的朋友肯定要有巨大的疑問了。這不是在自相矛盾嗎?前面的例子中,明明在onTouch事件裡面返回了false,ACTION_DOWN和ACTION_UP不是都得到執行了嗎?其實你只是被假象所迷惑了,讓我們仔細分析一下,在前面的例子當中,我們到底返回的是什麼。參考著我們前面分析的原始碼,首先在onTouch事件裡返回了false,就一定會進入到onTouchEvent方法中,然後我們來看一下onTouchEvent方法的細節。由於我們點選了按鈕,就會進入到第14行這個if判斷的內部,然後你會發現,不管當前的action是什麼,最終都一定會走到第89行,返回一個true。是不是有一種被欺騙的感覺?明明在onTouch事件裡返回了false,系統還是在onTouchEvent方法中幫你返回了true。就因為這個原因,才使得前面的例子中ACTION_UP可以得到執行。

那我們可以換一個控制元件,將按鈕替換成ImageView,然後給它也註冊一個touch事件,並返回false。在ACTION_DOWN執行完後,後面的一系列action都不會得到執行了。這又是為什麼呢?因為ImageView和按鈕不同,它是預設不可點選的,因此在onTouchEvent的內部判斷時無法進入到if的內部,直接跳到第最後面返回了false,也就導致後面其它的action都無法執行了。

三、總結

接下來我們來總結一下各個事件發生的流程。

針對於view來說,當發生一個事件時(譬如:onTouch事件),這個時候就會呼叫view的dispatchTouchEvent事件,它擁有boolean型別的返回值,當返回為true時,順序下發會中斷,也就是說,這個onTouch事件是不會繼續執行下去了,就執行完一個dispatchTouchEvent事件,當它返回false時事件繼續傳遞到onTouchListener中,這個onTouchListener(onTouch事件)也是一個擁有boolean型別的返回值的方法,預設返回false,這個時候就可以繼續執行onClick(在onTouchEvent事件中)事件了,如果onTouch事件返回了true,那麼就代表這個事件被它自己給消耗掉了,不會再繼續傳遞。

用一張圖來表示下:

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

對於View中的dispatchTouchEvent方法,在這個方法內,首先是進行了一個判斷,裡面有三個條件,如果這三個條件都滿足,就返回true,否則就返回onTouchEvent方法執行的結果。對於第一個條件是一個mOnTouchListener變數,這個變數是在View中的setOnTouchListener方法裡賦值的,也就是說只要我們給控制元件註冊了touch事件,mOnTouchListener就一定被賦值了。第二個條件是判斷當前點選的控制元件是否是enable的,按鈕預設都是enable的,因此這個條件恆定為true。第三個條件最為關鍵,mOnTouchListener.onTouch(this, event),其實也就是去回撥控制元件註冊touch事件時的onTouch方法。也就是說如果我們在onTouch方法裡返回true,就會讓這三個條件全部成立,從而整個方法直接返回true。如果我們在onTouch方法裡返回false,就會再去執行onTouchEvent(event)方法。

到這裡,整個view的事件分發就比較清楚了,接下來我們分析關於viewGroup的事件分發了。


關於作者

github: github.com/crazyandcod…
部落格: crazyandcoder.github.io

參考

1、http://www.cnblogs.com/linjzong/p/4191891.html
2、http://3y.uu456.com/bp_5728a9pduw6m3qo9y5s6_1.html

相關文章