原始碼角度再看Android事件分發機制
基礎瞭解
MotionEvent
所謂點選事件分發,其實就是對MotionEvent分發。當一個MotionEvent產生了以後,系統需要把這個事件傳遞給一個具體的View,而這個傳遞的過程就是分發過程。
三種主要事件Action
package android.view;
public final class MotionEvent extends InputEvent implements Parcelable {
...
/**
* Constant for {@link #getActionMasked}: A pressed gesture has started, the
* motion contains the initial starting location.
* <p>
* This is also a good time to check the button state to distinguish
* secondary and tertiary button clicks and handle them appropriately.
* Use {@link #getButtonState} to retrieve the button state.
* </p>
* 手指剛接觸螢幕
*/
public static final int ACTION_DOWN = 0;
/**
* Constant for {@link #getActionMasked}: A pressed gesture has finished, the
* motion contains the final release location as well as any intermediate
* points since the last down or move event.
* 手指在螢幕上移動
*/
public static final int ACTION_UP = 1;
/**
* Constant for {@link #getActionMasked}: A change has happened during a
* press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
* The motion contains the most recent point, as well as any intermediate
* points since the last down or move event.
* 手指從螢幕上鬆開的一瞬間
*/
public static final int ACTION_MOVE = 2;
...
}
- 當手指點選屏幕後鬆開,事件序列為DOWN->UP
- 當手指點選螢幕,滑動一會,再擡起手指離開螢幕,時間序列為DOWN->MOVE->…MOVE->UP
getX/getY | getRawX/getRawY
- getX/getY 返回的是相當於當前View左上角的x和y座標
- getRawX/getRawY 返回的是相對於手機螢幕左上角的x和y座標
三個方法瞭解一下
android.view.View # public boolean dispatchTouchEvent(MotionEvent event)
用來進行事件的分發。如果事件能夠傳遞給當前View,那麼此方法一定會被呼叫。
android.view.ViewGroup # public boolean onInterceptTouchEvent(MotionEvent ev)(MotionEvent ev)
在dispatchTouchEvent內部呼叫,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼同一個事件序列當中,此方法不會被再次呼叫,返回結果表示是否攔截當前事件**(true表示攔截,false表示繼續向下分發給它的子元素)**。
android.view.View # public boolean onTouchEvent(MotionEvent event)
還是在dispatchTouchEvent內部呼叫,用來處理點選事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。
一段經典的虛擬碼:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
從上面的程式碼可以看出,對於一個根ViewGroup,點選事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent被呼叫,如果這個ViewGroup的onInterceptTouchEvent執行後返回結果,並根據這個結果進行不同方式的處理: **a.**若返回結果為為true,則表示攔截,這時候根據程式碼顯示就是終止對事件的分發並且自身呼叫onTouchEvent對本此事件進行處理。 **b.**若返回結果為false,根據程式碼顯示,則會去呼叫child.dispatchTouchEvent(ev),也就是繼續向下分發給自己的子View元素,再往後就是子元素去進行處理直到這個事件結束。 _
一些結論
-
(1)同一個事件序列是指從手指接觸螢幕的那一刻起,到手指離開螢幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序列以down事件開始,中間含有數量不定的move事件,最終以up事件結束。
-
(2)正常情況下,一個事件序列只能被一個View攔截且消耗。這一條的原因可以參考(3),因為一旦一個元素攔截了某此事件,那麼同一個事件序列內的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理,但是通過特殊手段可以做到,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
-
(3)某個View一旦決定攔截,那麼這一個事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的onInterceptTouchEvent不會再被呼叫。這條也很好理解,就是說當一個View決定攔截一個事件後,那麼系統會把同一個事件序列內的其他方法都直接交給它來處理,因此就不用再呼叫這個View的onInterceptTouchEvent去詢問它是否要攔截了。
-
(4)某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會被呼叫。意思就是事件一旦交給一個View處理,那麼它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它來處理了,這就好比上級交給程式設計師一件事,如果這件事沒有處理好,短期內上級就不敢再把事情交給這個程式設計師做了,二者是類似的道理。
-
(5)如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點選事件會消失,此時父元素的onTouchEvent並不會被呼叫,並且當前View可以持續收到後續的事件,最終這些消失的點選事件會傳遞給Activity處理。
-
(6)ViewGroup預設不攔截任何事件。Android原始碼中ViewGroup的onInterceptTouch-Event方法預設返回false。
-
(7)View沒有onInterceptTouchEvent方法,一旦有點選事件傳遞給它,那麼它的onTouchEvent方法就會被呼叫。
-
(8)View的onTouchEvent預設都會消耗事件(返回true),除非它是不可點選的(clickable 和longClickable同時為false)。View的longClickable屬性預設都為false,clickable屬性要分情況,比如Button的clickable屬性預設為true,而TextView的clickable屬性預設為false。
-
(9)View的enable屬性不影響onTouchEvent的預設返回值。哪怕一個View是disable狀態的,只要它的clickable或者longClickable有一個為true,那麼它的onTouchEvent就返回true。
-
(10)onClick會發生的前提是當前View是可點選的,並且它收到了down和up的事件。
-
(11)事件傳遞過程是由外向內的,即事件總是先傳遞給父元素,然後再由父元素分發給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是ACTION_DOWN事件除外。
深入瞭解
事件分發流程梳理1—從Activity到頂級ViewGroup(View)
點選事件最先傳遞給Activity,由Activity的dispatchTouchEvent來進行事件派發,具體工作是由Window來完成。先知道這麼多,我們就以Activity作為入口,然後就邊看程式碼變分析吧。
Activity#dispatchTouchEvent
package android.app;
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
......
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
......
}
一個判斷,當事件DOWN來襲的時候。呼叫onUserInteraction()
,首先它是一個空實現方法,這種方法一般是系統留給我們複寫的介面方法。然後它跟另外一個方法有關係onUserLeaveHint
,這倆都是在特定情境下可以複寫的介面方法。
-
Activity#onUserInteraction() activity在分發各種事件的時候會呼叫該方法,注意:啟動另一個activity,Activity#onUserInteraction()會被呼叫兩次,一次是activity捕獲到事件,另一次是呼叫Activity#onUserLeaveHint()之前會呼叫Activity#onUserInteraction()。
-
Activity#onUserLeaveHint() 使用者手動離開當前activity,會呼叫該方法,比如使用者主動切換任務,短按home進入桌面等。系統自動切換activity不會呼叫此方法,如來電,滅屏等。
然後我們看到這一行:
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
getWindow()返回一個Window,然後看Window下的superDispatchTouchEvent。
Window#superDispatchTouchEvent
package android.view;
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
......
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
......
}
一個抽象方法,我們去找他的實現類。看頂上的註釋:
The only existing implementation of this abstract class is android.view.PhoneWindow
PhoneWindow#superDispatchTouchEvent
package com.android.internal.policy
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
......
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
......
}
科普一波:
DecorView是啥?它一般是當前介面最底層的容器(即setContentView之後產生的View的父容器)。可以通過程式碼Activity$View decorView = getWindow().getDecorView(); 獲得
繼續看,mDecor.superDispatchTouchEvent(event);
,看看DecorView嘛。
PhoneWindow#superDispatchTouchEvent
package com.android.internal.policy
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
......
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
......
}
它呼叫了super.dispatchTouchEvent(event)
,看它的父類,這裡DecorView extends FrameLayout
,FrameLayout是個ViewGroup,本質上也是個View,所以到這兒,事件就從Activity傳遞到了ViewGroup層。
事件分發流程梳理2-ViewGroup層的事件下發流程
分發邏輯回顧
點選事件達到頂級View(一般是一個ViewGroup)以後,會呼叫ViewGroup的dispatchTouchEvent方法,然後的邏輯是這樣的:
如果頂級ViewGroup攔截事件即onInterceptTouchEvent返回true,則事件由ViewGroup處理
- 這時如果ViewGroup的mOnTouchListener被設定,則onTouch會被呼叫。
- 否則onTouchEvent會被呼叫。在onTouchEvent中,如果設定了mOnClickListener,則onClick會被呼叫。
- 如果都提供的話,onTouch會遮蔽掉onTouchEvent。
如果頂級ViewGroup不攔截事件,返回false
- 則事件會傳遞給它所在的點選事件鏈上的子View,這時子View的dispatchTouchEvent會被呼叫。到此為止,事件已經從頂級View傳遞給了下一層View,接下來的傳遞過程和頂級View是一致的,如此迴圈,完成整個事件的分發。
ViewGroup—onInterceptTouchEvent呼叫流程分析(是否攔截事件)
在ViewGroup內搜到方法dispatchTouchEvent
,然後ctrl+f // Check for interception.
// 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;
}
- actionMasked == MotionEvent.ACTION_DOWN 當前事件是按下事件。
- mFirstTouchTarget != null 當事件由ViewGroup的子元素處理成功時,mFirstTouchTarget會被賦值指向子元素,於是(ViewGroup沒攔截時)mFirstTouchTarget!=null,當ViewGroup攔截時,則mFirstTouchTarget==null
- if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) 這層if的意思是:當摁下ACTION_DWON時,就不去考慮之前有沒有攔截過該事件,都具有攔截的資格(能夠呼叫onInterceptTouchEvent),噹噹前事件時UP或者MOVE時,則需要考慮之前有沒有攔截過事件,若沒攔截就還有機會去攔截該事件,若之前攔截過了,則不再考慮再次呼叫onInterceptTouchEvent方法,本次後續的時間序列都攔截
- 當onInterceptTouchEvent不再考慮被呼叫時,那麼走到else裡邊,
intercepted = true
,那麼當這個intercepted為true時,看他註釋:
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
我們主要看後一句,this view group continues to intercept touches
,該ViewGroup繼續攔截觸控事件,意思就是後續的事件序列繼續攔截。也就是說intercepted=true
表示當前事件序列攔截。
- 假設當前具備呼叫
OnInterceptTouchEvent
方法的資格時,還有一層判斷:final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
,這層判斷是啥意思呢?它產出一個值disallowIntercept
,用於判斷:
ViewGroup#dispatchTouchEvent
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
- 這個標記位
FLAG_DISALLOW_INTERCEPT
是通過方法requestDisallowInterceptTouchEvent(boolean disallowIntercept)
來設定的,當requestDisallowInterceptTouchEvent(true)
時,if (!disallowIntercept)
為false,那麼表示不攔截intercepted = false
,這也是這個方法名字的由來,“請求不攔截觸控事件”。但是,有時候我們子View呼叫這個方法會失效,為什麼?因為,當面對事件ACTION_DOWN
的時候這個標記會被重置,也就是resetTouchState
這個方法,那麼requestDisallowInterceptTouchEvent(boolean disallowIntercept)
這個方法豈不是很雞肋?這裡先TODO一下,等看到滑動衝突的時候再來看如何利用這個方法,原始碼:
重置FLAG_DISALLOW_INTERCEPT
為false
ViewGroup#dispatchTouchEvent
// Handle an initial down.
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();
}
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
寫FLAG_DISALLOW_INTERCEPT
ViewGroup#requestDisallowInterceptTouchEvent
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
ViewGroup—DispatchTouchEvent呼叫流程分析(分發到View)
- 遍歷childCount,ViewGroup的所有子元素。
- 判斷子元素是否能夠接收到事件 a.座標是否在區域內 b.是否在實行動畫 這個判斷是個||的關係,這倆個條件有一個不成立時,則忽略接下來的邏輯直接進入下一次子元素迴圈(continue)
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
- 若當前事件的座標在區域內,並且沒有在執行動畫,那麼就去呼叫dispatchformedTouchEvent**注意這裡dispatchformedTouchEvent()很重要,可以說ViewGroup內比較核心的一個方法,看下面的程式碼,如果child為空則呼叫super.dispatchTouchEvent,如果不為空則呼叫child.dispatchTouchEvent。那麼我可以猜測ViewGroup寫這個方法的目的是啥?因為一個MotionEvent的分發最終目的還是要被處理掉(onTouchEvent),child是ViewGroup時就遞迴執行ViewGroup的dispatchTouchEvent,直到是View時呼叫View的dispatchTouchEvent,而View的dispatchTouchEvent內有onTouchEvnet()的邏輯。ViewGroup內也有當intercept為true時呼叫super.onTouchEvent()方法的邏輯。這樣,就能保證事件最終總能有個地方被處理。** 另外,我們在ViewGroup內是找不到onTouchEvent方法的,但是在它的父類View裡可以找到,我們前面分析了當onIntercept方法攔截了事件之後,則會自己處理onTouchEvent(),這裡也是通過子類調父類的形式來呼叫的。
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
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);//實際上就是呼叫的子元素的dispatchTouchEvent
}
event.setAction(oldAction);
return handled;
}
......
return handled;
}
- 如果子元素的dispatchTouchEvent方法返回true,那麼mFirstTouchTarget就會被賦值同時跳出(break)for迴圈,賦值過程真實發生在addTouchTarget, 注意這裡很重要,mFirstTouchTarget的賦值直接影響事件攔截邏輯,並且ViewGroup向View的事件分發的過渡點也在這個mFirstTouchTarget
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
-
如果子元素的dispatchTouchEvent返回false,ViewGroup就會將事件分發給下一個子元素。(如果還有下一個子元素的話)
-
最後附上ViewGroup的DispatchTouchEvnetn的程式碼整體概況,已做好註釋:
ViewGroup#dispatchTouchEvent
if (!canceled && !intercepted) {
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
......
final View[] children = mChildren;
//遍歷childCount,ViewGroup的所有子元素
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判斷子元素是否能夠接收到事件 a.座標是否在區域內 b.是否在實行動畫
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
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);
//dispatchTransformedTouchEvent實際上就是呼叫的子元素的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();
//如果子元素的dispatchTouchEvent方法返回true,那麼mFirstTouchTarget就會被賦值同時跳出for迴圈,賦值過程真實發生在addTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
//如果子元素的dispatchTouchEvent返回false,ViewGroup就會將事件分發給下一個子元素。(如果還有下一個子元素的話)
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
......
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//No touch targets so treat this as an ordinary view.
//mFirstTouchTarget == null,表示上面那一波遍歷並沒有子View正常的處理了分發下去的事件
//a.包含倆種情況,ViewGroup沒有子View
//b.子元素了處理了事件,但子View的DispatchTouchEvent返回了false(一般是由於子View的OnTouchEvent返回了false)
//可以看到這裡第三個引數是null,當著引數為null的時候,該方法內回去調handled = super.dispatchTouchEvent(event);
//由於View是ViewGroup的父類,所以就轉跳到了View的事件分發流程裡去了
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
......
target = next;
}
}
事件分發流程梳理3-View事件分發和處理流程
從ViewGroup的dispatchTransformTouchEvent方法傳遞到View這裡,事件已經到達View,可能是父元素分發給子元素接著子View自己呼叫自己,也可能是父元素自己處理(子類ViewGroup呼叫父類View)。
到了View這一層,重點不在於如何分發,而是在於消費機制與處理,重點方法onTouchEvent()。
dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.