什麼叫事件分發機制
事件分發是:當發生了一個事件時,在屏幕上找到一個合適的控件來處理這個事件的過程。因爲一個界面上控件如此之多,發生一個事件後總要尋找一個合適來處理事件吧。這個過程就叫做事件分發的機制。
事件分發流程
Activity是Android應用程序的門面和載體,它代表一個完整的用戶界面。Activity提供了一個窗口來繪製各種視圖,即PhoneWindow類。該類繼承自頂層窗口類Window,並且包含一個DecorView類對象。DecorView繼承自FrameLayout(幀佈局),所以本質上是一個ViewGroup,而且是當前活動所放置的全部View的根視圖(RootView)。當我們創建一個活動時,在活動的onCreate()方法中調用 setContentView(layoutID) 方法就是爲該活動的ContentView部分指定佈局內容從而完成GUI的渲染。
當用戶點擊屏幕產生一個事件,事件通過底層硬件捕獲,然後交給ViewRootImpl處理,ViewRootImpl通過Window將事件交給Activity。事件要傳遞給Activity那麼它就必須持有Activity的引用,Window在Activity的attach方法中通過mWindow.setCallback(this)調用持有了Activity的引用,Activity實現了Window.Callback的接口方法。所以最終事件是通過Window.Callback.dispatchTouchEvent把時間交給Acitivity的。
/**
* 從窗口返回到調用方的API。這允許客戶端攔截密鑰調度、面板和菜單等。
*/
public interface Callback {
public boolean dispatchKeyEvent(KeyEvent event);
public boolean dispatchKeyShortcutEvent(KeyEvent event);
public boolean dispatchTouchEvent(MotionEvent event);
...........
}
事件發生時,ViewRootImpl通過Window將事件交給Activity,然後再一層層的向下層傳遞,直到找到合適的處理控件。大致如下:
硬件 -> ViewRootImpl -> Window -> Activity -> PhoneWindow -> DecorView -> VIewGroup -> View
但是如果事件傳遞到最後的View還是沒有找到合適的View消費事件,那麼事件就會向相反的方向傳遞,最終傳遞給Activity,如果最後 Activity 也沒有處理,本次事件纔會被拋棄:
Activity <- PhoneWindow <- DecorView <- ViewGroup <- View
事件的類型
事件主要分爲觸摸事件和點擊事件。
觸摸事件
觸摸事件對應的是MotionEvent類,主要有以下三種類型
- ACTION_DOWN:表示用戶手指按下的動作,標誌着觸摸事件的開始。
- ACTION_UP:表示用戶手指離開屏幕的動作,標誌着觸摸事件的結束。
- ACTION_CANCEL:如果某一個子View處理了Down事件,那麼隨之而來的Move和Up事件也會交給它處理。但是交給它處理之前,父View還是可以攔截事件的,如果攔截了事件,那麼子View就會收到一個Cancel事件,並且不會收到後續的Move和Up事件。
- ACTION_MOVE:表示用戶手指移動的動作。當用戶手指按下屏幕後,在鬆開之前,只要移動的距離超過了一定的閾值即判定爲ACTION_MOVE動作。實際上,即使是手指非常 輕微的移動也會被系統監測到從而判定爲ACTION_MOVE動作。
用戶觸摸屏幕操作由ACTION_DOWN事件開始,結束於ACTION_UP事件,可以有0次或多次ACTION_MOVE事件。
點擊事件
用戶手指按下→停留若干時間(可長可短)→用戶手指鬆開,這一完整的過程視爲一次點擊事件。可以看出,觸摸事件先於點擊事件執行。
事件的分發
在我們平時的使用或寫自定義View時,都會直接或間接的使用View的事件分發,View的事件分發主要與View源碼中的3個方法有關:
- dispatchTouchEvent()
- onTouch()
- onTouchEvent()
當事件發生時,ViewGroup會在dispatchTouchEvent方法中先看自己能否處理事件,如果不能再去遍歷子View查找合適的處理控件。如果到最後result還是false,表示所有的子View都不能處理,纔會調用自身的onTouchEvent來處理。View的事件分發我們需要看一個方法dispatchTouchEvent。我們通過View的事件分發的實現源碼來分析分發流程。
/**
* 將觸摸屏運動事件向下傳遞到目標視圖,如果它是目標視圖,則傳遞此視圖。
* @param 事件要調度的運動事件。
* @return 如果事件由視圖處理,則爲True;否則爲false。
*/
public boolean dispatchTouchEvent(MotionEvent event) {
//省略代碼
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//ListenerInfo是View的內部類,這個類定義了各種的監聽以及事件,
//包括焦點變化監聽,滾動變化監聽,點擊事件監聽等
ListenerInfo li = mListenerInfo;
//如果li.mOnTouchListener.onTouch(this, event)返回true,
//並且view的狀態是enable狀態下,該方法的result就直接返回true
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果result=false,纔會走下面的onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
//省略代碼
return result;
}
註釋寫得很清楚了,主要概括幾個要點:
- 如果我們傳入的OnTouchListener中的onTouch返回true的話,並且在enable=true情況下,就不會執行到onTouchEvent方法
- view.setEnable(false)的時候,不會執行onTouchListener的onTouch方法,但是會執行View自身的onTouchEvent方法
- view.setEnable(false)的時候,OnClickLisener.onClick方法不會執行(下面提到)
dispatchTouchEvent中沒有發現view的onClick方法的調用,其實onClick在onTouchEvent中,在判斷事件類型的swithc中,在ACTION_UP擡起的事件中,我們看到這樣的代碼:
public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
在performClickInternal中調用了 performClick()方法,就是在這個方法中執行了view的onClick方法
/**
* Entry point for {@link #performClick()} - other methods on View should call it instead of
* {@code performClick()} directly to make sure the autofill manager is notified when
* necessary (as subclasses could extend {@code performClick()} without calling the parent's
* method).
*/
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
在performClick中,可以看到如果設置了監聽的話就會調用view的onClick方法。
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
所以我們可以得出一個初步的結論。也就是說在view的onTouchEventListener中的onTouch()如果返回了true,表示此次事件被處理,也就是按下的ACTION_DOWN事件被消耗,但是ACTION_UP事件無法傳遞執行,所以onClick不會執行。
如果onTouchEventListener中的onTouch返回false。則會執行View自身的onTouchEvent方法,表示onTouch不會消耗事件,所以onClick點擊事件會響應。
有一點需要特別注意:setClickable(false)需要在setOnClickListener之後調用才起作用,因爲在setOnClickListener中將view設置setClickable(true)了
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
* @param l The callback that will run
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
ViewGroup事件傳遞
前面分析了View的事件分發,但在實際開發過程中真正要使用View事件分發時,基本都是因爲ViewGroup的嵌套導致的內外滑動問題,所以對ViewGroup的事件分發更需要深入瞭解,和View的事件分發一樣,ViewGroup事件分發一樣與幾個重要方法有關:
- dispatchTouchEvent() -> 用來分派事件
- onInterceptTouchEvent() -> 用來攔截事件
- onTouchEvent() -> 用來處理事件
使用一段僞代碼來表述上面三個方法在ViewGroup事件分發中的作用,代碼如下:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}
從上面代碼中看出,事件傳遞到ViewGroup時首先傳遞到dispatchTouchEvent(MotionEvent event)中,然後執行以下邏輯,首先在ViewGroup.dispatchTouchEvent() 中調用onInterceptTouchEvent() 方法:
- 返回true,表示攔截事件 -> onTouchEvent() -> 返回true 表示消耗
- 返回false,表示不攔截事件 -> child.dispatchTouchEvent(event) 事件向下傳遞,如此反覆傳遞分發
在onInterceptTouchEvent() 返回false時,表明當前ViewGroup不消耗事件,此事件會向下傳遞給子View,此子View可能是View也可能是ViewGroup,如果是View則按照上面的事件分發消耗事件。
事件的傳遞首先是從手指觸摸屏幕開始,所以我們先查看ViewGroup的dispatchTouchEvent()中的ACTION_DOWN方法,剔除剩餘複雜的邏輯,方法有一段主要的代碼:
public boolean dispatchTouchEvent(MotionEvent event){
...
// 標誌自身是否攔截此事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 當子View調用requestDisallowInterceptTouchEvent函數時該變量爲true。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 返回true表示子View設置了父容器不攔截事件
if (!disallowIntercept) {
// 如果子View沒有禁止父View攔截事件,父View通過該函數判斷是否需要攔截此事件。
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 一定攔截事條件:事件類型不爲ACTION_DOWN並且mFirstTouchTarget爲null。
// 這說明ACTION_DOWN事件已經被自身消耗,那麼該事件序列中的剩餘事件也應該被自身消耗。
intercepted = true;
}
...
}
上述代碼雖然簡單但ViewGroup的事件分發多半與此處的邏輯有關,裏面的每個細節都會影響到最終的事件消耗,總結上面代碼執行如下:
- 首先當一個事件進來的時候,會先判斷當前事件是否是down或着mFirstTouchTarget是否爲null(當我們第一次進到ViewGroup.dispatchTouchEvent的時候mFirstTouchTarget會爲空的),如果當前事件爲down或者mFirstTouchTarget爲null的時候就會調用ViewGroup.onInterceptTouchEvent方法,接着會將onInterceptTouchEvent的返回進行記錄。
- mFirstTouchTarget:指向處理觸摸事件的子View;當ViewGroup子View成功攔截後,mFirstTouchTarget指向子View,此時滿足mFirstTouchTarget != null,則在整個事件過程中會不斷詢問ViewGroup的攔截狀況;
- 如果ViewGroup確定攔截事件,mFirstTouchTarget爲null,所以整個觸摸事件不會詢問ViewGroup的onInterceptedTouchEvent(),且之後的事件直接交給ViewGroup執行;
- 可能有小夥伴會問,disallowIntercept這個值是什麼東西,不知道大家有沒有用過getParent().requestDisallowInterceptTouchEvent(true)方法來達到禁止父ViewGroup攔截事件,當我們給這個方法設置什麼,disallowIntercept就會是什麼值,所以我們在事件攔截的時候,可以在其子View裏面調用該方法進行事件攔截。
當intercepted爲false也就是不攔截的時候,就會遍歷子元素,並將事件向下分發交給子元素進行處理:
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//給mFirstTouchTarget賦值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
}
}
可以看到當在遍歷子孩子的時候會調用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法,,而在dispatchTransformedTouchEvent方法中我們會發現下面代碼,通過下面代碼會發現,當child不爲空的時候他就會直接調用子元素的dispatchTouchEvent方法,這樣事件就交由子元素處理了,從而完成一輪分發。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
當子View.dispatchTouchEvent返回爲true時,就會調用addTouchTarget(child, idBitsToAssign)方法,該方法就是在給mFirstTouchTarget賦值。
當子View.dispatchTouchEvent返回爲false時,就不會調用addTouchTarget(child, idBitsToAssign)方法,故mFirstTouchTarget爲null。
那麼mFirstTouchTarget爲null時會出現什麼情況呢,繼續向下看,會看到下面的代碼,注意這裏的view傳的是null,也就是說會調用super.dispatchTouchEvent(event)代碼,super.dispatchTouchEvent(event)是什麼呢?他就是我們自己的dispatchTouchEcent方法。也就是事件將我們自己去處理。
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
根據上面的View和ViewGroup的事件分發學習,這裏給出幾個View事件傳遞的結論(以下結論針對系統自動分發),並根據學習內容進行逐條分析
- 事件序列指的是從手指接觸屏幕那一刻起,到手指離開屏幕那一刻爲止產生的所有事件。
- ViewGroup默認不攔截任何事件。
- 事件分發過程中ViewGroup會考慮多點觸控的問題,例如在一個佈局中有兩個子控件,如果兩個手指同時對它們進行操作,控件是可以正常響應的。
- 正常情況下一個事件序列只能被一個View攔截或消耗,除非使用特殊方法控制事件傳遞;
- 對於View一旦決定攔截事件即onTouchEvent()返回true,那後續的整個事件序列都會交給它消耗;
- 如果View不消耗ACTION_DOWN事件,則後續的事件序列都不會再給他處理
- 如果View在ACTION_DOWN時返回false,那系統的mFirstTouchTarget爲null,在後續的MOVE、UP事件中onInterceptTouchEvent()不會再被調用,直接攔截事件
View事件攔截案例分析
事件攔截最經典的使用示例和場景就是滑動衝突,按照View的衝突場景分,滑動衝突可以分爲3類:
- 外部滑動和內部滑動方向不一致
- 外部滑動和內部滑動方向一致
- 以上兩種情況嵌套
一般處理滑動衝突有兩種攔截方法:外攔截和內攔截
外部攔截
外攔截顧名思義是在View的外部攔截事件,對View來說外部就是其父類容器,即在父容器中攔截事件,通過上面的代碼我們知道,ViewGroup的事件攔截取決與onInterceptTouchEvent()的返回值,所以我們在ViewGroup中重寫onInterceptTouchEvent()方法,在父類需要的時候返回true攔截事件,具體需要的場景要按照自己的業務邏輯判斷:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
if(isParentNeed()){
//父容器的邏輯
return true;
}
break;
default:
break;
}
super.onInterceptTouchEvent(event)
return false;
}
從上面代碼中看出:在onInterceptTouchEvent()的ACTION_DOWN中必須返回false,即不攔截ACTION_DOWN事件,因爲如果ACTION_DOWN一但攔截,事件後面的事件都會默認給ViewGroup處理,也不會再調用onInterceptTouchEvent()詢問攔截,那子View將沒有獲取事件的機會;在ACTION_DOWN中,根據自己需要的時候返回true,那此時事件就會被父ViewGroup消耗。
內部攔截
內部攔截法父View攔截除ACTION_DOWN以外的其它事件。子View在ACTION_DOWN中調用getParent().requestDisallowInterceptTouchEvent(true)方法接管事件並在ACTION_MOVE中根據業務邏輯決定事件是否教給父View處理。如需交給父View處理則調用requestDisallowInterceptTouchEvent(false)方法。內部攔截法不符合事件分發流程,是通過子VIew反向控制父View攔截,規則:
- 父元素要默認攔截除了ACTION_DOWN以外的其他事件
- 子元素調用parent.requestDisallowInterceptTouchEvent(false/true)來控制父元素是否攔截事件
- 父元素不能攔截ACTION_DOWN因爲它不受FLAG_DISALLOW_INTERCEPT標誌位控制,一旦父容器攔截ACTION_DOWN那麼所有的事件都不會傳遞給子View
/**
* 內部攔截法
* 父View需攔截除DOWN以外的其他事件
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(ev);
return false;
} else {
return true;
}
}
/**
* 內部攔截法
* 子View.dispatchTouchEvent特殊處理
*/
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (isParentNeed()) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
上述代碼是內部攔截的典型代碼,當面對不同的滑動策略時只需要修改裏面的條件即可,其他不需要做改動而且也不能有改動。