這是Android觸摸事件系列的第二篇,系列文章目錄以下:bash
把上一篇中領導分配任務的故事,延展一下:ide
大領導安排任務會經歷一個「遞」的過程:大領導先把任務告訴小領導,小領導再把任務告訴小明。也可能會經歷一個「歸」的過程:小明告訴小領導作不了,小領導告訴大領導任務完不成。而後,就沒有而後了。。。。但若是此次完成了任務,大領導還會繼續將後序任務分配給小明。post
故事的延展部分和今天要講的ACTION_DONW
後序事件很相似,先來回答上一篇中遺留的另外一個問題「攔截事件」:ui
ViewGroup
在遍歷孩子分發觸摸事件前還有一段攔截邏輯:this
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()
)。spa
用一張圖總結一下: 3d
onInterceptTouchEvent()
返回true
,致使onTouchEvent()
被調用,由於onTouchEvent()
返回true
,致使dispatchTouchEvent()
返回true
。ViewGroup
(包括本身),由於觸摸事件再也不會向下層的View
傳遞。上一篇在閱讀源碼的時候,埋下了一個伏筆,如今將其補全:rest
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()
會將願意消費觸摸事件的孩子存儲在觸摸鏈中,當後序事件會分發給觸摸鏈上的對象。code
用兩張圖總結一下: orm
ACTION_DOWN
事件的傳遞路徑,灰色箭頭表示ACTION_MOVE
和ACTION_UP
事件的傳遞路徑。即只要有視圖聲稱消費ACTION_DOWN
,則其後序事件也傳遞給它,無論它是否聲稱消費ACTION_MOVE
和ACTION_UP
,若是它不消費,則後序事件會像上一篇分析的ACTION_DOWN
同樣向上回溯給上層消費。ACTION_DOWN
事件的傳遞路徑,灰色箭頭表示ACTION_MOVE
和ACTION_UP
事件的傳遞路徑。即全部視圖都不消費ACTION_DOWN
,則其後序事件只會傳遞給Activity.onTouchEvent()
。把領導佈置任務的故事繼續延展一下:大領導給小領導佈置了任務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
給孩子。