這是Android觸控事件系列文章的第一篇。
大領導安排任務會經歷一個“遞”的過程:大領導先把任務告訴小領導,小領導再把任務告訴小明。也可能會經歷一個“歸”的過程:小明告訴小領導做不了,小領導告訴大領導任務完不成。然後,就沒有然後了。。。。
Android觸控事件和領導安排任務的過程很相似,也會經歷“遞”和“歸”。這一篇會試著閱讀原始碼來分析ACTION_DOWN
事件的這個遞迴過程。
(ps: 下文中的 粗斜體字 表示引導原始碼閱讀的內心戲)
分發觸控事件起點
寫一個包含ViewGroup
、View
、Activity
的demo,並在所有和touch有關的方法中打log。當觸控事件發生時,Activity.dispatchTouchEvent()
總是第一個被呼叫,就以這個方法為切入點:
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
:
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()
。
觸控事件之“遞”
- 在分析View繪製時,也遇到過“dispatchXXX”函式
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()
:
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()
才會被呼叫。 - 讀到這裡,畫一張圖總結一下觸控事件之“遞”:
- 圖中ViewGroup層後面的N表示在Activity層和View層之間可能有多個ViewGroup層。
- 圖中自上而下一共有三類層次,觸控事件會從最高層次開始沿著箭頭往下層傳遞。
- 為簡單起見,圖中省略了另一種觸控事件的處理方式:
OnTouchListener.onTouch
- 圖示觸控事件的傳遞只是眾多傳遞場景中的一種:被點選的View巢狀在ViewGroup中,ViewGroup在Activity中。
觸控事件之“歸”
觸控事件之所以在“遞”之後還會發生“歸”是因為:分發觸控事件的函式還沒有執行完。沿著剛才呼叫鏈相反的方向重新看一遍原始碼:
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()
的返回值:
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()
的返回值:
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(分發事件給孩子){
如果孩子消費了事件 直接返回(將觸控事件被消費這一事實往上傳遞)
}
//“歸”
如果孩子沒有消費事件,則自己消費事件
複製程式碼
“分發事件給孩子”這個函式的呼叫表示“遞”,即將觸控事件傳遞給下層。“分發事件給孩子”這個函式的返回表示“歸”,即將觸控事件的消費結果回溯給上層,以便上層採取進一步的行動。
同樣的套路,用圖片總結下觸控事件之“歸”:
- 這張圖是對圖1描述場景的補全。圖中黑色的線表示觸控事件的傳遞路徑,灰色的線表示觸控事件回溯的路徑。
- 因為
View.onTouchEvent()
返回true,表示消費觸控事件,所以ViewGroup.onTouchEvent()
以及Activity.onTouchEvent()
都不會被呼叫。
- 這張圖是對圖1描述場景的擴充套件。圖中黑色的線表示觸控事件的傳遞路徑,灰色的線表示觸控事件回溯的路徑。
- 圖示所對應的場景是:被點選的
View
不消費觸控事件,而ViewGroup
在onTouchEvent()
中返回true
自己消費觸控事件。
- 這張圖是對圖1描述場景的擴充套件。圖中黑色的線表示觸控事件的傳遞路徑,灰色的線表示觸控事件回溯的路徑。
- 圖示所對應的場景是:被點選的
View
和ViewGroup
都不消費觸控事件,最後只能由Activity
來消費觸控事件。
總結
Activity
接收到觸控事件後,會傳遞給PhoneWindow
,再傳遞給DecorView
,由DecorView
呼叫ViewGroup.dispatchTouchEvent()
自頂向下分發ACTION_DOWN
觸控事件。ACTION_DOWN
事件通過ViewGroup.dispatchTouchEvent()
從DecorView
經過若干個ViewGroup
層層傳遞下去,最終到達View
。- 每個層次都可以通過在
onTouchEvent()
或OnTouchListener.onTouch()
返回true
,來告訴自己的父控制元件觸控事件被消費。只有當下層控制元件不消費觸控事件時,其父控制元件才有機會自己消費。 - 觸控事件的傳遞是從根檢視自頂向下“遞”的過程,觸控事件的消費是自下而上“歸”的過程。
讀到這裡可能對於觸控事件還充滿諸多疑問:
ViewGroup
層是否有辦法攔截觸控事件?ACTION_DOWN
只是觸控序列的起點,後序的ACTION_MOVE
、ACTION_UP
、ACTION_CANCEL
是如何傳遞的?
這些問題會在下一篇繼續分析。