View的事件體系之三 android事件分發機制詳解(下)
接著上一篇來分析事件分發機制,在看了各位大牛的關於事件分發機制的分析後茅塞頓開,之前看過好幾遍郭霖,弘揚以及玉剛大神關於事件體系的講解,一直看不懂,比較模糊,最近複習時,看到一篇博文,寫的相當精彩,看完後,再回看各位大神的博文,收穫頗豐,記錄一下自己的理解和感受,不喜勿噴。文尾會貼一下各位大牛相關部落格地址,方便大家檢視。首先看一下View事件體系相關的知識點
接下來我會從三個方面去分析事件分發機制
1.從整體理解事件分發機制的原理
手機系統由於手指點選觸控而產生一系列的迴應稱之為事件,而Android中將一系列的Touch事件細節(包括觸控事件,位置等等)封裝為一個MotionEvent物件,而發生的主要的Touch事件有:
- MotionEvent.ACTION_DOWN:按下事件
- MotionEvent.ACTION_MOVE:移動事件
- MotionEvent.ACTION_UP: 擡起事件
- MotionEvent.ACTION_CANCEL:當前事件已終止,可以看做一個UP事件,但是不執行任何操作。
而事件的分發的過程本質 就是一個MotionEvent發生之後,在系統中傳遞最後至一個具體的View消費掉,即響應的過程。
一個完整的事件列由幾部分組成:
一個事件在這幾個物件中的傳遞過程是 Activity(PhoneWindow)–>ViewGroup–>View;
而事件在這幾個物件之間傳遞時是由哪幾個方法具體協調工作完成事件傳遞,最終被消費的呢?這裡就需要介紹整個事件傳遞過程中的核心:dispatchTouchEvent(),onInterceptTouchEvent(),以及onTouchEvent(),三個方法。
方法 | 作用 | 返回結果說明 |
---|---|---|
dispatchTouchEvent() | 傳遞事件 | true : 1.消費事件,事件將不會向下分發 2.後續事件列中的其他事件繼續分發至該view. false: 1.不消費事件,且事件停止傳遞 2.將事件回傳給父控制元件的OnTouchEvent() 處理 3.當前View仍然接受事件列中的後續事件。 預設實現: 根據當前呼叫物件不同返回也不同 1.Activity:呼叫superDispatchTouchEvent(ev)。 2.ViewGroup:預設呼叫自身的onInterceptTouchEvent() 3.View:預設呼叫自身的onTouchEvent()方法。 |
onInterceptTouchEvent() | 攔截事件分發 | true: 1.攔截事件,事件將不會再向下傳遞 2.自己處理事件,即執行自身的onTouchEvent()方法。 3.同一事件列中的後續事件也由該View處理,且該方法不會在此事件列中再被呼叫。 false(預設): 1.不會攔截,事件繼續向下分發 2.當前View仍然接受事件列中的後續事件。 |
onTouchEvent() | 處理事件 | true: 1.攔截事件,事件將不會再向下傳遞。 3.同一事件列中的後續事件也由其處理 false: 1.不會攔截,事件繼續向上傳遞給父控制元件的OnTouchEvent()處理 2.當前View不再接受事件列中的後續其他事件。 |
1.2流程解析
前面已經介紹過,Android的事件分發是由Activity,ViewGroup,View三者之間的傳遞來構成的,通過dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()這三個核心的方法來配合完成的。而具體的分發過程是:Activity–>ViewGroup–>View,具體的流程是什麼樣?方法之間由於返回值的不同是如何響應的呢?我們從一張圖入手。
根據這張圖我們來詳細的瞭解一下整個分發過程:
當你開始點選螢幕時,事件開始分發,首先會將按下事件即ACTION_DOWN進行分發,當傳到Activity的dispatchTouchEvent()時,返回false,呼叫自身的onTouchEvent()方法,事件被消費,後續事件不再向下分發。返回true,事件將被消費,且後續事件(Move、Up)會繼續分發到該View。預設情況下會呼叫Window類的superDispatchTouchEvent(),而Window為抽象類,它具體的實現類為PhoneWindow類(關於Activity,Window,PhoneWindow以及DecorView上面已經介紹過了,再不贅述),在PhoneWindow中我們可以看到,它又呼叫了DecorView中的superDispatchTouchEvent,再繼續跟蹤發現呼叫到了ViewGroup的dispatchTouchEvent()。
ViewGroup中的dispatchTouchEvent()返回true時,事件被消費,後續事件(Move、Up)會繼續分發到該View,返回false時,事件沒有被消費,但是也不會向下分發,會將事件回傳給父控制元件的onTouchEvent()去處理同樣後續事件(Move、Up)會繼續分發到該View,倘若呼叫super即呼叫父類的方法,會呼叫自身的onInterceptTouchEvent(),(相比Activity和View唯一不同的就是ViewGroup有一個攔截事件分發的方法onInterceptTouchEvent()),當onInterceptTouchEvent()返回true時,事件被攔截,將不會再向下分發,直接呼叫自身的onTouchEvent()處理,同一事件列中的其他事件將由該View來處理,在同一事件列中該方法將不會在被呼叫。返回為false時將不攔截事件,事件繼續向下分發
當事件分發至View中的dispatchTouchEvent()時,也是有三種情況,返回為true,即消費事件,事件不向下分發,後續事件會繼續分發到該View,返回false,事件沒有被消費,但是也不會向下分發,會將事件回傳給父控制元件的onTouchEvent()去處理,同樣後續事件(Move、Up)會繼續分發到該View,,返回super時呼叫自身的onTouchEvent()
倘若View的onTouchEvent()返回為true,事件被消費,自己處理(消費)該事件,該事件列的後續事件(Move、Up)讓其處理;返回false/super時,不處理該事件。呼叫父類onTouchEvent(),事件往上傳遞給父控制元件的onTouchEvent()處理,且不再接受此事件列中的後續事件。
當View中返回false時,事件被回傳至ViewGroup的onTouchEvent(),接下來同View中的一樣。
當事件傳遞完一圈沒人消費時,最終會被Activity的oTouchEvent()處理。
注意:整個傳遞過程會有特別需要注意的部分:
1、onInterceptTouchEvent()方法對DOWN事件返回了false,後續的事件(MOVE、UP)依然會傳遞給它的onInterceptTouchEvent(),一旦返回true,該事件列的其他事件(Move、Up)將不會再傳遞給B的onInterceptTouchEvent方法,在同一事件列此方法就再也不會被呼叫了,而onTouchEvent()對DOWN事件返回了false,將不會再接受事件列中的後續事件。
2、攔截DOWN的後續事件,這裡需要注意,ViewGroup的onInterceptTouchEvent方法返回true攔截後續事件比如MOVE事件,該事件並沒有傳遞給此ViewGroup的onTouchEvent();這個MOVE事件將會被系統變成一個CANCEL事件向下傳遞給的View的onTouchEvent方法。後續又來了一個MOVE事件,該MOVE事件才會直接傳遞給B的onTouchEvent()。
到這基本已經能理解整個事件的傳遞過程了,接下來我們將走進原始碼,從原始碼中再次深入理解事件分發機制。
2.3 走進原始碼理解事件分發機制
2.3.1 Activity中事件分發原始碼分析
新建一個TouchActivity,讓其繼承自Activity,前面也說過,一個點選事件是從Activity的dispatchTouchEvent()開始的,進入Activity的原始碼,我們看到:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
從上面原始碼中看:當點選事件發生傳遞時,第一個if條件肯定為真,那麼肯定會進入onUserInteraction():
/*
......
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
可以看到這個方法並沒有實現,這個方法的主要作用是:只要使用者與Activity有互動就會呼叫,Activity在分發各種事件的時候會呼叫它,與Android系統中輔助功能的相關的互動時會用到。
接著看Activity中dispatchTouchEvent()第二個if條件:
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
呼叫了Window類的superDispatchTouchEvent()方法,而Window是抽象類,他的具體實現類是PhoneWindow,我們可以從PhoneWindow中找到:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可以看出又呼叫了DecorView的superDispatchTouchEvent()再往下追,來到DecorView的superDispatchTouchEvent():
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
這裡會有三種情況,前兩種情況看下圖:
第三種是預設情況,呼叫super.dispatchTouchEvent(event)進入ViewGroup的dispatchTouchEvent()。那麼ViewGroup的dispatchTouchEvent()什麼時候返回true什麼時候返回fasle呢?請繼續往下看。
2.3.2 ViewGroup事件分發原始碼分析
接下來將走進ViewGroup的dispatchTouchEvent()方法,在這使用Android 7.1.1 (Nougat)版本的原始碼。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//(1)這部分是除錯用的,忽略程式碼
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
//確定事件分發的目標檢視是不是一個可以訪問的檢視,如果是,則開始正常的事件排程
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
/**View中的方法,應用安全策略過濾觸控事件,
有兩個返回值,true表示這個事件被分發,false表示被過濾掉**/
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 處理初始的Down事件.
if (actionMasked == MotionEvent.ACTION_DOWN) {
//在當開始一個新的觸控手勢前,清除之前的Touch狀態。
//由其他狀態的改變,比如切換app,ANR,系統會清除掉之前的UP和cancel事件。
cancelAndClearTouchTargets(ev);
resetTouchState();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~超級無敵分割線~重點來了~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ViewGroup中才會有的操作,檢查是否要攔截事件分發
final boolean intercepted;
//倘若是按下事件或者時間分發目標的列表中的第一個觸控目標不為null,開始執行
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//disallowIntercept 有兩個值,false不允許攔截,true允許攔截,disallowIntercept 可以通過viewGroup.requestDisallowInterceptTouchEvent(boolean);進行設定
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//不允許攔截
//再去判斷onInterceptTouchEvent方法,也有兩返回值,true,攔截,false不攔截。
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); //在更改之後恢復action
} else {
intercepted = false;
}
} else {
//後續事件就不會往子View傳遞了,並且DOWN,MOVE,UP子View都不會向下分發。
intercepted = true;
}
//如果被攔截,啟動正常的事件排程,即事件將不會向下分發
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 去檢查cancel事件。
//重新設定下一次cancel的標記,如果之前設定過就會返回true。
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// // 如果需要.更新指向Down的分發目標的指標列表,
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
//不cancel,也不攔截,接下來就會去遍歷整個ViewGroup,找到事件要傳遞到那的那個目標子View。
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//由於事件在MotionEvent中是以靜態的整型存在的,這地方是為了得到分發事件的整型然後根據這個int型別的數值查詢對應的pointerId
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
//為了保證分發目標的同步,清除掉之前這個pointerId所對應的觸控目標。
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//遍歷ViewGroup,將接收該事件的子View返回
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
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;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
//如果子View可以接受事件,那麼我們就給他一個觸控的標識。
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//接下來他會通過呼叫dispatchTransformedTouchEvent把事件分配給子View。
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沒有處理該事件,清除標記,想所有的子View分發,做一個正常的事件排程
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
//沒有發現子View去接收該事件,將指標指向上一次分發目標View
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
if (mFirstTouchTarget == null) {
......
} else {
//如果事件的分發目標已經分發過了,將不會再分發
......
}
}
// 如果需要.更新指向UP和cancel的分發目標的指標列表,
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
注意:
(1) dispatchTransformedTouchEvent方法說明:我們會發現,他先判斷狀態是否取消,如果取消了,把當前事件變成取消狀態,然後在判斷是否有子view。如果有子view的話直接呼叫子view的dispatch事件。下面就是多指了,一個pointer對應一個ID,防止處理衝突。我印象中能簡單粗暴的處理多指,應該是ViewDragHelper了。具體,你們可以自己去看。後面就如之前一樣,判斷child是否為null。然後得到是執行自身的事件還是child的事件
(2) ViewGroup靜態內部類TouchTarget說明:
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
//儲存著當前的所有touchtarget的物件
private static TouchTarget sRecycleBin;
//sRecycledCount來計數
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// 指向目標列表中的下一個目標
public TouchTarget next;
//核心方法說明:
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) {
throw new IllegalArgumentException("child must be non-null");
}
final TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
//新生成的target 的中next指向以前的以前存在的物件,就是依次往回加的感覺
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
}
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
//把連結串列中最上面的去掉,是誰呼叫recycle()的這個物件在連結串列被去除了
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
//事件攔截的方法
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())) {
//(1)按鍵處理判斷,主要判斷輸入源是否是滑鼠定位裝置,也可用於其他的滑鼠指向裝置如觸控板,小紅點等
//(2)判斷該事件是否是Down事件
//(3)檢測使用者是否按下按鈕
return true;
}
return false;
}
ViewGroup事件分發總結:
2.3.3 View事件分發原始碼分析
從上面ViewGroup事件分發機制知道,View事件分發機制從dispatchTouchEvent()開始,不重要的原始碼已經省略
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
//應用安全策略過濾觸控事件
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
//判斷View是可點選的,並且是通過滑鼠輸入或者指向裝置來處理滑動的
result = true;
}
//最主要點,ListenerInfo是View的靜態內部類,主要封裝了view中所有的點選事件的介面。
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//(1)首先先確保ListenerInfo的物件不能為null,並且判斷mOnTouchListener不為null,並且view是enable的狀態
//(2)然後li.mOnTouchListener.onTouch(this, event)返回true,也就是說這三個條件均成立,直接返回true,後面的額onTouchEvent(event)將不再執行。
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
注意
(1)在View中onTouch的執行是在onTouchEvent之前的.
(2)li.mOnTouchListener分析:
//也就是我們在Activity中設定的setOnTouchListener
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
如果我們在onTouch()中返回true,那麼View中的onTouchEvent就不會執行,事件分發結束,如果返回false,就會來到View的onTouchEvent()方法,看一下原始碼:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//View的狀態為DISABLED,也就是說View仍然可以接收到觸控事件,但是不會響應
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//如果設定了mTouchDelegate,則會將事件交給代理者處理,直接return true,如果希望自己的View增加它的touch範圍,可以嘗試使用TouchDelegate
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//接下來到重點了,如果我們的View可以點選或者可以長按,進入if語句
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
//判斷mPrivateFlags是否包含PREPRESSED
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepresed) {
// 如果包含PRESSED或者PREPRESSED則進入執行體
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
//如果mHasPerformedLongPress沒有被執行,進入IF
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//移除長按的檢測
removeLongPressCallback();
if (!focusTaken) {
//如果mPerformClick為null,初始化一個例項,然後立即通過handler新增到訊息佇列尾部
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//如果新增失敗則直接執行 performClick(),否則,將執行PerformClick的run方法中performClick();
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
//我們的mPrivateFlags中的PRESSED取消,然後重新整理背景,把setPress轉發下去。
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
//如果mPendingCheckForTap不為null,移除;
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
//標記是否為長按事件
mHasPerformedLongPress = false;
//檢查Down事件是否被消耗,true是被消耗
if (performButtonActionOnTouchDown(event)) {
break;
}
boolean isInScrollingContainer = isInScrollingContainer();
//對於滾動容器內的檢視, 短時間內延遲按下的反饋, 以防這是滾動
if (isInScrollingContainer) {
//給mPrivateFlags設定一個PREPRESSED的標識
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
//傳送一個延遲為ViewConfiguration.getTapTimeout()的延遲訊息,到達延時時間後會執行CheckForTap()裡面的run方法
//CheckForTap會在後面說明
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
//拿到當前觸控的x,y座標;
drawableHotspotChanged(x, y);
// 判斷當然觸控點有沒有移出我們的View
if (!pointInView(x, y, mTouchSlop)) {
//如果移出了,移除DOWN觸發時設定的PREPRESSED的檢測
removeTapCallback();
//然後判斷是否包含PRESSED標識
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
//如果包含,移除長按的檢查,最後把mPrivateFlags中PRESSED標識去除,重新整理背景
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
performClick()方法說明
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
//從這裡可以看出,onClick點選事件是在performClick方法中執行的,也就是說只要我們通過setOnClickListener()為控制元件View註冊1個點選事件那麼就會給mOnClickListener變數賦值(即不為空) 則會往下回調onClick()並且performClick()返回true
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
view事件分發總結:
2.4 從Demo入手理解事件分發機制
- 首先建立一個專案。
- 自定義一個TouchLayout的類,讓其繼承自LinearLayout,實現其構造方法,以及重寫相應的dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent(),監聽三個方法中相應的按下,擡起和移動事件。
- 同樣的再自定義一個TouchButton的類,讓其繼承自AppCompatButton,實現其構造方法,以及重寫相應的dispatchTouchEvent(),onTouchEvent(),監聽兩個方法中相應的按下,擡起和移動事件
- 然後我們在Activity的xml檔案中佈局如下
<widget.TouchLayout
android:id="@+id/vg_b"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/a"
tools:context="byd.com.byd.viewtest.TouchActivity">
<widget.TouchButton
android:id="@+id/btn_c"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是C"
android:background="@color/c"/>
<widget.TouchButton1
android:id="@+id/btn_c1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是C1"
android:background="@color/c"/>
</widget.TouchLayout>
說明:TouchButton1和TouchButton程式碼一樣。這裡建立TouchButton1只是為了研究事件在ViewGroup中如何遍歷子View找到事件分發的目標View的。
- 在Activity中重寫dispatchTouchEvent()和onTouchEvent()方法,監聽兩個方法中相應的按下,擡起和移動事件。
- 在Activity中對btn_c設定OnTouchListener和OnClickListener監聽。
- 接下來分好幾種情況分別來看。
1.事件正常分發:
我們可以看到:
首先Down事件分發至Activity的dispatchTouchEvent()—–>ViewGroup()的dispatchTouchEvent()—–>ViewGroup的onInterceptTouchEvent()—–>View的dispatchTouchEvent()—–>Activity中的View的onTouch()—–>View的onTouchEvent()然後事件被消費。後面的Move和UP都是一樣的,需要注意的是UP事件在執行完View的onTouchEvent()後執行了View的onClick(),這和我們上面的原始碼分析一致。
2.我們在Activity中的dispatchTouchEvent中返回true和false分別看一下。
可以看到事件並沒有向下分發,直接到Activity的dispatchTouchEvent()結束。
3.在ViewGroup的TouchLayout中的dispatchTouchEvent()中返回true
可以看到事件分發至ViewGroup的dispatchTouchEvent()之後就結束了。並沒有向下分發。
4..在ViewGroup的TouchLayout中的dispatchTouchEvent()中返回false
這裡看到事件的分發流程為:Activity的dispatchTouchEvent()—–>ViewGroup()的dispatchTouchEvent()—–>Activity的onTouchEvent(),也就是說事件最後被Activity消費了。
5.在ViewGroup的onInterceptTouchEvent()中返回true,這裡有兩種情況,
(1) 首先我們攔截Down事件
可以看到事件傳遞流程:Activity的dispatchTouchEvent()—–>ViewGroup()的dispatchTouchEvent()—–>ViewGroup的onInterceptTouchEvent()—–>ViewGroup的onTouchEvent()—–>Activity的onTouchEvent().且事件列中的後續事件並未向下分發,直接由Activity的dispatchTouchEvent()—–>Activity的onTouchEvent(),由Activity消費. 且ViewGroup中的 onInterceptTouchEvent只調用了一次,後續事件列中並未呼叫。
(2)不攔截down事件,攔截同一事件列中的Move事件
可以看到Down事件完整的分發完了,Move事件被攔截,但是Up事件也沒有向下分發,直接傳遞到ViewGroup的onTouchEven(),最後回傳至Activity的onTouchEven()。
6.後續的View中的狀況和ViewGroup類似,這就不贅述了。
2.4 總結
通過這次探索,發現有Android的事件分發機制相當複雜,理解起來也不太容易,如果有哪些點你覺得我的理解不對。歡迎留言指正,最近開通了自己的微信公眾號,偶爾更新文章,生活感悟,好笑的段子,歡迎訂閱
文章中所用到的demo的下載地址
參考資料:
《Android開發藝術探索》
鴻洋- Android View 事件分發機制 原始碼解析
Carson_Ho-Android事件分發機制詳解:史上最全面、最易懂