這是Android觸摸事件系列文章的第一篇。bash
大領導安排任務會經歷一個「遞」的過程:大領導先把任務告訴小領導,小領導再把任務告訴小明。也可能會經歷一個「歸」的過程:小明告訴小領導作不了,小領導告訴大領導任務完不成。而後,就沒有而後了。。。。ide
Android觸摸事件和領導安排任務的過程很類似,也會經歷「遞」和「歸」。這一篇會試着閱讀源碼來分析ACTION_DOWN
事件的這個遞歸過程。函數
(ps: 下文中的 粗斜體字 表示引導源碼閱讀的心裏戲)佈局
寫一個包含ViewGroup
、View
、Activity
的demo,並在全部和touch有關的方法中打log。當觸摸事件發生時,Activity.dispatchTouchEvent()
老是第一個被調用,就以這個方法爲切入點:post
public class Activity{
private Window mWindow;
//分發觸摸事件
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//讓PhoneWindow幫忙分發觸摸事件
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//得到PhoneWindow對象
public Window getWindow() {
return mWindow;
}
//參數太長,省略了
final void attach(...) {
...
//構造PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
}
複製代碼
Activity
將事件傳遞給PhoneWindow
:ui
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
//一個窗口的頂層視圖
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//將觸摸事件交給DecorView分發
return mDecor.superDispatchTouchEvent(event);
}
}
//DecorView繼承自ViewGroup
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{
public boolean superDispatchTouchEvent(MotionEvent event) {
//事件最終由ViewGroup.dispatchTouchEvent()分發觸摸事件
return super.dispatchTouchEvent(event);
}
}
複製代碼
PhoneWindow
繼續將事件傳遞給DecorView
,最終調用了ViewGroup.dispatchTouchEvent()
Activity
開始,通過PhoneWindow
,到達頂層視圖DecorView
。DecorView
調用了ViewGroup.dispatchTouchEvent()
。ViewGroup.dispatchDraw()
,它用於遍歷孩子並觸發它們本身繪製本身。那 dispatchTouchEvent()
會不會也遍歷孩子並將觸摸事件傳遞給它們? 帶着這個疑問來看下源碼:public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!canceled && !intercepted) {
...
//遍歷孩子
for (int i = childrenCount - 1; i >= 0; i--) {
//按照索引順序或者自定義繪製順序遍歷孩子
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : I;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
...
//若是孩子不在觸摸區域則直接跳過
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
//轉換觸摸座標並分發給孩子(child參數不爲null)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//這裏的代碼也很關鍵,先埋伏筆1
}
...
}
}
if (mFirstTouchTarget == null) {
//這裏的代碼也很關鍵,先埋伏筆2
} else {
//這裏的代碼也很關鍵,先埋伏筆3
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// Perform any necessary transformations and dispatch.
//進行必要的座標轉換而後分發觸摸事件
if (child == null) {
//這裏的代碼也很關鍵,先埋伏筆3
} else {
//將ViewGroup座標系轉換爲它孩子的座標系(座標原點從ViewGroup左上角移動到孩子左上角)
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;
}
}
複製代碼
果真沒猜錯!父控件在ViewGroup.dispatchTouchEvent()
中會遍歷孩子並將觸摸事件分發給被點中的子控件,若是子控件還有孩子,觸摸事件的「遞」將不斷持續,直到葉子結點。 最終View
類型的葉子結點調用的是View.dispatchTouchEvent()
:this
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//1.通知觸摸監聽器OnTouchListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//2.調用onTouchEvent()
//只有當OnTouchListener.onTouch()返回false時,onTouchEvent()纔有機會被調用
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
//返回值就是onTouch()或者onTouchEvent()的返回值
return result;
}
ListenerInfo mListenerInfo;
//監聽器容器類
static class ListenerInfo {
...
private OnTouchListener mOnTouchListener;
...
}
//設置觸摸監聽器
public void setOnTouchListener(OnTouchListener l) {
//將監聽器存儲在監聽器容器中
getListenerInfo().mOnTouchListener = l;
}
//得到監聽器管理實例
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
}
複製代碼
View.dispatchTouchEvent()
是傳遞觸摸事件的終點,消費觸摸事件的起點。OnTouchListener.onTouch()
或View.onTouchEvent()
,前者優先級高於後者。只有當沒有設置OnTouchListener
或者onTouch()
返回false
時,View.onTouchEvent()
纔會被調用。OnTouchListener.onTouch
觸摸事件之因此在「遞」以後還會發生「歸」是由於:分發觸摸事件的函數尚未執行完。沿着剛纔調用鏈相反的方向從新看一遍源碼:spa
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
* 返回true表示觸摸事件被消費,不然表示未被消費
*/
public boolean onTouchEvent(MotionEvent event) {
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
//省略了對不一樣觸摸事件的默認處理
...
//只要控件是可點擊的,就表示觸摸事件已被消費
return true;
}
//若控件不可點擊則不消費觸摸事件
return false;
}
}
複製代碼
View.dispatchTouchEvent()
調用了View.onTouchEvent()
後並無執行完。View.onTouchEvent()
的返回值會影響View.dispatchTouchEvent()
的返回值:3d
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
//返回當前View是否消費觸摸事件的布爾值
return result;
}
複製代碼
一樣的,ViewGroup.dispatchTouchEvent()
調用了View.dispatchTouchEvent()
後也沒有執行完,View.dispatchTouchEvent()
的返回值會影響ViewGroup.dispatchTouchEvent()
的返回值:code
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
//觸摸鏈頭結點
private TouchTarget mFirstTouchTarget;
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!canceled && !intercepted) {
...
//遍歷孩子
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);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//遍歷觸摸鏈分發觸摸事件給全部想接收的孩子
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//若是已經將觸摸事件分發給新的觸摸目標,則返回true
handled = true;
} else {
//這裏的代碼很重要,繼續埋伏筆,待下一篇分析。
}
predecessor = target;
target = next;
}
}
...
//返回觸摸事件是否被孩子或者本身消費的布爾值
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 {
//將觸摸事件分發給孩子
}
...
return handled;
}
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
* 添加View到觸摸鏈頭部
* @param child View
* @param pointerIdBits
* @return 新觸摸目標
*/
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
}
複製代碼
ViewGroup
會將其接入「觸摸鏈」,若是觸摸鏈中沒有結點則表示沒有孩子願意消費事件,此時ViewGroup
只能本身消費事件。ViewGroup
是View
的子類,他們消費觸摸事件的方式一摸同樣,都是經過View.dispatchTouchEvent()
調用View.onTouchEvent()
或OnTouchListener.onTouch()
。public class Activity {
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
//若是佈局中有控件願意消費觸摸事件,則返回true,onTouchEvent()不會被調用
return true;
}
return onTouchEvent(ev);
}
}
複製代碼
View
、ViewGroup
和Activity
,雖然它們分發觸摸事件的邏輯不太同樣,但基本結構都和上面這段代碼神似,用僞代碼能夠寫成:
//「遞」
if(分發事件給孩子){
若是孩子消費了事件 直接返回(將觸摸事件被消費這一事實往上傳遞)
}
//「歸」
若是孩子沒有消費事件,則本身消費事件
複製代碼
「分發事件給孩子」這個函數的調用表示「遞」,即將觸摸事件傳遞給下層。「分發事件給孩子」這個函數的返回表示「歸」,即將觸摸事件的消費結果回溯給上層,以便上層採起進一步的行動。
一樣的套路,用圖片總結下觸摸事件之「歸」:
View.onTouchEvent()
返回true,表示消費觸摸事件,因此ViewGroup.onTouchEvent()
以及Activity.onTouchEvent()
都不會被調用。View
不消費觸摸事件,而ViewGroup
在onTouchEvent()
中返回true
本身消費觸摸事件。View
和ViewGroup
都不消費觸摸事件,最後只能由Activity
來消費觸摸事件。Activity
接收到觸摸事件後,會傳遞給PhoneWindow
,再傳遞給DecorView
,由DecorView
調用ViewGroup.dispatchTouchEvent()
自頂向下分發ACTION_DOWN
觸摸事件。ACTION_DOWN
事件經過ViewGroup.dispatchTouchEvent()
從DecorView
通過若干個ViewGroup
層層傳遞下去,最終到達View
。View.dispatchTouchEvent()
被調用。View.dispatchTouchEvent()
是傳遞事件的終點,消費事件的起點。它會調用onTouchEvent()
或OnTouchListener.onTouch()
來消費事件。onTouchEvent()
或OnTouchListener.onTouch()
返回true
,來告訴本身的父控件觸摸事件被消費。只有當下層控件不消費觸摸事件時,其父控件纔有機會本身消費。讀到這裏可能對於觸摸事件還充滿諸多疑問:
ViewGroup
層是否有辦法攔截觸摸事件?ACTION_DOWN
只是觸摸序列的起點,後序的ACTION_MOVE
、ACTION_UP
、ACTION_CANCEL
是如何傳遞的?這些問題會在下一篇繼續分析。