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

本篇文章我們專門來研究一下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,那麼就代表這個事件被它自己給消耗掉了,不會再繼續傳遞。

對於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的事件分發了。

關於作者

專注於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎大家關注,一起交流學習~

在這裏插入圖片描述

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