編輯:Android資訊
觸摸事件傳遞機制是Android中一塊比較重要的知識體系,了解並熟悉整套的傳遞機制有助於更好的分析各種滑動沖突、滑動失效問題,更好去擴展控件的事件功能和開發自定義控件。
在Android設備中,觸摸事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和雙擊,另外還包括單指操作和多指操作等。一個最簡單的用戶觸摸事件一般經過以下幾個流程:
Android把這些事件的每一步抽象為MotionEvent這一概念,MotionEvent包含了觸摸的坐標位置,點按的數量(手指的數量),時間點等信息,用於描述用戶當前的具體動作,常見的MotionEvent有下面幾種類型:
ACTION_DOWNACTION_UPACTION_MOVEACTION_CANCEL其中,ACTION_DOWN、ACTION_MOVE、ACTION_UP就分別對應於上面的手指按下、手指滑動、手指抬起操作,即一個最簡單的用戶操作包含了一個ACTION_DOWN事件,若干個ACTION_MOVE事件和一個ACTION_UP事件。
事件分發過程中,涉及的主要方法有以下幾個:
dispatchTouchEvent: 用於事件的分發,所有的事件都要通過此方法進行分發,決定是自己對事件進行消費還是交由子View處理onTouchEvent: 主要用於事件的處理,返回true表示消費當前事件onInterceptTouchEvent: 是ViewGroup中獨有的方法,若返回true表示攔截當前事件,交由自己的onTouchEvent()進行處理,返回false表示不攔截我們的源碼分析也主要圍繞這幾個方法展開。
我們從Activity的dispatchTouchEvent方法作為入口進行分析:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
這個方法首先會判斷當前觸摸事件的類型,如果是ACTION_DOWN事件,會觸發onUserInteraction方法。根據文檔注釋,當有任意一個按鍵、觸屏或者軌跡球事件發生時,棧頂Activity的onUserInteraction會被觸發。如果我們需要知道用戶是不是正在和設備交互,可以在子類中重寫這個方法,去獲取通知(比如取消屏保這個場景)。
然後是調用Activity內部mWindow的superDispatchTouchEvent方法,mWindow其實是PhoneWindow的實例,我們看看這個方法做了什麼:
public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
...
}
}
原來PhoneWindow內部調用了DecorView的同名方法,而DecorView其實是FrameLayout的子類,FrameLayout並沒有重寫dispatchTouchEvent方法,所以事件開始交由ViewGroup的dispatchTouchEvent開始分發了,這個方法將在下一節分析。
我們回到Activity的dispatchTouchEvent方法,注意當getWindow().superDispatchTouchEvent(ev)這一語句返回false時,即事件沒有被任何子View消費時,最終會執行Activity的onTouchEvent:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
小結:
事件從Activity的dispatchTouchEvent開始,經由DecorView開始向下傳遞,交由子View處理,若事件未被任何Activity的子View處理,將由Activity自己處理。
由上節分析可知,事件來到DecorView後,經過層層調用,來到了ViewGroup的dispatchTouchEvent方法中:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
...
// 先檢驗事件是否需要被ViewGroup攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 校驗是否給mGroupFlags設置了FLAG_DISALLOW_INTERCEPT標志位
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 走onInterceptTouchEvent判斷是否攔截事件
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
if (!canceled && !intercepted) {
// 注意ACTION_DOWN等事件才會走遍歷所有子View的流程
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
// 開始遍歷所有子View開始逐個分發事件
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
// 判斷觸摸點是否在這個View的內部
final View child = children[i];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
...
// 事件被子View消費,退出循環,不再繼續分發給其他子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
// addTouchTarget內部將mFirstTouchTarget設置為child,即不為null
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
// 事件未被任何子View消費,自己處理
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 將MotionEvent.ACTION_DOWN後續事件分發給mFirstTouchTarget指向的View
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 如果已經在上面的遍歷過程中傳遞過事件,跳過本次傳遞
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
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);
}
}
return handled;
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// 注意傳參child為null時,調用的是自己的dispatchTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 默認不攔截事件
return false;
}
這個方法比較長,只要把握住主要脈絡,修枝剪葉後還是非常清晰的:
(1) 判斷事件是夠需要被ViewGroup攔截
首先會根據mGroupFlags判斷是否可以執行onInterceptTouchEvent方法,它的值可以通過requestDisallowInterceptTouchEvent方法設置:
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
// 層層向上傳遞,告知所有父View不攔截事件
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
所以我們在處理某些滑動沖突場景時,可以從子View中調用父View的requestDisallowInterceptTouchEvent方法,阻止父View攔截事件。
如果view沒有設置FLAG_DISALLOW_INTERCEPT,就可以進入onInterceptTouchEvent方法,判斷是否應該被自己攔截,
ViewGroup的onInterceptTouchEvent直接返回了false,即默認是不攔截事件的,ViewGroup的子類可以重寫這個方法,內部判斷攔截邏輯。
注意:只有當事件類型是ACTION_DOWN或者mFirstTouchTarget不為空時,才會走是否需要攔截事件這一判斷,如果事件是ACTION_DOWN的後續事件(如ACTION_MOVE、ACTION_UP等),且在傳遞ACTION_DOWN事件過程中沒有找到目標子View時,事件將會直接被攔截,交給ViewGroup自己處理。mFirstTouchTarget的賦值會在下一節提到。
(2) 遍歷所有子View,逐個分發事件:
執行遍歷分發的條件是:當前事件是ACTION_DOWN、ACTION_POINTER_DOWN或者ACTION_HOVER_MOVE三種類型中的一個(後兩種用的比較少,暫且忽略)。所以,如果事件是ACTION_DOWN的後續事件,如ACTION_UP事件,將不會進入遍歷流程!
進入遍歷流程後,拿到一個子View,首先會判斷觸摸點是不是在子View范圍內,如果不是直接跳過該子View;
否則通過dispatchTransformedTouchEvent方法,間接調用child.dispatchTouchEvent達到傳遞的目的;
如果dispatchTransformedTouchEvent返回true,即事件被子View消費,就會把mFirstTouchTarget設置為child,即不為null,並將alreadyDispatchedToNewTouchTarget設置為true,然後跳出循環,事件不再繼續傳遞給其他子View。
可以理解為,這一步的主要作用是,在事件的開始,即傳遞ACTION_DOWN事件過程中,找到一個需要消費事件的子View,我們可以稱之為目標子View,執行第一次事件傳遞,並把mFirstTouchTarget設置為這個目標子View
(3) 將事件交給ViewGroup自己或者目標子View處理
經過上面一步後,如果mFirstTouchTarget仍然為空,說明沒有任何一個子View消費事件,將同樣會調用dispatchTransformedTouchEvent,但此時這個方法的View child參數為null,所以調用的其實是super.dispatchTouchEvent(event),即事件交給ViewGroup自己處理。ViewGroup是View的子View,所以事件將會使用View的dispatchTouchEvent(event)方法判斷是否消費事件。
反之,如果mFirstTouchTarget不為null,說明上一次事件傳遞時,找到了需要處理事件的目標子View,此時,ACTION_DOWN的後續事件,如ACTION_UP等事件,都會傳遞至mFirstTouchTarget中保存的目標子View中。這裡面還有一個小細節,如果在上一節遍歷過程中已經把本次事件傳遞給子View,alreadyDispatchedToNewTouchTarget的值會被設置為true,代碼會判斷alreadyDispatchedToNewTouchTarget的值,避免做重復分發。
小結:
dispatchTouchEvent方法首先判斷事件是否需要被攔截,如果需要攔截會調用onInterceptTouchEvent,若該方法返回true,事件由ViewGroup自己處理,不在繼續傳遞。
若事件未被攔截,將先遍歷找出一個目標子View,後續事件也將交由目標子View處理。
若沒有目標子View,事件由ViewGroup自己處理。此外,如果一個子View沒有消費
ACTION_DOWN類型的事件,那麼事件將會被另一個子View或者ViewGroup自己消費,之後的事件都只會傳遞給目標子View(mFirstTouchTarget)或者ViewGroup自身。簡單來說,就是如果一個View沒有消費ACTION_DOWN事件,後續事件也不會傳遞進來。
現在回頭看上一節的第2、3步,不管是對子View分發事件,還是將事件分發給ViewGroup自身,最後都殊途同歸,調用到了View的dispatchTouchEvent,這就是我們這一節分析的目標。
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
// 判斷事件是否先交給ouTouch方法處理
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
// onTouch未消費事件,傳給onTouchEvent
if (onTouchEvent(event)) {
return true;
}
}
...
return false;
}
代碼量不多,主要做了三件事:
onTouchEvent方法繼續處理這樣,我們的分析轉到了View的onTouchEvent方法:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
// 如果一個View處於DISABLED狀態,但是CLICKABLE或者LONG_CLICKABLE的話,這個View仍然能消費事件
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
mPrivateFlags |= PRESSED;
refreshDrawableState();
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
public final boolean isFocusable() {
return FOCUSABLE == (mViewFlags & FOCUSABLE_MASK);
}
public final boolean isFocusableInTouchMode() {
return FOCUSABLE_IN_TOUCH_MODE == (mViewFlags & FOCUSABLE_IN_TOUCH_MODE);
}
onTouchEvent方法的主要流程如下:
ACTION_UP分支,這個分支內部經過重重判斷之後,會調用到performClick方法:public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
可以看到,如果設置了OnClickListener,就會回調我們的onClick方法,最終消費事件。
通過上面的源碼解析,我們可以總結出事件分發的整體流程:

