Android手機做爲手持設備,界面顯示區域並非很大,爲了有便攜的效果,只能犧牲手機的顯示區域;這就會帶來一個問題,可視內容少;爲了避免影響用戶體驗,咱們必需要在有限的區域作更多的展現,這就對界面的設計有很高的要求了;假如咱們是Google的工程師,咱們要怎麼來設計界面,以此帶來好的體驗效果呢?html
第一種設計:將界面顯示區域切割,根據所須要顯示的視圖,切割爲無數塊,每一塊對應着一部分視圖;以下:android
這種界面設計簡單粗暴,須要多少個視圖,就將界面切割成多少個視圖模塊,以此來放下全部的視圖內容;固然,這樣設計顯而易見會有問題,當視圖愈來愈多的時候,每個視圖的模塊所能展現的區域就會愈來愈小,這樣體驗效果是確定不行的;git
第二種設計:既然經過切割顯示區域以此來展現視圖的方案有問題,那麼咱們就來試試重疊的效果吧;以下:github
這種設計很好的解決了視圖模塊過多時,顯示區域不夠展現的問題;可是也會存在問題,每個顯示區域和用戶的交互順序混亂了,好比我要和模塊爲4的視圖作交互,結果觸發了視圖5的交互效果,而腦洞一方案則沒有該問題;既然如此,那麼咱們能不能針對腦洞二的方案來進行優化呢?
答案是:有的!設計模式
當多個模塊視圖重疊時,要協調好與用戶的交互就極其重要了,畢竟涉及到用戶體驗;bash
當用戶的觸碰屏幕的顯示區域,咱們並不知道哪一個模塊須要和用戶進行交互,而咱們又不能讓用戶和其中一個模塊的交互失效,那麼咱們只能去遍歷重疊的模塊,由內部的視圖來決定是否須要相應用戶的操做;ide
這樣就能夠解決多個模塊視圖重疊時,哪一個模塊須要相應用戶交互的問題了;oop
而這正是Android的事件分發機制;佈局
固然上面只是個人腦洞,用於方便理解,若是你有更好的想法,能夠和我交流;post
那麼這種機制是怎麼來實現這種效果的呢?請繼續往下看;
在深刻分析事件分發以前,先來了解一下事件的來源;
當屏幕被觸摸,Linux內核會將硬件產生的觸摸事件包裝爲Event存到/dev/input/event[x]目錄下。
接着,系統建立的一個InputReaderThread線程loop起來讓EventHub調用getEvent()不斷的從/dev/input/文件夾下讀取輸入事件。
而後InputReader則從EventHub中得到事件交給InputDispatcher。
而InputDispatcher又會把事件分發到須要的地方,好比ViewRootImpl的WindowInputEventReceiver中。
這裏只是簡單瞭解一下大概的流程,源碼過於複雜,這裏不作具體的分析;
歸納之:當觸摸屏幕的時候,硬件會捕捉到用戶的觸摸動做,告訴系統內核,系統內核將該事件保存下來,而後有一個線程會將這個事件讀取出來,交由專門分發的類進行分發;
當屏幕被觸摸時,系統底層會將觸摸事件(座標和時間等)封裝成MotionEvent事件返回給上層 View;從用戶首次觸摸屏幕開始,經歷手指在屏幕表面的任何移動,直到手指離開屏幕時結束都會產生一系列事件;
MotionEvent的類型:
在分析事件分發機制以前,咱們先來看一下事件分發涉及的涉及模式;
這個設計模式是事件分發機制的核心,Google工程師是經過這個設計模式來設計事件分發機制的;理解了這個設計模式有助於咱們理解事件分發機制;
而這個設計模式就是責任鏈模式;
顧名思義,責任鏈模式(Chain of Responsibility Pattern)爲請求建立了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。這種類型的設計模式屬於行爲型模式。
在這種模式中,一般每一個接收者都包含對另外一個接收者的引用。若是一個對象不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。
下面咱們經過一段僞代碼來解讀這個模式:
// 請求
switch (request) {
case 0:
// 對象一接收請求並處理
break;
case 1:
// 對象二接收請求並處理
break;
case 2:
// 對象三接收請求並處理
break;
case 3:
// 對象四接收請求並處理
break;
case 4:
// 對象五接收請求並處理
break;
default:
// 默認對象接收請求並處理
}
複製代碼
上面這個就是咱們用的最熟悉的責任鏈模式,當有一個請求進入責任鏈的時候,會遍歷當前責任鏈上全部的對象,若是匹配到了則提早結束遍歷,若是匹配不到則會被默認的對象接收;
責任鏈的本質是一個單向的鏈表結構,當有請求進入時,只會單向傳遞,直到被接收;
上面咱們理解了責任鏈設計模式以後,接下來咱們來看看事件分發機制的具體實現;
在上上篇博客裏面分析了View的繪製流程,裏面提到了View的層次關係,Activity是View的宿主,而最頂層的View是DecorView,而DecorView裏面則是View樹的結構,那麼咱們將這些關係一一對應到了責任鏈裏面,來看看效果吧;
當有一個事件進入責任鏈時,會從最頂層的DecorView開始往View樹傳遞,直到被其中一個對象所消費;
那麼由此可知事件分發總共能夠分爲三個部分;
接下來先來看一下事件分發機制的核心方法,主要有三個;
下面咱們經過Demo來看看事件是怎麼傳遞的?
寫了一個簡單的佈局,一個RelativeLayout裏面放一個按鈕;
接下來點擊屏幕,看看流程會怎麼走;
step1:當點擊屏幕的時候,會產出一個ACTION_DOWN的事件,傳遞到了Activity的dispatchTouchEvent方法裏,來看一下Activity的dispatchTouchEvent方法,這裏調用了super.dispatchTouchEvent(ev),也就是走了父類的dispatchTouchEvent方法;
這裏面有三個方法,第一個onUserInteraction()是空方法;
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activitys {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
複製代碼
將註釋翻譯過來的意思就是:
每當Key,Touch,Trackball事件分發到當前Activity就會被調用。若是你想當你的Activity在運行的時候,可以得知用戶正在與你的設備交互,你能夠override該方法。
這個回調方法和onUserLeaveHint是爲了幫助Activities智能的管理狀態欄Notification;特別是爲了幫助Activities在恰當的時間取消Notification。
全部Activity的onUserLeaveHint 回調都會伴隨着onUserInteraction。這保證當用戶相關的的操做都會被通知到,例以下拉下通知欄並點擊其中的條目。 這個方法不是重點,不須要過多關注;
須要關注的是第二個方法getWindow().superDispatchTouchEvent(ev),這個方法最終走的是PhoneWindow的superDispatchTouchEvent();
step3:這個mDecor是DecorView,看看DecorView裏的superDispatchTouchEvent(ev)方法作了啥?
這裏面仍是調的super,走的父類的方法;
最終走的是ViewGroup的dispatchTouchEvent()方法;在這個方法裏面經過遍歷當前全部的子View,經過子View的dispatchTouchEvent()方法將事件傳遞下去;ViewGroup的事件分發請看下面的分析;
到這裏Acitivity事件就已經傳遞到ViewGroup了,若是後續的對象都沒有處理該事件,即getWindow().superDispatchTouchEvent(ev)方法返回false時,Activity就會經過onTouchEvent()把當前的事件處理掉;
看一下Activity的onTouchEvent()裏面作了啥?
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
// Window裏面的方法;
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
複製代碼
Activity的onTouchEvent()會判斷當前的事件是否在屏幕的邊緣觸發的,若是是,則返回true,不然返回false;
總結爲流程圖:
接下來咱們來分析一下ViewGroup的事件分發;
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// step1;
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;
}
}
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
// step2;
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
...
}
...
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
// 將當前的事件分發下去;
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
...
}
複製代碼
step1:在ViewGroup的dispatchTouchEvent()方法裏面,在進行事件分發以前,會先調用onInterceptTouchEvent(ev)方法,用於判斷當前的事件是否攔截,若是被攔截了,則事件不分發給子類了,若是沒有攔截則繼續分發下去;
這裏須要注意的是,當事件爲MotionEvent.ACTION_DOWN,纔會走進onInterceptTouchEvent(ev)方法;
在走這個onInterceptTouchEvent(ev)方法以前,還有一個判斷條件,disallowIntercept,這個條件是用來判斷是否要禁用攔截事件,若是禁用了,則不會調用攔截的方法了;子類能夠經過調用requestDisallowInterceptTouchEvent()方法修改;
若是ViewGroup的子類若是沒有重寫onInterceptTouchEvent(ev)這個方法,那麼就會走ViewGroup的方法,這裏用了4個判斷條件,可是默認都是走的false,不攔截事件;
step2:若是事件沒有被攔截,那麼就會遍歷當前全部的子View,而後調用子View的dispatchTouchEvent()方法,將事件分發下去;
那若是被攔截了,則會走super.dispatchTouchEvent(event)方法,也就是View的dispatchTouchEvent(event)方法;這個邏輯寫在dispatchTransformedTouchEvent()方法裏;
到這裏ViewGroup的分發就講完了,至於ViewGroup攔截事件後,怎麼處理事件,請看下面的View事件分析;
流程圖:
View的事件分發也是調用的dispatchTouchEvent(event)方法,讓咱們來看一下這個方法的邏輯;
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// step1
result = true;
}
if (!result && onTouchEvent(event)) {
// step2
result = true;
}
}
...
return result;
}
複製代碼
經過源碼發現,當事件分發到了View的dispatchTouchEvent(event)後,事件就不會再繼續分發下去了;那麼這裏面的邏輯是怎樣的呢?
step1:先判斷當前View的狀態是可響應的((mViewFlags & ENABLED_MASK) == ENABLED),再判斷觸摸監聽mOnTouchListener的onTouch()的返回值,若是子類實現了OnTouchListener這個監聽,而且返回了true,那麼dispatchTouchEvent(event)就會返回true,表示當前View已經處理該事件;
step2:判斷當step1的狀態爲false時,則調用了onTouchEvent(event)來判斷子類是否返回true,返回true則表示當前View已經處理該事件;
看一下onTouchEvent(event)的源碼:
public boolean onTouchEvent(MotionEvent event) {
// 判斷當前狀態是不是可點擊的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
performClickInternal();
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(0, x, y);
break;
...
}
return true;
}
return false;
}
複製代碼
這裏須要關注的是MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP事件;
經過判斷當前的視圖是否處於按壓狀態,且判斷此視圖添加的窗口數量是否和原始的一致,若是這兩種狀態都知足,就會觸發長按監聽回調;最終調用是在performLongClickInternal()方法裏面;
流程圖:
到這裏,事件分發的流程就已經講完了;
讓咱們來回憶一下上面提到的三個方法:
分析到這裏,關於上面腦洞一的設計,這種分發機制是否是完美的解決了交互的問題;
不管你視圖重疊多少,事件都會一層層的傳遞過去,直到被某一層處理掉;有了這個機制,Android的界面就變的更靈活,更有創造性了;
看一下彙總的流程圖:
關於自定義View相關的文章,以前也總結了幾篇,感興趣的能夠看一下;