Android觸控事件(下)——事件的分發

mcksuu發表於2018-01-03

已經記不清有多久了,貌似自從接觸Android開發開始,Android的事件分發機制一直伴隨著我們。網上各種大神的各種分析,看完了可能還是會暈暈乎乎的。沒辦法,誰讓我們是菜雞呢(對!我菜雞我還有有理了。。)。

Android觸控事件(下)——事件的分發

前面寫過一篇關於Android事件的由來,介紹了Android輸入事件從產生到傳送到View的過程。今天,就來說說關於事件分發的流程。

1. 事件產生的整個流程(Java層)

流程圖
從前一篇文章中可以知道,整個流程我是通過debug方式去看方法的呼叫關係,最終證明是正確的。這裡面有個比較有意思的地方:在DecorView和Activity的事件傳遞,DecorView通過PhoneWindow獲取Activity的Window.Callback,並且呼叫Callback的dispatchTouchEvent方法。而dispatchTouchEvent又通過getWindow方法獲得PhoneWindow物件並呼叫superDispatchTouchEvent方法,通過DecorView呼叫ViewGroup的dispatchTouchEvent方法進行事件的分發。如果最終事件沒有被消費,那麼會呼叫Activity中的onTouchEvent方法。 上面說的過程比較拗口,細細一想就會發現:**DecorView通過PhoneWindow來呼叫Activity的dispatchTouchEvent方法,而Activity又通過PhoneWindow來呼叫DecorView的superDispatchTouchEvent(這裡會呼叫ViewGroup的dispatchTouchEvent方法)。**這裡我們就會知道,Activity可能不知道有DecorView這個東西,而DecorView也不知道有Activity這個玩意,但是他倆都有一個好朋友叫PhoneWindow,可以通過PhoneWindow來呼叫需要的方法,降低耦合度

2. ViewGroup的事件分發

