在Android中,事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和雙擊,另外還包括單指操做和多指操做。全部這些都構成了Android中的事件響應。總的來講,全部的事件都由以下三個部分做爲基礎:java
全部的操做事件首先必須執行的是按下操做(ACTIONDOWN),以後全部的操做都是以按下操做做爲前提,當按下操做完成後,接下來多是一段移動(ACTIONMOVE)而後擡起(ACTION_UP),或者是按下操做執行完成後沒有移動就直接擡起。這一系列的動做在Android中均可以進行控制。android
咱們知道,全部的事件操做都發生在觸摸屏上,而在屏幕上與咱們交互的就是各類各樣的視圖組件(View),在Android中,全部的視圖都繼承於View,另外經過各類佈局組件(ViewGroup)來對View進行佈局,ViewGroup也繼承於View。全部的UI控件例如Button、TextView都是繼承於View,而全部的佈局控件例如RelativeLayout、容器控件例如ListView都是繼承於ViewGroup。因此,咱們的事件操做主要就是發生在View和ViewGroup之間,那麼View和ViewGroup中主要有哪些方法來對這些事件進行響應呢?記住以下3個方法,咱們經過查看View和ViewGroup的源碼能夠看到:windows
View.java架構
public boolean dispatchTouchEvent(MotionEvent event) public boolean onTouchEvent(MotionEvent event)
ViewGroup.javaide
相關廠商內容工具
相關贊助商源碼分析
ArchSummit深圳2016將於07月15-16日在華僑城洲際大酒店舉行,現價9折搶購,團購報名更多優惠!佈局
public boolean dispatchTouchEvent(MotionEvent event) public boolean onTouchEvent(MotionEvent event) public boolean onInterceptTouchEvent(MotionEvent event)
在View和ViewGroup中都存在dispatchTouchEvent和onTouchEvent方法,可是在ViewGroup中還有一個onInterceptTouchEvent方法,那這些方法都是幹嗎的呢?別急,咱們先看看他們的返回值。這些方法的返回值所有都是boolean
型,爲何是boolean型呢,看看本文的標題,「事件傳遞」,傳遞的過程就是一個接一個,那到了某一個點後是否要繼續往下傳遞呢?你發現了嗎,「是否」二字就決定了這些方法應該用boolean來做爲返回值。沒錯,這些方法都返回true或者是false。在Android中,全部的事件都是從開始通過傳遞到完成事件的消費,這些方法的返回值就決定了某一事件是不是繼續往下傳,仍是被攔截了,或是被消費了。this
接下來就是這些方法的參數,都接受了一個MotionEvent
類型的參數,MotionEvent繼承於InputEvent,用於標記各類動做事件。以前提到的ACTIONDOWN、ACTIONMOVE、ACTION_UP都是MotinEvent中定義的常量。咱們經過MotionEvent傳進來的事件類型來判斷接收的是哪種類型的事件。到如今,這三個方法的返回值和參數你應該都明白了,接下來就解釋一下這三個方法分別在何時處理事件。url
dispatchTouchEvent
方法用於事件的分發,Android中全部的事件都必須通過這個方法的分發,而後決定是自身消費當前事件仍是繼續往下分發給子控件處理。返回true表示不繼續分發,事件沒有被消費。返回false則繼續往下分發,若是是ViewGroup則分發給onInterceptTouchEvent進行判斷是否攔截該事件。onTouchEvent
方法用於事件的處理,返回true表示消費處理當前事件,返回false則不處理,交給子控件進行繼續分發。onInterceptTouchEvent
是ViewGroup中才有的方法,View中沒有,它的做用是負責事件的攔截,返回true的時候表示攔截當前事件,不繼續往下分發,交給自身的onTouchEvent進行處理。返回false則不攔截,繼續往下傳。這是ViewGroup特有的方法,由於ViewGroup中可能還有子View,而在Android中View中是不能再包含子View的(iOS能夠)。到目前爲止,Android中事件的構成以及事件處理方法的做用你應該比較清楚了,接下來咱們就經過一個Demo來實際體驗實驗一下。
首先在Eclipse新建一個工程,並新建一個類RTButton繼承Button,用來實現咱們對按鈕事件的跟蹤。
RTButton.java
public class RTButton extends Button { public RTButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("RTButton---dispatchTouchEvent---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("RTButton---dispatchTouchEvent---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("RTButton---dispatchTouchEvent---UP"); break; default: break; } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("RTButton---onTouchEvent---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("RTButton---onTouchEvent---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("RTButton---onTouchEvent---UP"); break; default: break; } return super.onTouchEvent(event); } }
在RTButton中我重寫了dispatchTouchEvent和onTouchEvent方法,並獲取了MotionEvent各個事件狀態,打印輸出了每個狀態下的信息。而後在activity_main.xml中直接在根佈局下放入自定義的按鈕RTButton。
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/myLayout" android:layout_width="match_parent" android:layout_height="match_parent" > <com.ryantang.eventdispatchdemo.RTButton android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button"/> </LinearLayout>
接下來在Activity中爲RTButton設置onTouch和onClick的監聽器來跟蹤事件傳遞的過程,另外,Activity中也有一個dispatchTouchEvent方法和一個onTouchEvent方法,咱們也重寫他們並輸出打印信息。
MainActivity.java
public class MainActivity extends Activity { private RTButton button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = (RTButton)this.findViewById(R.id.btn); button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("RTButton---onTouch---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("RTButton---onTouch---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("RTButton---onTouch---UP"); break; default: break; } return false; } }); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { System.out.println("RTButton clicked!"); } }); } @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("Activity---dispatchTouchEvent---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("Activity---dispatchTouchEvent---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("Activity---dispatchTouchEvent---UP"); break; default: break; } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("Activity---onTouchEvent---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("Activity---onTouchEvent---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("Activity---onTouchEvent---UP"); break; default: break; } return super.onTouchEvent(event); } }
代碼部分已經完成了,接下來運行工程,並點擊按鈕,查看日誌輸出信息,咱們能夠看到以下結果:
經過日誌輸出能夠看到,首先執行了Activity的dispatchTouchEvent方法進行事件分發,在MainActivity.java
代碼第55行,dispatchTouchEvent方法的返回值是super.dispatchTouchEvent(event),所以調用了父類方法,咱們進入Activity.java
的源碼中看看具體實現。
Activity.java
/** * Called to process touch screen events. You can override this to * intercept all touch screen events before they are dispatched to the * window. Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
從源碼中能夠看到,dispatchTouchEvent方法只處理了ACTIONDOWN事件,前面提到過,全部的事件都是以按下爲起點的,因此,Android認爲當ACTIONDOWN事件沒有執行時,後面的事件都是沒有意義的,因此這裏首先判斷ACTION_DOWN事件。若是事件成立,則調用了onUserInteraction方法,該方法能夠在Activity中被重寫,在事件被分發前會調用該方法。該方法的返回值是void型,不會對事件傳遞結果形成影響,接着會判斷getWindow().superDispatchTouchEvent(ev)的執行結果,看看它的源碼:
Activity.java
/** * Used by custom windows, such as Dialog, to pass the touch screen event * further down the view hierarchy. Application developers should * not need to implement or call this. * */ public abstract boolean superDispatchTouchEvent(MotionEvent event);
經過源碼註釋咱們能夠了解到這是個抽象方法,用於自定義的Window,例如自定義Dialog傳遞觸屏事件,而且提到開發者不須要去實現或調用該方法,系統會完成,若是咱們在MainActivity中將dispatchTouchEvent方法的返回值設爲true,那麼這裏的執行結果就爲true,從而不會返回執行onTouchEvent(ev),若是這裏返回false,那麼最終會返回執行onTouchEvent方法,由此可知,接下來要調用的就是onTouchEvent方法了。別急,經過日誌輸出信息能夠看到,ACTION_DOWN事件從Activity被分發到了RTButton,接着執行了onTouch和onTouchEvent方法,爲何先執行onTouch方法呢?咱們到RTButton中的dispatchTouchEvent看看View中的源碼是如何處理的。
View.java
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }
挑選關鍵代碼進行分析,能夠看代碼第16行,這裏有幾個條件,當幾個條件都知足時該方法就返回true,當條件li.mOnTouchListener不爲空時,經過在源碼中查找,發現mOnTouchListener是在如下方法中進行設置的。
View.java
/** * Register a callback to be invoked when a touch event is sent to this view. * @param l the touch listener to attach to this view */ public void setOnTouchListener(OnTouchListener l) { getListenerInfo().mOnTouchListener = l; }
這個方法就已經很熟悉了,就是咱們在MainActivity.java
中爲RTButton設置的onTouchListener,條件(mViewFlags & ENABLED_MASK) == ENABLED判斷的是當前View是不是ENABLE的,默認都是ENABLE狀態的。接着就是li.mOnTouchListener.onTouch(this, event)條件,這裏調用了onTouch方法,該方法的調用就是咱們在MainActivity.java
中爲RTButton設置的監聽回調,若是該方法返回true,則整個條件都知足,dispatchTouchEvent就返回true,表示該事件就不繼續向下分發了,由於已經被onTouch消費了。
若是onTouch返回的是false,則這個判斷條件不成立,接着執行onTouchEvent(event)方法進行判斷,若是該方法返回true,表示事件被onTouchEvent處理了,則整個dispatchTouchEvent就返回true。到這裏,咱們就能夠回答以前提出的「爲何先執行onTouch方法」的問題了。到目前爲止,ACTIONDOWN的事件通過了從Activity到RTButton的分發,而後通過onTouch和onTouchEvent的處理,最終,ACTIONDOWN事件交給了RTButton得onTouchEvent進行處理。
當咱們的手(我這裏用的Genymotion而後用鼠標進行的操做,用手的話可能會執行一些ACTIONMOVE操做)從屏幕擡起時,會發生ACTIONUP事件。從以前輸出的日誌信心中能夠看到,ACTIONUP事件一樣從Activity開始到RTButton進行分發和處理,最後,因爲咱們註冊了onClick事件,當onTouchEvent執行完畢後,就調用了onClick事件,那麼onClick是在哪裏被調用的呢?繼續回到View.java
的源代碼中尋找。因爲onTouchEvent在View.java
中的源碼比較長,這裏就不貼出來了,感興趣的能夠本身去研究一下,經過源碼閱讀,咱們在ACTIONUP的處理分支中能夠看到一個performClick()
方法,從這個方法的源碼中能夠看到執行了哪些操做。
View.java
/** * Call this view's OnClickListener, if it is defined. Performs all normal * actions associated with clicking: reporting accessibility event, playing * a sound, etc. * * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); return true; } return false; }
在if分支裏能夠看到執行了li.mOnClickListener.onClick(this);這句代碼,這裏就執行了咱們爲RTButton實現的onClick方法,因此,到目前爲止,能夠回答前一個「onClick是在哪裏被調用的呢?」的問題了,onClick是在onTouchEvent中被執行的,而且,onClick要後於onTouch的執行。
到此,點擊按鈕的事件傳遞就結束了,咱們結合源代碼窺探了其中的執行細節,若是咱們修改各個事件控制方法的返回值又會發生什麼狀況呢,帶着這個問題,進入下一節的討論。
從上一節分析中,咱們知道了在Android中存在哪些事件類型,事件的傳遞過程以及在源碼中對應哪些處理方法。咱們能夠知道在Android中,事件是經過層級傳遞的,一次事件傳遞對應一個完整的層級關係,例如上節中分析的ACTIONDOWN事件從Activity傳遞到RTButton,ACTIONUP事件也一樣。結合源碼分析各個事件處理的方法,也能夠明確看到事件的處理流程。
以前提過,全部事件處理方法的返回值都是boolean類型的,如今咱們來修改這個返回值,首先從Activity開始,根據以前的日誌輸出結果,首先執行的是Activity的dispatchTouchEvent方法,如今將以前的返回值super.dispatchTouchEvent(event)修改成true,而後從新編譯運行並點擊按鈕,看到以下的日誌輸出結果。
能夠看到,事件執行到dispatchTouchEvent方法就沒有再繼續往下分發了,這也驗證了以前的說法,返回true時,再也不繼續往下分發,從以前分析過的Activity的dispatchTouchEvent源碼中也可知,當返回true時,就沒有去執行onTouchEvent方法了。
接着,將上述修改還原,讓事件在Activity這繼續往下分發,接着就分發到了RTButton,將RTButton的dispatchTouchEvent方法的返回值修改成true,從新編譯運行並查看輸出日誌結果。
從結果能夠看到,事件在RTButton的dispatchTouchEvent方法中就沒有再繼續往下分發了。接着將上述修改還原,將RTButton的onTouchEvent方法返回值修改成true,讓其消費事件,根據以前的分析,onClick方法是在onTouchEvent方法中被調用的,事件在這被消費後將不會調用onClick方法了,編譯運行,獲得以下日誌輸出結果。
跟分析結果同樣,onClick方法並無被執行,由於事件在RTButton的onTouchEvent方法中被消費了。下圖是整個事件傳遞的流程圖。
到目前爲止,Android中的事件攔截機制就分析完了。但這裏咱們只討論了單佈局結構下單控件的狀況,若是是嵌套佈局,那狀況又是怎樣的呢?接下來咱們就在嵌套佈局的狀況下對Android的事件傳遞機制進行進一步的探究和分析。
首先,新建一個類RTLayout繼承於LinearLayout,一樣重寫dispatchTouchEvent和onTouchEvent方法,另外,還須要重寫onInterceptTouchEvent方法,在文章開頭介紹過,這個方法只有在ViewGroup和其子類中才存在,做用是控制是否須要攔截事件。這裏不要和dispatchTouchEvent弄混淆了,後者是控制對事件的分發,而且後者要先執行。
那麼,事件是先傳遞到View呢,仍是先傳遞到ViewGroup的?經過下面的分析咱們能夠得出結論。首先,咱們須要對工程代碼進行一些修改。
RTLayout.java
public class RTLayout extends LinearLayout { public RTLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("RTLayout---dispatchTouchEvent---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("RTLayout---dispatchTouchEvent---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("RTLayout---dispatchTouchEvent---UP"); break; default: break; } return super.dispatchTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("RTLayout---onInterceptTouchEvent---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("RTLayout---onInterceptTouchEvent---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("RTLayout---onInterceptTouchEvent---UP"); break; default: break; } return super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("RTLayout---onTouchEvent---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("RTLayout---onTouchEvent---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("RTLayout---onTouchEvent---UP"); break; default: break; } return super.onTouchEvent(event); } }
同時,在佈局文件中爲RTButton添加一個父佈局,指明爲自定義的RTLayout,修改後的佈局文件以下。
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <com.ryantang.eventdispatchdemo.RTLayout android:id="@+id/myLayout" android:layout_width="match_parent" android:layout_height="match_parent" > <com.ryantang.eventdispatchdemo.RTButton android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button" /> </com.ryantang.eventdispatchdemo.RTLayout> </LinearLayout>
最後,咱們在Activity中也爲RTLayout設置onTouch和onClick事件,在MainActivity中添加以下代碼。
MainActivity.java
rtLayout.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("RTLayout---onTouch---DOWN"); break; case MotionEvent.ACTION_MOVE: System.out.println("RTLayout---onTouch---MOVE"); break; case MotionEvent.ACTION_UP: System.out.println("RTLayout---onTouch---UP"); break; default: break; } return false; } }); rtLayout.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { System.out.println("RTLayout clicked!"); } });
代碼修改完畢後,編譯運行工程,一樣,點擊按鈕,查看日誌輸出結果以下:
從日誌輸出結果咱們能夠看到,嵌套了RTLayout之後,事件傳遞的順序變成了Activity->RTLayout->RTButton,這也就回答了前面提出的問題,Android中事件傳遞是從ViewGroup傳遞到View的,而不是反過來傳遞的。
從輸出結果第三行能夠看到,執行了RTLayout的onInterceptTouchEvent方法,該方法的做用就是判斷是否須要攔截事件,咱們到ViewGroup的源碼中看看該方法的實現。
ViewGroup.java
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
該方法的實現很簡單,只返回了一個false。那麼這個方法是在哪被調用的呢,經過日誌輸出分析可知它是在RTLayout的dispatchTouchEvent執行後執行的,那咱們就進到dispatchTouchEvent源碼裏面去看看。因爲源碼比較長,我將其中的關鍵部分截取出來作解釋說明。
ViewGroup.java
// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
從這部分代碼中能夠看到onInterceptTouchEvent調用後返回值被賦值給intercepted,該變量控制了事件是否要向其子控件分發,因此它起到攔截的做用,若是onInterceptTouchEvent返回false則不攔截,若是返回true則攔截當前事件。咱們如今將RTLayout中的該方法返回值修改成true,並從新編譯運行,而後點擊按鈕,查看輸出結果以下。
能夠看到,咱們明明點擊的按鈕,但輸出結果顯示RTLayout點擊事件被執行了,再經過輸出結果分析,對比上次的輸出結果,發現本次的輸出結果徹底沒有RTButton的信息,沒錯,因爲onInterceptTouchEvent方法咱們返回了true,在這裏就將事件攔截了,因此他不會繼續分發給RTButton了,反而交給自身的onTouchEvent方法執行了,理所固然,最後執行的就是RTLayout的點擊事件了。
以上咱們對Android事件傳遞機制進行了分析,期間結合系統源碼對事件傳遞過程當中的處理狀況進行了探究。經過單佈局狀況和嵌套佈局狀況下的事件傳遞和處理進行了分析,現總結以下:
dispatchTouchEvent
、onTouchEvent
、onInterceptTouchEvent
,其中前兩個是View和ViewGroup都有的,最後一個是隻有ViewGroup纔有的方法。這三個方法的做用分別是負責事件分發、事件處理、事件攔截。