下面做一個總體概括:
事件由Activity的dispatchTouchEvent()開始,將事件傳遞給當前Activity的根ViewGroup:mDecorView,事件開始自上而下進行傳遞,直至被消費。
事件傳遞至ViewGroup時,調用dispatchTouchEvent()進行分發處理:
onInterceptTouchEvent(),若為true,跳過2步驟;事件傳遞至View的dispatchTouchEvent()時, 首先會判斷OnTouchListener是否存在,倘若存在,則執行onTouch(),若onTouch()未對事件進行消費,事件將繼續交由onTouchEvent處理,根據上面分析可知,View的onClick事件是在onTouchEvent的ACTION_UP中觸發的,因此,onTouch事件優先於onClick事件。
若事件在自上而下的傳遞過程中一直沒有被消費,而且最底層的子View也沒有對其進行消費,事件會反向向上傳遞,此時,父ViewGroup可以對事件進行消費,若仍然沒有被消費的話,最後會回到Activity的onTouchEvent。
如果一個子View沒有消費ACTION_DOWN類型的事件,那麼事件將會被另一個子View或者ViewGroup自己消費,之後的事件都只會傳遞給目標子View(mFirstTouchTarget)或者ViewGroup自身。簡單來說,就是如果一個View沒有消費ACTION_DOWN事件,後續事件也不會傳遞進來。
通過 Hardware Layer 提升 Android 動畫性能
當有人問我關於動畫性能表現不佳問題的時候,我首先會詢問他們是否使用了Hardware Layer層。 你的View可能在執行動畫期間的每一幀都進行重繪,如果使用V
Android IntentService使用全面介紹及源碼解析
一 IntentService介紹 IntentService定義的三個基本點:是什麼?怎麼用?如何work? 官方解釋如下: //IntentService定義
Android安全加密:HTTPS編程
Android安全加密專題文章索引 Android安全加密:對稱加密 Android安全加密:非對稱加密 Android安全加密:消息摘要Message Dig
改變Android按鈕背景顏色的高效方法
本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! 本文將介紹一種有效改變Android按鈕顏色的方法。 按鈕可以在狀