我們都知道ViewGroup事件分發是Parent向Child進行分發,如果Child消費了事件,則返回true告訴Parent;否則,返回false。如果Child都沒有消費,那麼此時會看Parent是否有消費。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
	if (mInputEventConsistencyVerifier != null) {
		mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
	}

	// If the event targets the accessibility focused view and this is it, start
	// normal event dispatch. Maybe a descendant is what will handle the click.
	if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
		ev.setTargetAccessibilityFocus(false);
	}

	boolean handled = false;
	// 是否過濾此次事件
	if (onFilterTouchEventForSecurity(ev)) {
		// 獲得Action
		final int action = ev.getAction();
		// Action & 0xff, 正常是的Action包括了1-12,其他的會有ACTION_POINTER_1_DOWN等Action
		// 這裡 & 0xff則需要獲得1-12的Action
		final int actionMasked = action & MotionEvent.ACTION_MASK;
		// 如果是按下事件
		// 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();
		}
		
		// Check for interception.
		// 是否被攔截
		final boolean intercepted;
		// 如果是按下事件或者第一個觸控目標不為空
		if (actionMasked == MotionEvent.ACTION_DOWN
				|| mFirstTouchTarget != null) {
			// 是否允許攔截,根據標誌位FLAG_DISALLOW_INTERCEPT來判斷
			final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
			// 為false說明允許攔截,根據onInterceptTouchEvent的返回值來判斷是否攔截觸控事件
			if (!disallowIntercept) {
				intercepted = onInterceptTouchEvent(ev);
				// 重新設定MotionEvent的Action防止其被改變
				ev.setAction(action); // restore action in case it was changed
			} else {
				// 不允許攔截的話則設定為false
				intercepted = false;
			}
		} else {
			// There are no touch targets and this action is not an initial down
			// so this view group continues to intercept touches.
			// 如果沒有觸控目標並且這個事件不是按下事件,這個ViewGroup需要繼續攔截這個事件
			intercepted = true;
		}

		// If intercepted, start normal event dispatch. Also if there is already
		// a view that is handling the gesture, do normal event dispatch.
		if (intercepted || mFirstTouchTarget != null) {
			ev.setTargetAccessibilityFocus(false);
		}

		// Check for cancelation.
		// 檢查是否取消了事件
		final boolean canceled = resetCancelNextUpFlag(this)
				|| actionMasked == MotionEvent.ACTION_CANCEL;
		
		// Update list of touch targets for pointer down, if needed.
		// FLAG_SPLIT_MOTION_EVENTS在initViewGroup()初始化中設定
		//if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) {
        //    mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
        //}
		final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
		TouchTarget newTouchTarget = null;
		// 是否已經分發給新的目標
		boolean alreadyDispatchedToNewTouchTarget = false;
		// 如果事件沒有被取消並且也沒有被攔截,則進行分發
		if (!canceled && !intercepted) {

			// If the event is targeting accessiiblity focus we give it to the
			// view that has accessibility focus and if it does not handle it
			// we clear the flag and dispatch the event to all children as usual.
			// We are looking up the accessibility focused host to avoid keeping
			// state since these events are very rare.
			// 輔助功能
			View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
					? findChildWithAccessibilityFocus() : null;
			// 如果是按下事件或者是指標按下或者是游標移動的Action,則需要進行分發
			if (actionMasked == MotionEvent.ACTION_DOWN
					|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
					|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
				// 按下事件總是0
				final int actionIndex = ev.getActionIndex(); // always 0 for down
				// split為true,idBitsToAssign = 1 << ev.getPointerId(actionIndex)
				// 每一個觸控點Pointer都會有一個當次動作序列的唯一Id和Index.MotionEvent中多個手指的操作API大部分都是通過pointerIndex來進行的,
				// 如:獲取不同Pointer的觸碰位置,getX(int pointerIndex);獲取PointerId等等。大部分情況下,pointerId = pointerIndex
				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.
				// 為這個點的id清除之前的觸控目標,防止不同步
				removePointersFromTouchTargets(idBitsToAssign);
				
				final int childrenCount = mChildrenCount;
				// 新的目標為null並且childCount!=0
				if (newTouchTarget == null && childrenCount != 0) {
					// 
					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.
					// 構建觸控分發list,構建的時候有個條件hasChildWithZ,如果沒有則返回null,一般返回null
					final ArrayList<View> preorderedList = buildTouchDispatchChildList();
					final boolean customOrder = preorderedList == null
							&& isChildrenDrawingOrderEnabled();
					final View[] children = mChildren;
					for (int i = childrenCount - 1; i >= 0; i--) {
						// 獲得childIndex
						final int childIndex = getAndVerifyPreorderedIndex(
								childrenCount, i, customOrder);
						// 獲得childView
						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;
						}
						// 如果View不能接收PointerEvents(不是VISIBLE或者child.getAnimation() != null)或者該事件的點不在View中
						if (!canViewReceivePointerEvents(child)
								|| !isTransformedTouchPointInView(x, y, child, null)) {
							ev.setTargetAccessibilityFocus(false);
							continue;
						}
						
						// 通過child獲得touchTarget,如果有的話說明Child已經接收事件了
						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.
							// 設定新的pointerIdBits
							newTouchTarget.pointerIdBits |= idBitsToAssign;
							break;
						}
						
						resetCancelNextUpFlag(child);
						// 這裡是分發給child,如果返回true,則說明接收事件
						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();
							// 生成一個touchTarget
							newTouchTarget = addTouchTarget(child, idBitsToAssign);
							// 設定已經接收事件
							alreadyDispatchedToNewTouchTarget = true;
							break;
						}

						// 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();
				}
				
				// 沒有找到可以接收事件的child,所以把這個指派給最近新增的目標
				if (newTouchTarget == null && mFirstTouchTarget != null) {
					// Did not find a child to receive the event.
					// Assign the pointer to the least recently added target.
					newTouchTarget = mFirstTouchTarget;
					while (newTouchTarget.next != null) {
						newTouchTarget = newTouchTarget.next;
					}
					newTouchTarget.pointerIdBits |= idBitsToAssign;
				}
			}
		}

		// Dispatch to touch targets.
		// mFirstTouchTarget不等於null是在addTouchTarget中賦值,這個需要事件分發的時候返回true
		// 這時候可以知道此次事件是被View接收,而上面一部分只是關於DOWN事件的處理,所以一旦有View接收了DOWN事件,
		// 那麼接下來的事件都將交給此View處理
		if (mFirstTouchTarget == null) {
			// No touch targets so treat this as an ordinary view.
			// 沒有觸控目標,所以把它認為是普通View,呼叫super.dispatchTouchEvent
			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.
			TouchTarget predecessor = null;
			// 臨時變數tagert
			TouchTarget target = mFirstTouchTarget;
			// 連結串列遍歷,target = target.next
			while (target != null) {
				final TouchTarget next = target.next;
				// 這個條件為真的話,是在DOWN事件是有View接收,此時alreadyDispatchedToNewTouchTarget = true
				// 並且mFirstTouchTarget = target = newTouchTarget
				if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
					handled = true;
				} else {
					// 是否取消,判斷flag PFLAG_CANCEL_NEXT_UP_EVENT
					final boolean cancelChild = resetCancelNextUpFlag(target.child)
							|| intercepted;
					// 到這邊的話此次事件已經不再是DOWN事件,而是MOVE事件,此時分發給target.child,知道最終接收DOWN事件的View
					// 如果返回true,則設定handled = true
					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;
			}
		}
		
		// Update list of touch targets for pointer up or cancel, if needed.
		// 抬起事件重置狀態
		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. 通過安全策略檢查是否需要過濾此次事件(檢查Window是否被覆蓋),如果過濾了,則不會處理此次事件;否則進行第2步。 2. 獲得事件的Action,並且如果是按下事件的話,則需要清除觸控事件的目標並且重置觸控狀態。 3. 檢查是否需要攔截此次事件,這時候先判斷標誌位FLAG_DISALLOW_INTERCEPT是否存在,如果存在則說明不允許進行攔截,否則的話根據onInterceptTouchEvent的返回值來判斷是否攔截觸控事件。這裡我們可以清楚一點:如果ViewGroup的onInterceptTouchEvent返回true,則不會對事件的進行向下分發。 4. 檢查事件是否被取消。 5. 如果事件沒有被取消並且沒有被攔截,則需要獲取事件的id。每一個觸控點Pointer都會有一個當次動作序列的唯一Id。獲取不同Pointer的觸碰位置可以通過getX(int pointerIndex)方法。大部分情況下,pointerId = pointerIndex 6. 構建分發列表,構建的時候有個條件hasChildWithZ,如果沒有則返回null,一般返回null。 7. 對子View進行遍歷,如果可以接收事件,那麼先去獲得觸控目標(getTouchTarget)。如果返回值不為null,則說明子View已經接收事件了,跳出迴圈;否則,呼叫dispatchTransformedTouchEvent方法將事件傳遞給子View。 8. 根據dispatchTransformedTouchEvent返回值來判斷是否子View接收了事件。true:接收事件;false:沒有接收。 9. 如果沒有接收事件,則mFirstTouchTarget = null,所以會呼叫dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)這裡會呼叫super.dispatchTouchEvent交給View去處理這次touch事件;如果接收了這次事件,則mFirstTouchTarget !=null 並且alreadyDispatchedToNewTouchTarget = true&&target == newTouchTarget成立,最終handled設定為true。 10. 如果事件不是DOWN事件,那麼去尋找是否有TouchTarget,如果有的話,說明有View消費了事件,那麼接下來的該事件的其他操作將交由此View處理 說完dispatchTouchEvent方法,這裡需要說下dispatchTransformedTouchEvent方法:

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.
	// 事件取消,如果child不為空則向child分發ACTION_CANCEL事件;否則呼叫super.dispatchTouchEvent
	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;
	}

	// Calculate the number of pointers to deliver.
	// 通過getPointerIdBits獲取事件的觸碰點的id和傳入的進行&運算,如果此時仍和oldPointerIdBits相同,
	// 則說明desiredPointerIdBits包括了oldPointerIdBits,可以理解為是同一個事件
	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;
	}

	// 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.
	// 如果新的事件和舊的事件的getPointerIdBits & desiredPointerIdBits相同,則設定offsetLocation並且直接去進行事件的分發
	// 一般來說單點觸控事件會進行這裡
	final MotionEvent transformedEvent;
	if (newPointerIdBits == oldPointerIdBits) {
		if (child == null || child.hasIdentityMatrix()) {
			if (child == null) {
				// 如果沒有child的話,則呼叫View的dispatchTouchEvent方法
				handled = super.dispatchTouchEvent(event);
			} else {
				final float offsetX = mScrollX - child.mLeft;
				final float offsetY = mScrollY - child.mTop;
				event.offsetLocation(offsetX, offsetY);
				// 對child的dispatchTouchEvent方法進行事件分發:
				// 1. 如果child是ViewGroup,則會進行ViewGroup的事件分發過程
				// 2. 如果child是View,則進行View的事件分發過程
				handled = child.dispatchTouchEvent(event);

				event.offsetLocation(-offsetX, -offsetY);
			}
			return handled;
		}
		transformedEvent = MotionEvent.obtain(event);
	} else {
		transformedEvent = event.split(newPointerIdBits);
	}

	// 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();
	return handled;
}
複製程式碼

