這是Android觸控事件系列的第二篇,系列文章目錄如下:
把上一篇中領導分配任務的故事,延展一下:
大領導安排任務會經歷一個“遞”的過程:大領導先把任務告訴小領導,小領導再把任務告訴小明。也可能會經歷一個“歸”的過程:小明告訴小領導做不了,小領導告訴大領導任務完不成。然後,就沒有然後了。。。。但如果這次完成了任務,大領導還會繼續將後序任務分配給小明。
故事的延展部分和今天要講的ACTION_DONW
後序事件很類似,先來回答上一篇中遺留的另一個問題“攔截事件”:
攔截事件
ViewGroup
在遍歷孩子分發觸控事件前還有一段攔截邏輯:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
//檢查ViewGroup是否要攔截觸控事件的下發
final boolean intercepted;
//第一個條件表示攔截ACTION_DOWN事件
//第二個條件表示攔截ACTION_DOWN事件已經分發給孩子,現在攔截後序事件
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;
}
...
//當事件沒有被攔截的時候,將其分發給孩子
if (!canceled && !intercepted) {
//遍歷孩子並將事件分發給它們
//如果有孩子聲稱要消費事件,則將其新增到觸控鏈上
//這段邏輯在上一篇中分析過,這裡就省略了
}
}
//將觸控事件分發給觸控鏈
if (mFirstTouchTarget == null) { //沒有觸控鏈
//如果事件被ViewGroup攔截,則觸控鏈為空,ViewGroup自己消費事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
}
//返回true表示攔截事件,預設返回false
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
...
if (child == null) {
//ViewGroup孩子都不願意接收觸控事件或者觸控事件被攔截 則其將自己當成View處理(呼叫View.dispatchTouchEvent())
handled = super.dispatchTouchEvent(transformedEvent);
}
...
}
}
複製程式碼
當允許攔截時,onInterceptTouchEvent()
會被呼叫,如果過載這個方法並且返回true
,表示ViewGroup
要對事件進行攔截,此時不再將事件分發給孩子而是自己消費(通過呼叫View.dispatchTouchEvent()
最終走到ViewGroup.onTouchEvent()
)。
用一張圖總結一下:
- 圖中黑色的箭頭表示觸控事件傳遞的路徑,灰色的箭頭表示觸控事件消費的回溯路徑。
onInterceptTouchEvent()
返回true
,導致onTouchEvent()
被呼叫,因為onTouchEvent()
返回true
,導致dispatchTouchEvent()
返回true
。 - 準確的說,攔截觸控事件的受益者是所有上層的
ViewGroup
(包括自己),因為觸控事件不再會向下層的View
傳遞。
ACTION_MOVE 、 ACTION_UP
上一篇在閱讀原始碼的時候,埋下了一個伏筆,現在將其補全:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
//觸控鏈頭結點
private TouchTarget mFirstTouchTarget;
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!canceled && !intercepted) {
...
//當ACTION_DOWN的時候才遍歷尋找消費觸控事件的孩子,若找到則將其加入到觸控鏈
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//遍歷孩子
for (int i = childrenCount - 1; i >= 0; i--) {
...
//轉換觸控座標並分發給孩子(child引數不為null)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//有孩子願意消費觸控事件,將其插入“觸控鏈”
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示已經將觸控事件分發給新的觸控目標
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
}
}
if (mFirstTouchTarget == null) {
//如果沒有孩子願意消費觸控事件,則自己消費(child引數為null)
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
//觸控鏈不為null,表示有孩子消費了ACTION_DOWN
else {
//將伏筆補全
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//遍歷觸控鏈將ACTION_DOWN的後序事件分發給孩子
while (target != null) {
final TouchTarget next = target.next;
//上一篇分析了,ACTION_DOWN會走這裡
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//如果已經將觸控事件分發給新的觸控目標,則返回true
handled = true;
}
//ACTION_DONW的後序事件走這裡
else {
...
//將觸控事件分發給觸控鏈上的觸控目標
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
}
...
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//如果是ACTION_UP事件,則將觸控鏈清空
resetTouchState();
}
return handled;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// Perform any necessary transformations and dispatch.
//進行必要的座標轉換然後分發觸控事件
if (child == null) {
//ViewGroup孩子都不願意消費觸控事件 則其將自己當成View處理(呼叫View.dispatchTouchEvent())
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);
}
...
return handled;
}
/**
* Resets all touch state in preparation for a new cycle.
* 重置Touch標誌
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
/**
* Clears all touch targets.
* 清空觸控鏈
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
}
複製程式碼
觸控事件是一個序列,序列總是以ACTION_DOWN
開始,緊接著有ACTION_MOVE
和ACTION_UP
。當ACTION_DOWN
發生時,ViewGroup.dispatchTouchEvent()
會將願意消費觸控事件的孩子儲存在觸控鏈中,當後序事件會分發給觸控鏈上的物件。
用兩張圖總結一下:
- 圖中黑色箭頭表示
ACTION_DOWN
事件的傳遞路徑,灰色箭頭表示ACTION_MOVE
和ACTION_UP
事件的傳遞路徑。即只要有檢視聲稱消費ACTION_DOWN
,則其後序事件也傳遞給它,不管它是否聲稱消費ACTION_MOVE
和ACTION_UP
,如果它不消費,則後序事件會像上一篇分析的ACTION_DOWN
一樣向上回溯給上層消費。
- 圖中黑色箭頭表示
ACTION_DOWN
事件的傳遞路徑,灰色箭頭表示ACTION_MOVE
和ACTION_UP
事件的傳遞路徑。即所有檢視都不消費ACTION_DOWN
,則其後序事件只會傳遞給Activity.onTouchEvent()
。
ACTION_CANCEL
把領導佈置任務的故事繼續延展一下:大領導給小領導佈置了任務1,小領導把他傳遞給小明,小明完成了。緊接著大領導給小領導佈置了任務2,小領導決定自己處理任務2,於是他和小明說後序任務我來接手,你可以忙別的事情。
故事對應的觸控事件傳遞場景是:Activity
將ACTION_DOWN
傳遞給ViewGroup
,ViewGroup
將其傳遞給View
,View
聲稱消費ACTION_DOWN
。Activity
繼續將ACTION_MOVE
傳遞給ViewGroup
,但ViewGroup
對其做了攔截,此時ViewGroup
會傳送ACTION_CANCEL
事件給View
。
看下原始碼:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
public boolean dispatchTouchEvent(MotionEvent ev) {
//檢查ViewGroup是否要攔截觸控事件的下發
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;
}
}
...
//如果孩子消費ACTION_DOWN事件,則會在這裡將其新增到觸控鏈中
if (!canceled && !intercepted) {
...
}
//將觸控事件分發給觸控鏈
if (mFirstTouchTarget == null) { //沒有觸控鏈 表示當前ViewGroup中沒有孩子願意接收觸控事件
//將觸控事件分發給自己
} 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 {
//如果事件被攔截則cancelChild為true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//將ACTION_CANCEL事件傳遞給孩子
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//如果傳送了ACTION_CANCEL事件,將孩子從觸控鏈上摘除
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
}
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 {
//將ACTION_CANCEL事件傳遞給孩子
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
複製程式碼
當孩子消費了ACTION_DOWN
事件,它的引用被會儲存在父親的觸控鏈中。當父親攔截後序事件時,父親會向觸控鏈上的孩子傳送ACTION_CANCEL
事件,並將孩子從觸控鏈上摘除。後序事件就傳遞到父親為止。
總結
經過兩篇文章的分析,對Android觸控事件的分發有了初步的瞭解,得出了以下結論:
Activity
接收到觸控事件後,會傳遞給PhoneWindow
,再傳遞給DecorView
,由DecorView
呼叫ViewGroup.dispatchTouchEvent()
自頂向下分發ACTION_DOWN
觸控事件。ACTION_DOWN
事件通過ViewGroup.dispatchTouchEvent()
從DecorView
經過若干個ViewGroup
層層傳遞下去,最終到達View
。- 每個層次都可以通過在
onTouchEvent()
或OnTouchListener.onTouch()
返回true
,來告訴自己的父控制元件觸控事件被消費。在父控制元件不攔截事件的情況下,只有當下層控制元件不消費觸控事件時,其父控制元件才有機會自己消費。 - 觸控事件的傳遞是從根檢視自頂向下“遞”的過程,觸控事件的消費是自下而上“歸”的過程。
ACTION_MOVE
和ACTION_UP
會沿著剛才ACTION_DOWN
的傳遞路徑,傳遞給消費了ACTION_DOWN
的控制元件,如果該控制元件沒有宣告消費這些後序事件,則它們也像ACTION_DOWN
一樣會向上回溯讓其父控制元件消費。- 父控制元件可以通過在
onInterceptTouchEvent()
返回true
來攔截事件向其孩子傳遞。如果在孩子已經消費了ACTION_DOWN
事情後才進行攔截,父控制元件會傳送ACTION_CANCEL
給孩子。