前言
昨天面試了騰訊Android,基本上是照著簡歷問,但都問的比較深入。其中問到了事件體系,含含糊糊的答了出來(之前有看過藝術探索),但後來自己想想感覺自己答的並不是特別好。雖然面試結果還不知道,但覺得還是應該好好整理一下。
分析的起點
不管是書上還是網上都說事件的起點是ViewGroup的dispatchEvent,但大多數都沒有給出理由,本著探索的精神,我採用了最簡單的方法:斷點除錯。
點選這個View,果然,檢視棧幀: 是通過WindowCallback傳遞到Activity,再專遞到Activity的Window->DecorView,DecorView實際上就是一個FrameLayout,最終呼叫的就是ViewGroup的dispatchTouchEvent,所以下面就可以愉快的分析DispatchEvent啦。ViewGroup#dispatchTouchEvent()
// 前面省略...
final int action = ev.getAction();
// 獲取事件型別
final int actionMasked = action & MotionEvent.ACTION_MASK;
// ACTION_DOWN就是你手機接觸螢幕的事件,通常被認為是一系列觸控事件的起點
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 這裡是重置當前的事件狀態,後面會分析
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 檢查是否攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 這個標誌如果有效,則不會呼叫自己的onInterceptTouchEvent方法
// 可以通過ViewParent#requestDisallowInterceptTouchEvent()修改
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果intercepted為true,就會攔截這一系列事件,具體可以在後面的原始碼到
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 觸控事件不是ACTION_DOWN,並且touchTarget==null
intercepted = true;
}
複製程式碼
可以看到,是否攔截的邏輯還與touchTarget這個成員相關,這個成員是什麼呢?
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
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;
// The next target in the target list.
public TouchTarget next;
複製程式碼
看一下這個類的結構,很容易想到,這是個連結串列節點的結構,而它的child是什麼呢?可以通過後面的程式碼去挖掘,因為mFirstTouchTarget這個成員變數是在後面賦值的,初始為null,所以我們可以把它認為是null,帶著這個條件去走下面的邏輯。 按照ViewGroup的預設情況,不攔截事件,這個先看intercept為false的情況。以下是不攔截本次事件的時候會執行的一段程式碼。
// 首先會判斷是不是ACTION_DOWN或者支援多指時是不是其他手機按下或者是滑鼠按下(Hover是跟滑鼠相關的處理,這裡不用過多關心)
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 獲取按下的手指編號,暫時不用關心
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 獲取子View的列表,順序可以通過ViewGroup提供的介面自定義
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 按一定順序遍歷子View
for (int i = childrenCount - 1; i >= 0; i--) {
...
// 判斷這個View是否接收事件,並判斷事件是否在View對應的那塊矩形內,如果不在,找下一個
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 這個方法實際上是遍歷mFirstTouchTarget這個連結串列,找到child域和當前View相同的TouchTarget,但第一次收到down時,這個會返回null
newTouchTarget = getTouchTarget(child);
// 如果找到了,會把touchTarget響應的手指編號資訊更新
if (newTouchTarget != null)
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 分發事件,如果成功處理,更新事件處理的資訊並退出迴圈,這裡是把事件交給child去分發,具體如何實現這裡不展開,邏輯比較簡單
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
...
複製程式碼
這裡的程式碼就比較長了,但也不是很難懂,重要的地方都在註釋。我們這裡暫時是以第一次點選事件來描述這個流程的,因此去掉了一些與這個流程無關的程式碼。這段程式碼實際上對一些特殊情況進行了處理,這裡我們們先略過。後面雖然還有很多程式碼,但實際上會發現,執行到這個地方,基本就結束了,alreadyDispatchedToNewTouchTarget被置為了true,帶入原始碼讀,可以看到
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
...
}
predecessor = target;
target = next;
}
}
複製程式碼
對於這個流程來講,else分支已經不重要了,到此DOWN事件處理完畢。 當然,我們現在可以回過頭看前面的問題。DOWN事件分發到的時候到底做了什麼呢? 首先是cancelAndClearTouchTargets方法
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
複製程式碼
ViewGroup#clearTouchTargets
// 清空連結串列
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
複製程式碼
可以看到,在這裡會分發event,但是即使event不為null,傳給dispatchTransformedTouchEvent的cancel的值為true,在這個方法處理的時候,會把event的Action設為ACTION_CANCEL,所以我們在處理ACTION_CANCEL的時候,一般要把事件相關的狀態和變數重置。 接下來會呼叫ViewGroup#resetTouchState
private void resetTouchState() {
// 清空連結串列
clearTouchTargets();
// 重置狀態
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
複製程式碼
這裡我們能看到,它會把FLAG_DISALLOW_INTERCEPT這個標誌設定為false,也就是說,它這個時候會呼叫自己的interceptTouchEvent方法。由此我們得出一條結論: ACTION_DOWN事件不能被取消攔截 假設我們按下來,移動手指,這樣就會產生一個move事件,這裡,仍然假設預設不會攔截。
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE)
...
複製程式碼
這一串程式碼自然不會執行,到了下面
if (mFirstTouchTarget == null) {
// 暫不關心
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// alreadyDispatchedToNewTouchTarget這個時候是false,會執行else分支
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;
}
}
複製程式碼
這個時候,會遍歷touchTarget這個連結串列並分發事件,從原始碼中可以看出,只要又一個touchTarget的child成功處理這個事件,handled就是true。 這裡又有疑問了,為什麼touchTarget會用連結串列來存?會有多個touchTarget的情況嗎?這個時候,就要想到之前分析忽略的地方,對多指的支援。首先還是看上面那一長串程式碼的進入條件:
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE)
...
複製程式碼
還有一個ACTION_POINTER_DOWN條件。什麼是POINTER_DOWN呢?首先DOWN是指你第一個手指觸控螢幕,然後你第一根手指不放,按下第二根手指、第三根手指都會產生這樣的事件,並且,還會記錄手指的id。 可以看到上面的split這個變數,這是一個flag,當關心多指時為true(預設true)。接下來,獲取本次pointerId:
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
複製程式碼
idBitsToAssign實際上就是把手指id那一位置1的數。 接下來,會把之前處理過這個手指id的touchTarget清除。
private void removePointersFromTouchTargets(int pointerIdBits) {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 處理了這個手指的事件
if ((target.pointerIdBits & pointerIdBits) != 0) {
// 把這個手指對應的位置0
target.pointerIdBits &= ~pointerIdBits;
// 置0後沒有對應處理的手指id了,則從連結串列中刪除
if (target.pointerIdBits == 0) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
複製程式碼
也不難理解為什麼需要清除前面的,這個方法是為了同步狀態,前面的手指,因為對於當前手指來說,相當於新開始一個DOWN事件,所以前面不應該有處理這個事件的touchTarget,這樣做也是為了保險,可見Google大佬思維的嚴密。接下來的就有三種情況了:
- 情況一:
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
複製程式碼
在遍歷的時候,首先遍歷了已經在touchTarget中的child,這個時候顯然沒有增加新的touchTarget,而是把它的處理的手指對應位置一。而之後的流程如前面分析,遍歷touchTarget,分發事件。
- 情況二: 先遇到了一個沒有在連結串列中的結點,就會像前面處理DOWN事件那樣新增到連結串列中。之後的處理也類似。
- 情況三:
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
複製程式碼
遍歷完子View都沒有找到,這時候把連結串列最後一個(最近新增的)手指資訊對應位置1。 事實上,個人認為比較常見的是情況一。情況二、情況三的話需要改變遍歷順序或者移除上一次處理過的View。 上面是intercept=false的情況。那如果intercept=true呢?這個就比較簡單了。
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
複製程式碼
會把dispatchTransformedTouchEvent的child引數設定為null,如果為null,會把事件交給super.dispatchTouchEvent。super是誰?可別忘了ViewGroup的爸爸是View!View又有自己的dispatchTouchEvent方法,這個方法就相對來講比較簡單了,主要是touchEventListener、click、longClick等的處理。
結語
當然,這個方法裡還有一些細節的處理我沒有分析,比如上面那段程式碼的canceled變數、accessibilityFocus的處理等。這裡先埋個坑,之後有空回來補。 如果有什麼分析錯誤的地方,歡迎各位大神指正!