事件這里指的是一系列的MotionEvent(android.view.MotionEvent)類對象,實際上是一個動作碼和一個坐標軸值的集合,動作碼指明了在觸摸時發生的變化,坐標軸值含有位置,時間等運動屬性信息,常見動作類型即action code如下所示:
ACTION_MASKED | 描述 |
---|---|
ACTION_DOWN | 當手指第一次觸摸屏幕的時候產生,是一個事件的開始,包含著初始位置等信息,該指針的指針數據索引始終為MotionEvent中0 |
ACTION_MOVE | 當手機在屏幕上移動的時候,產生一系列的MOVE,包括坐標軸和其他的運動屬性 |
ACTION_UP | 最后一根手指離開屏幕時產生,標志著事件的結束(或者是ACTION_CANCEL) |
ACTION_CANCEL | 動作終止,類似于ACTION_UP,但是不執行任何正常狀態下要觸發的動作 |
ACTION_POINTER_DOWN! | 超出第一個進入屏幕的觸摸手指,多點觸控的情況,對應的數據由getActionIndex()返回的索引獲取 |
ACTION_POINTER_UP | 非最后一根手指離開屏幕,對應多點觸控的情況 |
當屏幕接收到點擊,就產生了一個事件,緊接著就會觸發一系列特定的方法,一套完整的事件分發機制,從上到下依次是Activity→ViewGroup→View,實際上是按照視圖的層次結構進行分發的。
Activity對事件的分發
事件首先傳遞給當前屏幕上對應的Activity,它是用來和用戶進行交換的窗口,每個Activity都會有一個用于繪制其用戶界面的窗口,窗口通常會充滿屏幕。
Activity要執行的是dispatchTouchEvent(MotionEvent ev)
函數,它可以把捕獲到的動作傳遞給根視圖。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
首先如果是觸摸事件的開始,即對應動作是ACTION_DOWN,進入if判斷內部的onUserInteraction()
,該回調函數的意義是表明了用戶早與當前Activity以某種方式進行交動,這些輸入事件可能來自鍵盤,觸摸或者軌跡球。與該函數對應的onUserLeaveHint()可以一起重載,用以智能地管理狀態欄通知的活動等。
然后將事件交給Window進行分發,如果返回值不為true(一般因為超出Window邊界之外沒有View去接收觸摸事件),則執行Activity本身的onTouchEvent(),這是最后的保障手段,默認返回值為真,表明動作已經被消耗處理。
而getWindow()獲取的Window類對象是一個抽象類,可以控制頂層類的外觀和行為策略,它的唯一實現類是android.view.PhoneWindow 。PhoneWindow實際上把事件傳遞給了DecorView。
DecorView是FrameLayout的子類,是最頂層的視圖,包含標題欄(title)和內容欄(content)。對應于它的唯一一個子視圖結構LinearLayout中的兩個FrameLayout子元素,其中一個是標題欄,會隨著主題的不同而不同,另一個是內容欄,此外內容欄ID:Android.R.id.content
是固定的。而我們經常在調用的setContentView(View view)
,就是指的這個名稱為content的視圖。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView中該函數的內容為super.dispatchTouchEvent(),而FrameLayout中并沒有這個方法,繼續向上,最終實際上對應的是ViewGroup中的這個方法。如此,整個觸摸事件的動作便從Activity傳到頂級View。
ViewGroup對事件的分發
函數dispatchTouchEvent(MotionEvent ev)
,返回值表示事件是否分發處理。
清除狀態
首先是通過onFilterTouchEventForSecurity(MotionEvent)
函數過濾TouchEvent,如果被攔截就直接返回FALSE。之后獲得動作類型,如果是ACTION_DOWN,表示一個新的手勢動作開始了,就取消清除所有之前的TouchTarget記錄,該類是一個單鏈表數據結構;并且重置觸摸狀態,例如將FLAG_DISALLOW_INTERCEPT標志位重置為0,表示允許父視圖中斷事件。
判定攔截
然后是判斷是否攔截事件,ViewGroup在兩種狀態下會攔截事件,當動作為ACTION_DOWN或者mFirstTouchTarget非空,具體代碼如下:
// 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;
}
當一個事件開始時(ACTION_DOWN),必然要判斷是否攔截該事件。另一方面,函數會記錄下哪一個子View消耗了該事件,以便之后把后同一個事件序列的所有動作都交給它處理。如果當前事件被該ViewGroup攔截,那么mFirstTouchTarget的值就為null,不管后續到來的動作是什么,判決條件都是FALSE,即始終攔截后續的事件。即沒有觸摸目標并且這個動作不是初始按下,就直接攔截該事件。
下一步是是獲取FLAG_DISALLOW_INTERCEPT
標志位的情況,這個標志位可以通過調用requestDisallowInterceptTouchEvent(boolean disallowIntercept)
方法設定的,參數值為true,表示不允許攔截事件,不執行onInterceptTouchEvent
函數,否則就執行。
在ViewGroup不攔截正常分發MotionEvent時,每一個動作都會經過onInterceptTouchEvent()方法。即onInterceptTouchEvent()函數使父視圖有機會在子視圖接收到事件以前,看到它所要接收處理的事件。
onInterceptTouchEvent()返回值含義
返回值 | 描述 |
---|---|
true | 攔截MotionEvent事件,這表示它不會被傳遞給子View,先前正在消耗處理事件的子視圖會收到ACTION_CANCEL,并且從該點開始的所有后續事件將發送到父節點的onTouchEvent()方法 |
false | 簡單地監視事件,事件依舊沿著視圖層次結構傳播到通常的目標,使用目標的onTouchEvent()方法處理事件 |
事件向下分發
如果該ViewGroup沒有攔截事件的時候,事件繼續向下分發,遍歷所有子視圖,找到一個子視圖來接收事件。
首先判斷視圖是否可以接收點事件,當視圖是可見的,視圖正在或者計劃播放動畫效果都是可以接收點擊事件的,同時判斷事件對應的坐標點是否在子視圖的區域內,不滿足條件的跳過,進行下一個。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
最終在dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desirePointerIdBits)
函數中,調用了子視圖的dispatchTouchEvent
方法,這個過程中,對觸摸事件的位置進行了轉換操作,實際上就是根據Scroll計算了位置偏移。這樣就將事件分發給了子視圖。
如果子視圖返回值為true,那么就可以確定分發對象,跳出for循環。在此之前調用addTouchTarget
方法,函數中對mFirstTouchTarget(TouchTarget類對象)賦值,它影響著ViewGroup的分發攔截方式,不為null時,當后邊的一系列動作到來時,就可以直接傳遞給相應的視圖,否則會攔截同一手勢序列中的所有觸摸事件。
如果遍歷結束以后,都沒有找到消耗事件的子視圖,那么ViewGroup會自己處理該事件,調用dispatchTransformeTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)
函數,把子視圖參數設置為null,即把自身當做一個普通的View,調用父類View的dispatchTouchEvent
方法。
View對事件的處理
View是一個單獨的視圖,沒有子視圖需要分發,所有直接由自身處理。
調用onTouch方法
首先是判斷View有沒有設置觸摸監聽(View默認情況下是ENABLE的),以及是否設置OnTouchListener接口的回調函數onTouch(View v, MotionEvent event)
,如果設置了,則調用該函數并取得處理之后的返回值。為true就不會調用后續的onTouchEvent方法,顯然onTouch
方法優先級高于onTouchEvent
方法;如果返回值是false,則調用onTouchEvent
方法。
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;
}
調用onTouchEvent方法
對onTouchEvent
方法進行分析,首先會涉及到視圖的狀態,視圖狀態有很多,常用的視圖狀態有如下:
名稱 | 描述 |
---|---|
enabled | 表示當前視圖可用狀態。可以通過setEnable 方法進行位運算,改變視圖的狀態。如果對應的為為0,表示不可用,就無法響應onTouch事件,正常情況下(mViewFlags & ENABLED_MASK) == ENABLED
|
focused | 視圖是否獲得焦點。判斷方式(mViewFlags & FOCUSABLE_MASK) == FOCUSABLE ,類似于打游戲通過手柄的上下左右鍵切焦點,requestFocus 方法可以改變焦點 |
pressed | 視圖是否處于按下狀態,按下對象,必須為Clickable。調用setPressed 方法來對這一狀態進行改變,判斷方式(mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED
|
clickable | 視圖是否可以點擊,CLICKABLE對應的是setClickable 函數,在設定點擊的響應函數setOnClickListene r時,自動會把視圖設置為可點擊狀態 |
當View處于不可用狀態,即·(viewFlags & ENABLED_MASK) == DISABLED·時,函數的返回值為:
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
即只要是可以點擊的,不論是長按LONG_CLICKABLE還是短按CLICKABLE,還是內容(用于觸控筆按鈕或鼠標右鍵單擊)是可點擊的CONTEXT_CLICKABLE,都會消耗點擊事件,盡管處于不可用狀態,View只是不作出相應的響應。
接下來判斷視圖代理TouchDelegate,用于想要視圖具有比其實際視圖邊界更大的觸摸面積。觸摸區域被更改的視圖稱為委托視圖。mTouchDelegate的使用機制和mTouchListener,mOnClickListener等接口類似。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
動作處理
最后是對具體的動作進行處理,當View是可點擊視圖的時候,最后就一定會返回true,View的CLICKABLE屬性狀態要分情況看待,實質就是視圖是否可以點擊,而View的LONG_CLICKABLE屬性狀態默認是關閉的。
通過switch-case語句,對不同的動作狀態進行不同的響應
ACTION_DOWN
初始化長按狀態,即把mHasPerformedLongPress賦值為false,尚未執行長按動作;
判斷View是否在一個可以滾動的容器中,比如ListView,進行延時,防止當用戶實際上是要滑動容器時,出現按下的狀態。
如果是在一個這樣的容器中,把mPrivateFlags的PREPRESSED標識位置1;通過postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout())
函數短時間推延這個動作按下的反饋,設置推延的時長為 ViewConfiguration.getTapTimeout
(默認115ms),如果在這個時間段內,該Message沒有從消息隊列中取出,那么等到時間導到以后就運行CheckForTap
類內的run函數,內容包括:
- 設定視圖的PREPRESSED狀態位為0;
- 調用
setPressed
方法,把View的PRESSED狀態位設置為1; - 執行
checkForLongClick
函數,檢測View的LONG_CLICKABLE標志位,為1就把長按檢測的函數通過postDelayed(mPendingCheckForLongPress,ViewConfiguration.getLongPressTimeout()-delayOffset)
加入MessageQueue中,設定延時的時間為長按的檢測時間(默認500ms)- 之前延時檢測按下狀態的時間(默認為115ms),即385ms。
同樣如果在這個時間段以內沒有從消息隊列中取出該Message,執行以下內容:
- 執行
performLongClick
函數,即檢測視圖的OnLongClickListener接口,如果有定義,就調用onLongClick
函數,根據它的返回結果,確定是否把mHasPerformedLongPress狀態設為已執行。
當視圖不在滾動容器內時,就立即顯示按下的反饋,直接調用setPressed
方法,并且發出一個檢測長按的延遲事件為0毫秒的任務checkForLongClick(0, x, y)
總之就是檢測Tap和LongClick:把mPendingCheckForTap,mPendingCheckForLongPress對應的run添加到Message隊列中
ACTION_MOVE
調用drawableHotspotChanged(x, y)
,表明View的熱點hotspot發生變化,并將變化傳播到視圖管理的Drawable對象或子視圖時。
然后用pointInView
方法判斷確定給定觸摸點(在局部坐標中)是否在視圖內,其中視圖的邊界都擴展了一個最小滑動距離TOUCH_SLOP的大小。如果移出了范圍:
-
removeTapCallback
函數,把PFLAG_PREPRESSED標志位置0,并把之前CheckForTap對象mPendingCheckForTap要延時執行的Runnable通過removeCallbacks
函數從消息隊列中取消(如果還未執行的話); - 查看PRESSED狀態位,為1,說明已經過了檢測Tap的115ms,第一條中的Runnable已經執行了,要移除在run函數中添加的檢測長按的CheckForLongPress類對象
mPendingCheckForLongPress
;并且執行setPressed
,把PRESSED標志位置為0。
即只要用戶移出了對應的視圖的坐標范圍,就將所有關于輕觸(tap)和長按(long press)的狀態全部取消。
ACTION_UP:
- 動作結束了,對之前的所有標識進行一個總的判斷,查看PREPRESSED和PRESSED狀態位,不管哪一個是真,都進入下一階段;
- 為視圖請求焦點,并且進入觸摸模式。如果View可以獲得焦點,并且還沒有獲得焦點,就請求焦點;
- 把之前為預按下狀態(PFLAG_PREPRESSED)的設置為按下,確保用戶可以看到按下狀態的出現。即如果 prepressed 值為true,調用
setPressed(true, x, y)
,把 PRESSED**標志位設定為1,對應于下邊的第6點; - 如果表示長按狀態的mHasPerformedLongPress為false,并且忽略下一次ACTION_UP事件的mIgnoreNextUpEvent狀態標識為false,就移除長按的檢測,因為手勢已經到此結束了,不可能再有長按了;
- 判斷mPerformClick,如果為null,初始化一個實例,該類實現了一個Runable接口,然后調用post,通過異步處理Handler發送run函數到消息隊列尾部,如果添加Message失敗則直接執行
performClick
函數,確保執行,不直接調用performClick
函數,可以讓View的其他視覺狀態在點擊動作開始之前更新。 - 如果之前獲取的prepressed值為true,64毫秒(
ViewConfiguration.getPresedStateDuration
)后執行UnsetPressedState類對象mUnsetPresedState,否則立即執行mUnsetPresedState;最后無論如何mUnsetPresedState.run()都會執行,其內部調用了·setPresed(false)·,把的PRESSED標志位重置為0,這樣實際上是為了保證之前處于預按下狀態的View,變為按下狀態,有一個足夠的延時(默認為64ms),來讓用戶觀察到。 - 最后調用·removeTapCallback·函數,目的是移除PFLAG_PREPRESSED狀態位,并且撤銷在消息隊列中對應的tap延時執行內容 。
總結
這就是Android的事件分發機制的主要流程,關鍵方法如下
方法 | 調用位置 | 描述 |
---|---|---|
dispatchTouchEvent | A, VG, V | 分發事件到子視圖 |
onInterceptTouchEvent | VG | 在傳遞到子視圖前攔截事件 |
onTouchEvent | V | 處理觸摸事件 |
A代表Activity,VG代表ViewGroup, V 代表View
上述內容參照了網上一些博客的描述。