Android事件構成
在Android中,事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和輕按兩下,另外還包括單指操作和多指操作。所有這些都構成了Android中的事件響應。總的來說,所有的事件都由如下三個部分作為基礎:
按下(ACTION_DOWN)
移動(ACTION_MOVE)
擡起(ACTION_UP)
所有的操作事件首先必須執行的是按下操作(ACTIONDOWN),之後所有的操作都是以按下操作作為前提,當按下操作完成後,接下來可能是一段移動(ACTIONMOVE)然後擡起(ACTION_UP),或者是按下操作執行完成後沒有移動就直接擡起。這一系列的動作在Android中都可以進行控制。
我們知道,所有的事件操作都發生在觸摸屏上,而在螢幕上與我們互動的就是各種各樣的視圖元件(View),在Android中,所有的視圖都繼承于 View,另外通過各種布局元件(ViewGroup)來對View進行布局,ViewGroup也繼承于View。所有的UI控件例如Button、 TextView都是繼承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是繼承于ViewGroup。 是以,我們的事件操作主要就是發生在View和ViewGroup之間,那麼View和ViewGroup中主要有哪些方法來對這些事件進行響應呢?記住 如下3個方法,我們通過檢視View和ViewGroup的源碼可以看到:
View.java
ViewGroup.java
在View和ViewGroup中都存在dispatchTouchEvent和onTouchEvent方法,但是在ViewGroup中還有一 個onInterceptTouchEvent方法,那這些方法都是幹嘛的呢?别急,我們先看看他們的傳回值。這些方法的傳回值全部都是<code>boolean</code>型, 為什麼是boolean型呢,看看本文的标題,“事件傳遞”,傳遞的過程就是一個接一個,那到了某一個點後是否要繼續往下傳遞呢?你發現了嗎,“是否”二 字就決定了這些方法應該用boolean來作為傳回值。沒錯,這些方法都傳回true或者是false。在Android中,所有的事件都是從開始經過傳 遞到完成事件的消費,這些方法的傳回值就決定了某一事件是否是繼續往下傳,還是被攔截了,或是被消費了。
接下來就是這些方法的參數,都接受了一個<code>MotionEvent</code>類型的參數,MotionEvent繼承于InputEvent,用于标記各種動作事件。之前提到的ACTIONDOWN、ACTIONMOVE、ACTION_UP都是MotinEvent中定義的常量。我們通過MotionEvent傳進來的事件類型來判斷接收的是哪一種類型的事件。到現在,這三個方法的傳回值和參數你應該都明白了,接下來就解釋一下這三個方法分别在什麼時候處理事件。
<code>dispatchTouchEvent</code>方法用于事件的分發,Android中所有的事件都必須經過這個方法的 分發,然後決定是自身消費目前事件還是繼續往下分發給子控件處理。傳回true表示不繼續分發,事件沒有被消費。傳回false則繼續往下分發,如果是 ViewGroup則分發給onInterceptTouchEvent進行判斷是否攔截該事件。
<code>onTouchEvent</code>方法用于事件的處理,傳回true表示消費處理目前事件,傳回false則不處理,交給子控件進行繼續分發。
<code>onInterceptTouchEvent</code>是ViewGroup中才有的方法,View中沒有,它的作用是 負責事件的攔截,傳回true的時候表示攔截目前事件,不繼續往下分發,交給自身的onTouchEvent進行處理。傳回false則不攔截,繼續往下 傳。這是ViewGroup特有的方法,因為ViewGroup中可能還有子View,而在Android中View中是不能再包含子View的(iOS 可以)。
到目前為止,Android中事件的構成以及事件處理方法的作用你應該比較清楚了,接下來我們就通過一個Demo來實際體驗實驗一下。
建立一個工程,并建立一個類RTButton繼承Button,用來實作我們對按鈕事件的跟蹤。
RTButton.java
<a></a>
在RTButton中我重寫了dispatchTouchEvent和onTouchEvent方法,并擷取了MotionEvent各個事件狀 态,列印輸出了每一個狀态下的資訊。然後在activity_main.xml中直接在根布局下放入自定義的按鈕RTButton。
activity_main.xml
接下來在Activity中為RTButton設定onTouch和onClick的監聽器來跟蹤事件傳遞的過程,另外,Activity中也有一個dispatchTouchEvent方法和一個onTouchEvent方法,我們也重寫他們并輸出列印資訊。
MainActivity.java
代碼部分已經完成了,接下來運作工程,并點選按鈕,檢視日志輸出資訊,我們可以看到如下結果:
通過日志輸出可以看到,首先執行了Activity的dispatchTouchEvent方法進行事件分發,在<code>MainActivity.java</code>代碼第55行,dispatchTouchEvent方法的傳回值是super.dispatchTouchEvent(event),是以調用了父類方法,我們進入<code>Activity.java</code>的源碼中看看具體實作。
Activity.java
從源碼中可以看到,dispatchTouchEvent方法隻處理了ACTIONDOWN事件,前面提到過,所有的事件都是以按下為起點的,是以,Android認為當ACTIONDOWN 事件沒有執行時,後面的事件都是沒有意義的,是以這裡首先判斷ACTION_DOWN事件。如果事件成立,則調用了onUserInteraction方 法,該方法可以在Activity中被重寫,在事件被分發前會調用該方法。該方法的傳回值是void型,不會對事件傳遞結果造成影響,接着會判斷 getWindow().superDispatchTouchEvent(ev)的執行結果,看看它的源碼:
通過源碼注釋我們可以了解到這是個抽象方法,用于自定義的Window,例如自定義Dialog傳遞觸屏事件,并且提到開發者不需要去實作或調用該 方法,系統會完成,如果我們在MainActivity中将dispatchTouchEvent方法的傳回值設為true,那麼這裡的執行結果就為 true,進而不會傳回執行onTouchEvent(ev),如果這裡傳回false,那麼最終會傳回執行onTouchEvent方法,由此可知,接 下來要調用的就是onTouchEvent方法了。别急,通過日志輸出資訊可以看到,ACTION_DOWN事件從Activity被分發到了 RTButton,接着執行了onTouch和onTouchEvent方法,為什麼先執行onTouch方法呢?我們到RTButton中的 dispatchTouchEvent看看View中的源碼是如何處理的。
挑選關鍵代碼進行分析,可以看代碼第16行,這裡有幾個條件,當幾個條件都滿足時該方法就傳回true,當條件li.mOnTouchListener不為空時,通過在源碼中查找,發現mOnTouchListener是在以下方法中進行設定的。
這個方法就已經很熟悉了,就是我們在<code>MainActivity.java</code>中為RTButton設定的 onTouchListener,條件(mViewFlags & ENABLED_MASK) == ENABLED判斷的是目前View是否是ENABLE的,預設都是ENABLE狀态的。接着就是 li.mOnTouchListener.onTouch(this, event)條件,這裡調用了onTouch方法,該方法的調用就是我們在<code>MainActivity.java</code>中為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是在哪裡被調用的呢?繼續回到<code>View.java</code>的源代碼中尋找。由于onTouchEvent在<code>View.java</code>中的源碼比較長,這裡就不貼出來了,感興趣的可以自己去研究一下,通過源碼閱讀,我們在ACTIONUP的處理分支中可以看到一個<code>performClick()</code>方法,從這個方法的源碼中可以看到執行了哪些操作。
在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
同時,在布局檔案中為RTButton添加一個父布局,指明為自定義的RTLayout,修改後的布局檔案如下。
最後,我們在Activity中也為RTLayout設定onTouch和onClick事件,在MainActivity中添加如下代碼。
代碼修改完畢後,編譯運作工程,同樣,點選按鈕,檢視日志輸出結果如下:
從日志輸出結果我們可以看到,嵌套了RTLayout以後,事件傳遞的順序變成了 Activity->RTLayout->RTButton,這也就回答了前面提出的問題,Android中事件傳遞是從ViewGroup 傳遞到View的,而不是反過來傳遞的。
從輸出結果第三行可以看到,執行了RTLayout的onInterceptTouchEvent方法,該方法的作用就是判斷是否需要攔截事件,我們到ViewGroup的源碼中看看該方法的實作。
該方法的實作很簡單,隻傳回了一個false。那麼這個方法是在哪被調用的呢,通過日志輸出分析可知它是在RTLayout的 dispatchTouchEvent執行後執行的,那我們就進到dispatchTouchEvent源碼裡面去看看。由于源碼比較長,我将其中的關鍵 部分截取出來做解釋說明。
從這部分代碼中可以看到onInterceptTouchEvent調用後傳回值被指派給intercepted,該變量控制了事件是否要向其子控 件分發,是以它起到攔截的作用,如果onInterceptTouchEvent傳回false則不攔截,如果傳回true則攔截目前事件。我們現在将 RTLayout中的該方法傳回值修改為true,并重新編譯運作,然後點選按鈕,檢視輸出結果如下。
可以看到,我們明明點選的按鈕,但輸出結果顯示RTLayout點選事件被執行了,再通過輸出結果分析,對比上次的輸出結果,發現本次的輸出結果完 全沒有RTButton的資訊,沒錯,由于onInterceptTouchEvent方法我們傳回了true,在這裡就将事件攔截了,是以他不會繼續分 發給RTButton了,反而交給自身的onTouchEvent方法執行了,理所當然,最後執行的就是RTLayout的點選事件了。
再看一下整個ViewGroup事件分發過程的流程圖吧,相信可以幫助大家更好地去了解:
詳解onInterceptTouchEvent()
down事件首先會傳遞到onInterceptTouchEvent()方法
如果該ViewGroup的onInterceptTouchEvent()在接收到down事件處理完成之後return false,那麼後續的move, up等事件将繼續會先傳遞給該ViewGroup,之後才和down事件一樣傳遞給最終的目标view的onTouchEvent()處理。
如果該ViewGroup的onInterceptTouchEvent()在接收到down事件處理完成之後return true,那麼後續的move, up等事件将不再傳遞給onInterceptTouchEvent(),而是和down事件一樣傳遞給該ViewGroup的 onTouchEvent()處理,注意,目标view将接收不到任何事件。
如果最終需要處理事件的view的onTouchEvent()傳回了false,那麼該事件将被傳遞至其上一層次的view的onTouchEvent()處理。
如果最終需要處理事件的view 的onTouchEvent()傳回了true,那麼後續事件将可以繼續傳遞給該view的onTouchEvent()處理。
以上我們對Android事件傳遞機制進行了分析,期間結合系統源碼對事件傳遞過程中的處理情況進行了探究。通過單布局情況和嵌套布局情況下的事件傳遞和處理進行了分析,現總結如下:
Android中事件傳遞按照從上到下再從下到上進行層級傳遞,事件處理從Activity開始到ViewGroup再到View,如果View沒有消費事件會再次從View到ViewGroup再到Activity最後事件被抛出消費掉。
事件傳遞方法包括<code>dispatchTouchEvent</code>、<code>onInterceptTouchEven</code>、<code>onTouchEvent</code>,這三個方法的作用分别是負責事件分發、事件攔截、事件處理。其中事件分發和事件處理是View和ViewGroup都有的,最後一個事件攔截隻有ViewGroup才有的方法。
事件從被點選的控件開始,先開始從下到上的通過getParent()找到根布局rootView,然後開始從上向下的開始事件分發dispatchTouchEvent(),事件攔截onInterceptTouchEvent()和事件處理onTouchEvent()。如果事件被攔截了被處理但事件沒有被消費的話,事件就會從被攔截處理的控件開始自下到上的開始往上傳遞,在傳遞過程中會調用父類的事 件攔截onInterceptTouchEvent()和事件處理onTouchEvent()方法;如果事件一直沒被消費,将會被rootView抛出 結束事件傳遞。在事件被觸發開始從下到上的傳遞後開始自上到下的事件分發,等事件傳到最底部的控件後如果沒有被消費的話,事件又開始自下到上的傳遞直到被 根部布局抛出,一旦事件被消費,事件傳遞機制就會停止結束。
在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法傳回true代表不允許事件繼續向子View傳遞,傳回false代表不對事件進行攔截,預設傳回false。
在事件從上到下分發到底後開始自下到上的傳遞過程中,子View可以通過調用getParent().requestDisallowInterceptTouchEvent(true); 讓其父類ViewGroup布局不攔截事件(requestDisallowInterceptTouchEvent(true)此方法參數為true 表示不攔截事件,為false 表示攔截事件)
本文轉自 一點點征服 部落格園部落格,原文連結:http://www.cnblogs.com/ldq2016/p/5216489.html,如需轉載請自行聯系原作者