這個方法其實總得來說就執行了兩個過程: 1. 傳入的子View不為null,將事件分發給子View。 2. 傳入的子View為null,呼叫super.dispatchTouchEvent方法去處理。

3. View的dispatchTouchEvent方法

上面說到,如果子View為null,則會呼叫super.dispatchTouchEvent方法。這裡看下View的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
	......
	final int actionMasked = event.getActionMasked();
	if (actionMasked == MotionEvent.ACTION_DOWN) {
		// Defensive cleanup for new gesture
		stopNestedScroll();
	}
	// 通過安全策略檢查是否需要過濾
	if (onFilterTouchEventForSecurity(event)) {
		// 如果View是enable並且正在處理ScrollBar的滑鼠拖拽事件,設定結果為true
		if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
			result = true;
		}
		//noinspection SimplifiableIfStatement
		// 判斷ListenerInfo中的OnTouchListener是否為null
		// 不為null,並且ENABLED,則呼叫mOnTouchListener.onTouch方法
		// 如果mOnTouchListener.onTouch返回true,則結果設為true
		ListenerInfo li = mListenerInfo;
		if (li != null && li.mOnTouchListener != null
				&& (mViewFlags & ENABLED_MASK) == ENABLED
				&& li.mOnTouchListener.onTouch(this, event)) {
			result = true;
		}
		
		// 如果結果為false並且onTouchEvent方法返回值為true,結果設為true
		if (!result && onTouchEvent(event)) {
			result = true;
		}
	}
	......
	return result;
}
複製程式碼

