最近在使用Robolectric進行單元測試的時候經常需要模擬click、touch等UI操作,期間遇到了各種問題,例如:onTouch和onTouchEvent有什么區別?onClick什么時候會響應?使用ShadowView中clickOn模擬點擊操作為什么無法響應onTouch?為什么ListView添加滑動菜單功能后,ListView本身不能再滑動?被這些問題困擾了很久,因此決定徹底搞清楚Android Touch事件分發機制。這篇文章先整體介紹了Android UI事件處理機制-基于監聽的處理方式和基于回調的處理方式,然后分別從View、ViewGoup角度分析Touch事件背后的實現邏輯,最后結合Robolectric介紹如何在單元測試中測試相關的回調、監聽方法。
Android UI事件處理機制
事件是指一個對象的狀態發生變化,事件處理是指事件發生時執行的代碼。與界面編程相關的就是UI事件-用戶在程序界面中執行各種操作,之后應用程序需要對這些操作提供響應動作,這個響應就是由UI事件的處理機制來完成。
Android針對UI事件,提供了兩種處理機制:一種是基于監聽方式的處理機制,另一種是基于回調方式的處理機制。對于基于監聽的處理方式,主要是為Android 界面組件綁定特定的事件監聽器;對于基于回調的處理方式,主要的做法是通過為Android 組件重寫?特定的回調方法實現。下面我們詳細介紹二者具體模型、流程、使用方法。
基于監聽的事件處理機制
基于監聽的事件處理方式實際是一種委托式(代理)模式,某個組件(事件源)將整個事件的處理委托給特定的對象(綁定在該組件上的事件監聽器),由這個特定的對象來進行該事件的響應。
事件監聽處理模型
事件監聽處理模型涉及到三類對象:
- 事件源-事件產生的地方,一般就是界面組件。
- 事件-事件封裝了界面組件上的一次用戶操作,如果程序需要獲取界面組件上所發生事件的相關信息,就可以通過Event對象來獲取。
- 事件監聽器-包含事件處理方法,負責監聽事件源所發生的事情,并對各種事件做出響應。
事件處理流程
簡單來說,基于監聽器的事件處理流程是事件源組件發生事件,系統會執行該事件源組件上監聽器的對應處理方法。具體流程如下圖所示。
常見的事件監聽器實現方式
在事件處理模型三個重要組成部分中,事件由系統負責生成、任意界面組件都可作為事件源,而事件監聽器是整個事件處理核心。因此我們主要的工作就是實現事件監聽器。所謂實現事件監聽器其實就是實現特定接口的Java類實例,常用的方法包括:
-
內部類實現
內部類實現事件監聽器的優點是可以在當前外部類中復用該監聽器類、監聽器類是外部類的內部類,可以自由訪問外部類的所有界面組件。
-
外部類實現
使用外部類定義事件監聽器的形式比較少見,使用外部類實現的缺點主要是事件監聽器屬于特定GUI,定義成外部類不利于提高程序內聚性、外部類形式的事件監聽器不能自由訪問創建GUI界面的類中組件,編程不夠簡潔。
-
Activity實現
直接在Activity中實現監聽器接口,這種方式雖然簡單,但是卻使得Activity類的職能混亂,Activity主要職責是完成界面初始化工作,但此時還需包含事件處理器方法,違反面向對象單職能原則。
-
匿名內部類實現
使用匿名內部類實現,代碼簡潔,是目前使用最廣泛的方法。
基于回調的事件處理機制
與監聽的委托式事件處理不一樣,基于回調事件處理模型,事件源和事件監聽器是統一的。用戶在組件上激發某個事件,組件自己特定的方法將會負責處理該事件。通常實現的方法是通過繼承組件類,重寫相關的事件處理方法。
基于回調的事件傳播
基于回調的事件處理方法都有一個boolean返回值,使用該返回值標識該回調方法是否已經完全消費該事件。
- true-該處理方法已經完全處理事件,該事件不會傳播出去
- false-該處理方法并未完全處理該事件,該事件會傳播出去
二者對比
基于監聽器的事件處理機制優點:
- 事件源、事件監聽器由兩個類實現,使用委托(代理)模式,分工明確,易于維護。
- Android事件處理機制優先觸發該組件綁定的事件監聽器,然后才會觸發該組件提供的事件回調方法。即監聽機制優先級更高。
基于回調的事件處理機制優點:
- 適合處理事件邏輯比較固定的View
Android Touch 事件分發、響應機制
Android UI事件包括Touch事件、Key事件等,我們這里主要研究Touch事件分發、消費的流程。Touch事件分發消費流程中包括兩個主角:View和ViewGroup(Activity的Touch事件實際上是調用它內部的ViewGroup的Touch事件)。Touch事件類型包括ACTION_DOWN,ACTION_UP,ACTION_MOVE。這里我們通過解析View、ViewGroup事件分發響應的源碼,來對Android Touch事件的分發進行研究。
View Touch事件響應詳細流程
View中關于Touch事件主要涉及兩個方法:dispatchTouchEvent、onTouchEvent。Android View 事件分發機制 源碼解析 (上)中結合實例及源碼對這兩個方法進行了詳細研究,具體細節可參考該文章。
dispatchTouchEvent
View的dispatchTouchEvent主要就是將事件按照onTouchListener監聽器、onTouchEvent事件回調的順序依次分發。任意一個返回true則表示該事件已經被當前這個View處理了。再回過頭來看上面我們提到的Android提供的兩種事件處理方法優缺點中『Android事件處理機制優先觸發該組件綁定的事件監聽器,然后才會觸發該組件提供的事件回調方法。即監聽機制優先級更高。』,這段話是不是更能理解了?
onTouchEvent
View的onTouchEvent是Android系統處理Touch事件的回調方法,這個方法針對不同事件類型-ACTION_DOWN、ACTION_MOVE、ACTION_UP分別進行處理,我們下面也按照這三種事件類型進行分析。
-
ACTION_DOWN:更新View狀態、發送一個檢測長按的延時任務(500ms)。檢測長按的延時任務是指
用戶從ACTION_DOWN觸發開始算起,如果500ms內沒有抬起則認為產生了長按事件,這個時候就會觸發長按事件的監聽器 onLongClickListener的onLongClick方法執行,如果該方法返回為true,則設置mHasPerformedLongPress值為true,這個值 會在ACTION_UP事件中協助判斷是否需要觸發Click事件的監聽器。如果onLongClick方法執行返回false或者是沒有產生長按事件,mHasPerformedLongPress值仍為false。
ACTION_MOVE:這個事件的處理相對會比較簡單,主要是判斷是否移出了當前View,如果移出則取消在ACTION_DOWN中設置的View狀態、取消長按操作檢測等。
ACTION_UP:執行ACTION_UP后續步驟的前提條件是當前View的狀態需要是pressed或者prepressed,即該View已經響應了ACTION_DOWN事件。然后通過判斷mHasPerformedLongPress來決定是否對Click事件進行響應,如果mHasPerformedLongPress為true表示該View已經響應了長按事件,那么就不會再對Click事件進行處理;反之則表示需要處理Click事件,處理方法就是調用Click事件監聽器onClickListener的onClick方法。最后清除View的狀態,刷新頁面完成該事件的處理。
總結
- 我們在屏幕上簡單的一個觸摸操作就會產生Touch事件、LongClick事件、Click事件。
- 當我們在屏幕上觸摸時,第一個產生的事件就是Touch事件,。Android系統為Touch事件提供了兩種處理機制-基于監聽器方式(onTouchListener的onTouch處理方法)和基于回調的方式(onTouchEvent),其中優先處理監聽器方式。
- 當ACTION_DOWN持續500ms以上,會產生LongClick事件。Android系統為LongClick事件提供了監聽器方式(onLongClickLIstener的onLongClick處理方法)。
- 當ACTION_UP隨ACTION_DOWN而來,就會產生Click事件。Android系統為Click事件也是提供了監聽器方式(onClickListener的onClick處理方法)。但是Click事件的監聽器是否觸發則取決于該View是否消費了LongClick事件,如果未消費,那么則觸發當前View 的Click事件監聽器;如果已經消費了LongClick,則不會觸發Click事件監聽器。
下一篇將總結ViewGroup Touch事件處理流程及如何結合Robolectric在單元測試中測試Touch相關的回調方法、監聽器。