概念
在移動設備上,我們去做一些操作,無論是 Android 還是 IOS 其實在系統中是根據事件驅動的,用戶通過屏幕與手機交互的時候,每一次的點擊,長按,移動等都是一個事件。
而事件分發機制呢?他其實是因爲 Android 每一個頁面都是基於 Activity 進行實現的,一個 Activity 裏面有若干個 View 以及若干個 ViewGroup 組成的,而事件分發機制就是某一個事件從屏幕傳遞給各個 View,由這個 View 來消費這個事件或者忽略這個事件,交與其他 View 進行消費的這個過程的控制。
事件分發的對象是什麼呢?系統會把整個事件封裝爲 MotionEvent 對象,事件分發的過程就是 MotionEvent 對象分發的過程
事件的類型
結合我們的人爲操作過程,事件的類型有下面四種:
- 按下(ACTION_DOWN)
- 移動(ACTION_MOVE)
- 擡起(ACTION_UP)
- 取消(ACTION_CANCEL)
所以說一個完整的事件序列是從手指按下屏幕開始,到手指離開屏幕爲止所產生的一系列事件。也就是說一個完整的事件序列是以一個 ACTION_DOWN 事件開始,到一個 ACTION_UP 事件爲止,中間有若干個 ACTION_MOVE 事件(當然可以沒有)。
在同一個事件序列中,如果子 View / ViewGroup 沒有消費該事件,那同一事件序列的後續事件就不會傳遞到該子 View / ViewGroup 中去。
事件分發
那 Android 中是怎樣傳遞事件的呢?
其實主要的對象就是 Activity,ViewGroup 以及 View。事件的分發就是對這一系列的傳遞的操作。接下來我們就圍繞這三種主要的對象的事件分發來進行理解。
Activity
流程圖
下面就是 Activity 事件分發的流程圖:
源碼分析
我們從流程圖中可以知道當事件開始觸發的時候會調用 dispatchTouchEvent
方法,那我們來看下對應的源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
從源碼中我們可以知道當事件的類型是 DOWN 的時候,會執行 onUserInteraction
方法
public void onUserInteraction() {
}
然後進入這個方法,我們可以發現在源碼中該方法爲空方法。所以說當我們需要監聽按下手勢的時候,重寫 onUserInteraction
方法就可以達到監聽的效果。
然後接着往下面看,我們就會發現會調用 Window 的 superDispatchTouchEvent
方法。假如消費了該事件的話,就會返回 true ,代表事件已被消費,否則調用 onTouchEvent
方法消費該事件。
public abstract boolean superDispatchTouchEvent(MotionEvent event);
觀看源碼後我們發現其實 superDispatchTouchEvent
方法是一個抽象方法。我們看 Window 類的註釋會發現
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
其實他有唯一的實現,就是 PhoneWindow 類,然後我們看下對應的實現類的方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
其實可以發現調用的是 DecorView 的 superDispatchTouchEvent
方法,而 DecorView 呢?其實就是 Activity 頂層的View,也我們 setContentView 方法傳遞進來的 layout 就是添加到了這個 View 上面。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
然後我們就發現他其實調用了他的父類的 dispatchTouchEvent
方法,也就是 ViewGroup 的 dispatchTouchEvent
方法,那這個方法就一起在後面的 ViewGroup 裏面記錄。
接下來就是說假如 getWindow().superDispatchTouchEvent(ev)
返回了 true ,那就什麼該事件已經被消費了,直接返回就行,如果返回的是 false ,就說明當前任何視圖都沒有處理這個事件,那我們就是要調用 Activity 的 onTouchEvent
方法去消費該事件,並且直接返回 onTouchEvent
方法的結果。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
如果 mWindow.shouldCloseOnTouch(this, event)
返回結果爲 true ,就將該 Activity finish掉,並且返回true,否則爲 false。
/** @hide */
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;
}
mCloseOnTouchOutside 只有當 Activity 是以 Dialog 方式進行實現的時候纔會爲 true 否則爲 false
然後怎麼確保有 View 呢?其實 peekDecorView
方法就是用來獲取當前 Activity 的 DecorView 的
最後怎麼保證是點擊在 View 外部的呢,其實就是靠 isOutside 變量。所以我們就可以理解爲什麼之前會調用 finish
方法了。所以說只有在當 Activity 是以 Dialog 方式進行實現的時候,並且點擊了 View 外部的空白纔會將該 Activity 關閉,否則不做任何處理直接消費事件。
ViewGroup
流程圖
上面說到了之後就會進入 ViewGroup 的 dispatchTouchEvent
方法,這個方法就是標誌着事件已經到了 ViewGroup 這一層。然後呢?就是 onInterceptTouchEvent
方法,這個方法的意義就是是否攔截事件,假如返回結果爲 true 的話,則代碼該事件,當前的 ViewGroup 會攔截該事件,事件就不會再向下傳遞了。最後就是 onTouchEvent
方法了。這個方法在 ViewGroup 中沒有實現,而是在 View 中進行實現的。這個方法就是用來當我們把事件攔截了以後,自己來處理這個事件重寫的。
下面就是 ViewGroup 的事件分發流程圖:
源碼分析
我們首先就來看一下 ViewGroup 的 dispatchTouchEvent
方法,源碼的行數較多,就不貼上來了,其實這個方法主要就是做了三件事:
- 判斷是否需要攔截事件
- 在當前的 ViewGroup 中找到用戶真正點擊的 View
- 分發事件到 View 上
根據流程圖我們可以發現從 onFilterTouchEventForSecurity
方法開始進入事件分發的過程
onFilterTouchEventForSecurity
方法做的就是一些安全策略的操作,主要的用處就是去判斷這個 View 是不是可以被觸摸,假如這個視圖被其他視圖遮擋,那就不會去處理這個事件。
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
如果這個方法返回了 false,那就說明安全策略不通過,所以直接返回 false,否則的話再進行後面的事件分發。
然後我們就會對這個事件的類型進行判斷,假如這個事件的類型是一個按下的操作的時候,就回去做一些初始化的操作,因爲按下是一個事件系列的開始。
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
cancelAndClearTouchTargets
方法是用於取消和清除所有的觸摸目標,然後通過 resetTouchState
方法來重置觸摸狀態。
然後就要開始檢測當前的事件是否是需要攔截的,就是靠 intercepted 這個變量去記錄的。
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;
}
如果當前事件爲按下事件或者是已經有處理改事件的子 View 的時候就要進入後面的判斷,否則就爲 true,表示的是事件被攔截,事件就不會再向下傳遞。disallowIntercept 變量的意義就是判斷當前事件是否可以攔截,如果爲 true 的話,就代表當前事件在這個 ViewGroup 是不允許被攔截的,如果爲 false 代表這個事件是可以被攔截的,然後就要通過 onInterceptTouchEvent
方法來判斷是否對事件攔截。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
這幾個判斷條件都是什麼呢?
- 判斷當前事件是不是來自鼠標(因爲一般我們使用都不會使用鼠標進行操作,所有一般來說,這個返回爲 false)
- 當前事件是不是按下事件
- 判斷當前我們是否按下鼠標左鍵(如果按下返回 true,否則返回 false)
- 判斷當前觸摸位置是不是在一個滾動條的上面(如果是的話返回爲 true,否則爲 false)
只有這四個條件都滿足的話,我們纔會去攔截這個事件。所以說一般情況下 onInterceptTouchEvent
方法都是返回 false,不去攔截該事件的。
接下來就是判斷改事件是不是一個取消事件:
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
然後再去判斷該事件是不是作用與多個視圖:
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
最後如果即通過了安全判斷也不是取消事件以後就是 開始進入事件分發的邏輯。
if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE)
當事件的類型是按下或者是移動的時候進入事件的分發,首先系統就會清除之前觸摸點的信息,然後判斷當前觸摸點是否大於 0,之後就去獲取當前觸摸點的座標。並且獲取到可以接受到該觸摸事件的子 View 的集合 preorderedList,以及判斷是否對自定義 View 繪製順序有要求。
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
然後就開始對這個子 View 列表進行遍歷。然後通過索引獲取到每一個子 View
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
getAndVerifyPreorderedIndex
方法裏面就利用到了我們之前的 customOrder 變量,如果當前的 View 的繪製是有自定義順序的話就要通過 getChildDrawingOrder
方法去獲取,這個方法就是在我們自定義繪製順序的時候需要重寫的方法,否則索引就是列表的下標。
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
final int childIndex;
if (customOrder) {
final int childIndex1 = getChildDrawingOrder(childrenCount, i);
if (childIndex1 >= childrenCount) {
throw new IndexOutOfBoundsException("getChildDrawingOrder() "
+ "returned invalid index " + childIndex1
+ " (child count is " + childrenCount + ")");
}
childIndex = childIndex1;
} else {
childIndex = i;
}
return childIndex;
}
然後就是通過 getAndVerifyPreorderedView
方法去獲取對應的 View。
private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children, int childIndex) {
final View child;
if (preorderedList != null) {
child = preorderedList.get(childIndex);
if (child == null) {
throw new RuntimeException("Invalid preorderedList contained null child at index "
+ childIndex);
}
} else {
child = children[childIndex];
}
return child;
}
然後就是判斷這個 View 能否接受到觸摸事件以及當前的觸摸事件是不是在這個 View 範圍之內。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
如果兩個方法都返回了 true,就說明這個 View 就可以處理該事件。然後就要獲取當前 View 的觸摸對象。
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
如果該 View 已經有對應的觸摸對象的話直接退出循環即可,否則的話就表示該 View 還沒有對應的觸摸事件,然後就要去判斷,該 View 有沒有設置不接收觸摸事件的標誌位,如果有的話就清除這個標誌。
然後就是最主要的一個方法了,dispatchTransformedTouchEvent
方法裏面講述了一個事件是如何從一個 ViewGroup 傳遞到一個具體的 View 中是如何過度的。
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
首先我們會判斷該事件是否爲一個取消事件,如果是取消事件的話,就要去判斷是否有 View 處理,如果 child 爲 null 則直接用 ViewGroup 父類的 dispatchTouchEvent
方法處理,否則調用 View 的方法,最後直接返回結果。
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
如果不是取消事件的話,就會去獲取一個新的指針位,如果指針位爲 0 的話,直接返回 false
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
然後呢就要去判斷原有的指針位和新的指針位是不是一樣的,如果是一樣的就要去判斷子視圖是否存在,如果沒有的話還是調用父類的 dispatchTouchEvent
方法處理,否則就要去計算子 View 的偏移量,然後調用子 View 的 dispatchTouchEvent
方法處理。
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
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);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
如果前面的兩個條件都不滿足,沒有返回值的話,就要創建出一個 MotionEvent 類,然後再去判斷子 View 是否爲空,如果爲空的話還是調用父類的 dispatchTouchEvent
方法處理,否則就要去計算子 View 的偏移量,然後調用子 View 的 dispatchTouchEvent
方法處理。最後釋放相關資源。
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
然後如果 dispatchTransformedTouchEvent
方法返回了 true 的話,就代表了已經傳遞到了子 View 的 dispatchTouchEvent
方法了,也就代表了該事件已經被消費了,所以就可以直接結束。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
這樣我們就解決了當前事件不爲取消事件以及子 View 允許事件傳遞的情況。然後就要看通用的情況了。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
在這個時候 mFirstTouchTarget 還是爲 null 的話,就說明沒有子 View 去執行這個事件,就要通過 dispatchTransformedTouchEvent
方法去消費事件獲取返回值,我們就可以發現他的 View 的傳值爲 null,在之前講的 dispatchTransformedTouchEvent
方法裏面,就會交與他的父類去執行 dispatchTouchEvent
方法。
如果不爲空的話就會遍歷整個鏈表,假如在之前已經處理了的話,他就會直接返回 true,否則的話就會去重新分發事件。
else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
View
流程圖
最後就到了最後一步 View 了。Android 中事件在 View 裏面會怎麼進行處理呢?首先和前面是一樣的,就是 dispatchTouchEvent
方法,這個方法就是標誌着事件已經到了 View 這一層。然後呢?就是 onTouchEvent
方法了。這個方法裏面就是 Android 系統處理觸摸事件的相關邏輯。
下面就是 View 的事件分發流程圖:
源碼分析
首先判斷該 View 是否有可相應焦點,如果沒有的話直接返回 false。
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
然後對事件的類型進行判斷,如果當前的事件爲按下事件的話,如果存在視圖滾動效果的話就要立刻停止滾動
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
之後就開始進入真正的事件分發的過程了,首先和 ViewGroup 是一樣的,通過 onFilterTouchEventForSecurity
方法來進行事件的安全判斷。這塊的邏輯和 View Group 中是一樣的,如果不符合的話就返回 false ,如果符合的話就要接下來進行事件的處理了。
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)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
首先就是判斷當前的操控方式是不是爲鼠標操作,如果爲鼠標操作的話,返回值就可以直接爲 ture 了,表示消費了該事件。然後就要檢查是否有觸摸事件的監聽,然後就要調用 listener 的 onTouch
方法,如果結果返回的是 true,則我們的返回結果也爲 true。如果還是不去消費該事件的話,就要調用 View 的 onTouchEvent
方法,根據返回的結果來進行返回。
然後就要進入 View 的 onTouchEvent
方法裏面來看了
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
首先獲取到點擊的位置座標,View 的標誌位,事件類型以及可點擊狀態,然後去判斷該 View 是否處於一個禁用狀態的話,返回結果就爲可點擊的狀態,這樣就可以說明,當 View 是處於一個禁用狀態的話,如果是可點擊的,也會去消費這個事件,但是因爲是直接返回的,所以說不會去多事件有所響應。
然後就會去判斷有沒有設置觸摸的代理。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
如果有的話就要通過代理的 onTouchEvent
方法去獲取結果,如果能夠消費事件的話就直接返回 true,否則繼續後面的事件處理。
最後就是去判斷是否爲一個可點擊的狀態或者在標記位上 TOOLTIP 位爲 1 的話,就會開始根據事件類型的不同做不同的處理,然後返回 true,表示事件已經被消費,否則返回結構爲 false。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
case MotionEvent.ACTION_DOWN:
...
case MotionEvent.ACTION_CANCEL:
...
case MotionEvent.ACTION_MOVE:
...
}
return true;
}
return false;
}