這個方法就簡單了很多,因為它作為最終的View(View不包括子View),所能進行的操作要麼消費事件要麼不消費事件。從方法的實現中也可以看到:

  1. 如果設定了OnTouchListener,那麼在執行過程中會先執行OnTouchListener的onTouch方法,接著根據返回值來確定是否需要執行onTouchEvent方法
  2. onTouchEvent是否需要呼叫是和result的值有關如果result為true,則不呼叫;反之,則呼叫

4. 驗證

驗證過程其是很簡單,只要能夠進行debug看方法的呼叫即可。我這裡測試的程式碼如下:

佈局程式碼
測試程式碼
方法的呼叫圖:
debug圖
可以看到圖中是通過ViewGroup一步一步呼叫到最終的TextView的dispatchTouchEvent方法的,關於為啥會執行這麼多次,需要看下頁面結構圖:
頁面結構
從圖中可以看到我們最終的TextView被n多個layout包裹,所以會出現ViewGroup中的dispatchTouchEvent方法呼叫多次。

5. 總結

從原始碼可以看出:

  • 事件的分發最重要的是對ACTION_DOWN事件的分發,在分發過程中:如果該ViewGroup沒有攔截,那麼會對其子View進行事件分發。如果子View沒有消費事件(返回值為true),那麼交由上一級處理。
  • 同一事件的其他操作,例如ACTION_MOVE這個是在處理完ACTION_DOWN事件後進行的,主要是通過查詢TouchTarget是否存在來判斷是否事件需要傳遞。
  • View中OnTouchListener的onTouch方法優先順序高於onTouchEvent方法,並且onTouchEvent在result為true的時候不會呼叫。

相關文章