【Android】事件分發機制源碼解析

1. 分發順序

Activity.dispatchTouchEvent()ViewGroup.dispatchTouchEvent()ViewGroup.onInterceptTouchEvent()View.dispatchTouchEvent()View.onTouchEvent()

2.源碼分析

2.1 Activity中的分發流程

dispatchTouchEvent

首先事件進入Activity.dispatchTouchEvent()

事件鏈:指由ACTION_DOWN開始,途經≥0個ACTION_MOVE,最終結束於ACTION_UPACTION_CANCEL

有特殊情況,可能存在一次事件鏈中有兩個ACTION_DOWN,這涉及到多指操作了,詳情可以查看這篇文章:《每日一問 很多書籍上寫:“事件分發只有一次 ACTION_DOWN,一次 ACTION_UP”嚴謹嗎?》。大概是這樣的:在ViewGroup中收到ACTION_POINTER_DOWN,即手指按下,但是該手指按下的View不是之前的View,因此會在分發過程中變成ACTION_DOWN,然後給到View。

下面開始正式的源碼分析:

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 一條事件鏈的開始
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();// 該方法爲空實現
    }
    
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

排除第一個if語句,直接看第二個。

getWindow()返回的是一個Window對象,而Window對象是一個抽象對象,其唯一實現是PhoneWindow,因此這裏返回的就是一個PhoneWindow。那麼繼續去看看PhoneWindow.superDispatchTouchEvent()

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

mDecor是一個DecorView,那麼繼續跟進。

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

調用了父類的dispatchTouchEvent()。根據源碼可以知道,DecorView是繼承自FrameLayout的。那麼這裏也就相當於調用了FrameLayout.dispatchTouchEvent()。然而在FrameLayout中沒有找到對應的方法,那麼繼續找FrameLayout的父類,也就是ViewGroup。此處留待後面分析ViewGroup的分發的時候再詳看,目前先假設ViewGroup返回了false,即不消費該事件,那麼該事件將由Activity進行消費。

onTouchEvent

現在回到最初的入口,看看後續代碼,onTouchEvent()

/**
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

根據源碼的註釋中可以看到,如果沒有重寫該方法,默認是返回false的,即該事件沒有被消費。但是爲了刨根問底,還是看看mWindow.shouldCloseOnTouch()方法。

首先在PhoneWindow中沒有找到該方法,猜測是在抽象類Window中。

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}

通過同時滿足以下幾個條件來返回true:

  1. mCloseOnTouchOutside爲true

  2. decorView不爲null

  3. 當前事件爲ACTION_OUTSIDE或當前事件爲ACTION_DOWN但是超出邊界

然而通過源碼發現,mCloseOnTouchOutside默認下是爲false的,意味着在默認情況下條件無法同時滿足,因此一直返回false。

至此,在Activity中,一個事件的傳遞結束了。

總結

Activity首先將事件通過Window傳遞給DecorView,然後DecorView通過默認的ViewGroup.dispatchTouchEvent()進行事件分發並返回結果。默認情況下,如果沒有任何ViewGroupView消費事件,那麼該事件最後會去到Activity.onTouchEvent(),方法中沒有對事件進行任何操作,相當於忽略了這個事件。

下面繼續看看,什麼時候ViewGroup.dispatchTouchEvent()返回true什麼時候返回false。

2.2 ViewGroup中的分發流程

dispatchTouchEvent

在上面的分析中提到,事件第一次到ViewGroup是調用到了dispatchTouchEvent()這個方法。

整個方法比較長,這裏慢慢來分析。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
      // 翻譯:如果該事件以可訪問性爲焦點的視圖爲目標,則啓動正常的事件分發。 也許後代將處理點擊。
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    boolean handled = false;
    // onFilterTouchEventForSecurity 當視圖或window被隱藏、遮擋時返回false
    if (onFilterTouchEventForSecurity(ev)) {
        ...暫時省略部分代碼
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

首先整體的看這個方法,在關鍵代碼(我省略的那部分)的前後,都進行了一些驗證性的動作,先不關注。首先涉及到事件分發的第一個關鍵方法,onFilterTouchEventForSecurity(),這個方法主要是判斷當前View和當前window是否被遮擋或隱藏,如果是的話則返回false。也就意味着,如果view被隱藏、遮擋並且觸發事件所在的window也遮擋、隱藏,那麼事件就不會繼續進行傳遞了,直接返回了false。

而根據前面的分析可以知道,返回false之後,事件會傳遞到Activity.onTouchEvent()中,而在其中並沒有對事件進行消費處理,因此事件鏈就這樣結束了。

現在,按照正常流程,分析下前面省略的關鍵代碼。

final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// Handle an initial down.
// 翻譯:處理初次按下
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    // 翻譯:開始新的觸摸手勢時,放棄所有先前的狀態。
    // 由於應用程序切換,ANR或某些其他狀態更改,框架可能已放棄
    // 上一個手勢的擡起或取消事件。

    // 主要是清空mFirstTouchTarget,清空前會向之前的接收事件的view發送一個cancel事件
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

如果是ACTION_DOWN的話,就先清空之前的接收事件的目標以及觸摸狀態等。調用的兩個方法,主要看cancelAndClearTouchTargets()

private void cancelAndClearTouchTargets(MotionEvent event) {
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        if (event == null) {
            final long now = SystemClock.uptimeMillis();
            event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
            syntheticEvent = true;
        }

        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            resetCancelNextUpFlag(target.child);
            // 向之前的接收事件的view發送一個cancel事件
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
        clearTouchTargets();//將mFirstTouchTarget設置爲null

        if (syntheticEvent) {
            event.recycle();
        }
    }
}

先進行了一個判斷,由於ACTION_DOWN是一個事件鏈的開始,因此這裏的mFirstTouchTarget是上一條事件鏈的目標View。注意看,如果傳入的事件爲空的話,則會構建一個ACTION_CANCEL事件。然後遍歷目標View去分發該事件,另外需要注意的是分發事件的方法dispatchTransformedTouchEvent(),第二個參數這裏傳的是true。繼續跟進,查看是如何分發事件的。

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);

        // child爲null時,調用view.dispatchTouchEvent
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...省略了部分代碼
}

這裏我省略掉後面分發正常事件的代碼。這裏可以看到,cancel也就是方法的第二個參數,在上面傳過來的時候是傳的true,因此這裏會進入這個if語句。

在這裏,再次將事件設置成ACTION_CANCEL,然後判斷是否有子View,如果有則向子View傳遞事件,否則將當前ViewGroup當成一個View來處理事件。super.dispatchTouchEvent(event)即調用View.dispatchTouchEvent(event),因爲ViewGroup是繼承自View的,也就相當於讓當前ViewGroup來自己處理這個事件。這個後續再進行分析。

現在回到ViewGroup.dispatchTouchEvent(),知道當遇到ACTION_DOWN時,ViewGroup會向上一條事件鏈的目標發送ACTION_CANCEL事件,然後將mFirstTouchTarget置爲null。

繼續向下看。

// Check for interception.
// 檢查是否需要攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {// 事件鏈開始或之前有消費事件的View
    // 這裏是判斷是否允許攔截事件,可以用requestDisallowInterceptTouchEvent()
    // 來進行設置,默認情況下disallowIntercept=false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        // 這裏先判斷是否需要將事件攔截下來自己處理,默認返回false
        // 這裏也就是事件分發順序中dispatchTouchEvent→onInterceptTouchEvent的體現
        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;
}

這一段主要是判斷是否要攔截當前事件,也是在這裏,將事件傳遞到了ViewGroup.onInterceptTouchEvent()中。注意一下,調用攔截方法是有條件的。

onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        // 這裏的判斷是,鼠標按住左鍵拖動滾動條,所以要攔截事件
        return true;
    }
    return false;
}

默認情況下,是返回false的,也即是不攔截事件。上面返回true的特殊情況不考慮,畢竟很少出現用鼠標的情況。

OK,現在繼續回到ViewGroup.dispatchTouchEvent()中查看後面的代碼。

// Check for cancelation.
// 檢查是否取消
final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;

// Update list of touch targets for pointer down, if needed.
// 翻譯:如有必要,更新觸摸目標列表以使指針向下。
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;// 新的接收事件的目標
boolean alreadyDispatchedToNewTouchTarget = false;// 標記事件是否已經分發

// 下面這個if只給down事件進入的
if (!canceled && !intercepted) {
    // 這裏是找到當前已經獲得焦點的View,下面會用到
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;

    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        
        ...忽略部分關鍵代碼

    }
}

首先進行了是否取消的檢查,然後開始了第一個比較關鍵的代碼塊了,由於代碼過長,所以這裏先省略掉了。省略的那部分關鍵代碼,進行了兩個if的判斷,首先假設第一個if通過,即該事件即不取消也不攔截。那麼進入第二個代碼判斷,滿足一下條件之一即可進入:

  1. ACTION_DOWN事件
  2. 允許多指操作且事件爲ACTION_POINTER_DOWN(手指按下)
  3. ACTION_HOVER_MOVE事件,不屬於常見情況。

下面開始分析第一個關鍵代碼塊了。這裏假設進入的條件是滿足了ACTION_DOWN

...省略部分代碼
for (int i = childrenCount - 1; i >= 0; i--) {
    // 倒序的方式遍歷,優先獲取到新加入的View
    // 這裏是可以改變獲取view的順序的,詳細的話可以查看getAndVerifyPreorderedIndex
    // 和getAndVerifyPreorderedView
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    // 如果前面找到了當前獲取焦點的View,那麼將事件優先傳遞給它
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;// 將循環重置,防止由於優先級的問題錯過了前面的view		
    }

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        // 該子View不能接受event事件:visibility!=visiable或animation=null
        // 或者事件沒有落在子view的範圍內
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    // 查找子view是否在之前的觸摸目標內,由於這裏假設給down
    // 事件進入,因此這裏獲取肯定沒有的,爲null
    // 如果是多指的話,那麼這裏可能不爲null
    newTouchTarget = getTouchTarget(child);

    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // 翻譯:子View已經在其範圍內接觸了。
        // Give it the new pointer in addition to the ones it is handling.
        // 翻譯:除了要處理的手指外,還要爲其提供新的手指。
        
        // 能進入到這裏,表示是有新的手指按下了
        // 那麼下面的分發代碼就不走了
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);

    // 調用dispatchTransformedTouchEvent把事件分發給子View
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // true表示子View消費了事件
        ...省略部分代碼
            
        // 這裏爲mFirstTouchTarget賦值了
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
}
...省略部分代碼

使用一個倒序的for循環遍歷子View,調用canViewReceivePointerEvents()判斷子View是否能有接收事件的能力,調用isTransformedTouchPointInView()判斷事件是否落在子View中。

private static boolean canViewReceivePointerEvents(@NonNull View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}

通過這個方法可以知道,如果子View不是VISIBLE且沒有設置動畫的情況下,是不能接收事件的。

找到目標子View之後,調用dispatchTransformedTouchEvent()進行事件分發,這裏需要注意,第二個參數爲false,並且child是不爲null的。

現在回到之前分析過的dispatchTransformedTouchEvent()方法,這次將會執行後面的正常分發邏輯。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    
    ...省略ACTION_CANCEL代碼
    
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {// 不是新手指按下的事件
        if (child == null || child.hasIdentityMatrix()) {
            // hasIdentityMatrix用於判斷view是否進行過矩陣變換
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                // 對事件的座標進行偏移,因爲這是相對座標
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                // 事件分發給子View的dispatchTouchEvent
                // 體現了事件傳遞中的ViewGroup.dispatchTouchEvent→View.dispatchTouchEvent
                handled = child.dispatchTouchEvent(event);

                // 分發完成後將之前的偏移量移除
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    ...省略了部分代碼

首先判斷是否是多指事件,這裏先假設爲不是,也即newPointerIdBits == oldPointerIdBits條件成立。然後判斷child是否爲null或調用child.hasIdentityMatrix()判斷是否進行過矩陣變換。這裏需要關注的是child.hasIdentityMatrix()當使用過補間動畫平移進行變化時,這個方法會返回false。那麼這裏的話依然假設沒有進行過矩陣變換。

前面說到,這次傳來的child是不爲null的,那麼就可以正常的進行事件分發了。直接調用了child.dispatchTouchEvent(),完成向子View的事件傳遞,並返回結果:子View是否消費事件。

下面假設是多指事件或者child進行過矩陣變化,看看後面的代碼針對這種情況是如何進行事件分發的。

if (child == null) {
    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);
}

可以很明顯的看到,這裏事件調用了transform()方法,並且傳入的矩陣是child.getInverseMatrix()。根據方法名稱可以猜到,這個方法返回的矩陣是執行變化前的,也就意味着獲取到的是child本身的矩陣。這也就導致了事件的觸發區域依然在之前的位置了。

現在回到前面,假設這裏子View消費了事件,也即dispatchTransformedTouchEvent()返回了true,然後會調用addTouchTarget()mFirstTouchTarget賦值,然後構建一個TouchTarget對象,其nextmFirstTouchTargetchild爲目標View,然後將該TouchTarget返回並賦值給newTouchTarget

到這裏,關於ACTION_DOWN如何找到目標View就完成了。通過這裏的分析,可以知道,一條事件鏈只有在ACTION_DOWNACTION_PONITER_DOWN的時候纔會去找目標View,後續的事件將直接傳遞到目標View。

下面看看如果沒有找到目標View或不是ACTION_DOWN事件會如何進行處理。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    // 翻譯:沒有觸摸目標,因此請將其視爲普通視圖。
    
    // 沒有觸摸目標,事件將會由默認的View.dispatch去處理
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    ...省略找到目標view後分發事件的代碼
}

如果沒有找到目標View,則開始分發事件,注意dispatchTransformedTouchEvent()的第三個參數,這裏傳的是null。回想前面看過的源碼,可以知道,在方法經常數出現這麼一個if語句:

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    ...省略部分代碼
    handled = child.dispatchTouchEvent(event);
}
return handled;

因此可以知道,如果child即方法的第三個參數爲null的情況下,是會將該ViewGroup當成普通的View去處理事件,即調用View.dispatchTouchEvent()

下面看看如何分發其他非ACTION_DOWN事件。

TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
    final TouchTarget next = target.next;
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
        // down事件,並且被消費,那麼就會進入到這裏
        // 而且target.next是Null,所以在下次循環的時候就會退出
        handled = true;
    } else {
        // 如果攔截事件的話,這裏爲true
        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                || intercepted;
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)) {
            // 將事件分發到子view中
            handled = true;
        }
        if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();// 這裏將target.child清空了,即沒有目標view了
            target = next;
            continue;
        }
    }
    predecessor = target;
    target = next;
}

這裏的話,使用循環去向target.child分發事件,不難理解。但是關鍵需要關注這一段代碼:

if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();// 這裏將target.child清空了,即沒有目標view了
            target = next;
            continue;
        }

這段代碼實現的是:當該ViewGroup攔截事件的話,那麼會將之前的目標View給清空,也就導致後續分發事件的時候,target.child爲null。並且mFirstTouchTarget最後也會變成null,因爲next在最後的目標View肯定是null的。

總結

首先可以確定的是,具體進行事件分發的方法是dispatchTransformedTouchEvent(),而該方法的第三個參數就是目標View。如果爲null的話,就會把事件交給當前ViewGroup去處理,調用當前ViewGroup的super.dispatchTouchEvent()

通過前面的源碼分析可以得出以下結論:

  1. 如果當前ViewGroup攔截了事件鏈中的任何一次事件,那麼該事件鏈後續的事件都會被攔截下來,即使onInterceptTouchEvent()返回false;並且後續的事件也不會再走onInterceptTouchEvent()
  2. 能接收事件的View要麼是可見的(Visibility=Visible)要麼設置了動畫(getAnimation()!=null)。
  3. 對於執行過矩陣變換的View,會獲取最初的矩陣去當做目標位置進行事件分發。這是爲什麼補間動畫執行後原來的位置才能響應事件的原因。

至此,ViewGroup中的分發流程就走完了。其實可以看到,整個事件分發流程的主要就是在ViewGroup,因爲View是不具備分發功能的,畢竟它下面沒東西了。

2.3 View中的分發流程

在View中,關鍵方法是onTouchEvent(),如果沒有重寫該方法的話,默認下所有控件對事件的處理都是在這裏執行的。如果說ViewGroup.dispatchTouchEvent()是事件分發流程中的主要步驟,那麼View.onTouchEvent()就是事件處理的主要步驟,也是事件分發流程中最後的步驟。

儘管View不具備再將事件傳遞下去的資格,然而它依然有dispatchTouchEvent()方法。在方法中主要是確定事件該交由誰處理,是自己的onTouchEvent()處理還是給onTouchListener處理?

dispatchTouchEvent

和ViewGroup一樣,上來首先判斷當前事件是否要求傳遞給已經獲取焦點的View。

if (event.isTargetAccessibilityFocus()) {
    // We don't have focus or no virtual descendant has it, do not handle the event.
    // 沒有焦點,無法處理該事件
    if (!isAccessibilityFocusedViewOrHost()) {
        return false;
    }
    // We have focus and got the event, then use normal event dispatch.
    event.setTargetAccessibilityFocus(false);
}

如果當前事件要求傳遞給獲取到焦點的View,但是當前View沒有獲取到焦點,那麼直接返回false,無法處理事件。否則就進入正常的流程。

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopNestedScroll();
}

屬於優化手感的代碼,如果是ACTION_DOWN事件的話,則停止滾動。

下面是方法的關鍵代碼,首先進行了一個判斷當前View是否可以接收事件的判斷,如果可以則進行分發。

if (onFilterTouchEventForSecurity(event)) {// 判斷當前view和當前window是否能接收事件
    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)) {
        // 這裏通知到onTouchListener,如果返回true則返回true
        result = true;
    }

    // 如果前面的listener返回了true的話,那麼onTouchEvent也就不執行了
    if (!result && onTouchEvent(event)) {
        result = true;
    }
}

這裏的話主要是將事件分發給了3個地方處理,首先給到handleScrollBarDragging(),根據名字可以知道這個是處理滾動條事件的,該方法只處理來自鼠標的事件,因此一般情況下都返回false。

然後將事件交給onTouchListener處理,並記錄處理結果。

最後再判斷,前面兩者是否已經消費了事件,如果是的話則不再將事件分發給onTouchEvent(),否則再將事件交給onTouchEvent()去處理。

通過這裏我們可以知道,如果我們在onTouchListener中返回了true,即消費了事件的話,那麼該事件將不再進入View.onTouchEvent()。也就意味着無法處理點擊、長按等事件處理了。

View.dispatchTouchEvent()中的關鍵邏輯就這麼多,後面的代碼不涉及到事件分發,因此就不繼續分析了。下面去看看View.onTouchEvent()中,對事件的默認處理是怎麼樣的。

onTouchEvent

// 當前view是否可點擊
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {// 當前view不可用
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        // 擡起事件,且當前頁面處於按壓狀態,則釋放按壓狀態
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    // 翻譯:可單擊的禁用視圖仍然消耗觸摸事件,只是不響應它們。
    return clickable;
}

// 如果代理對象消費了事件,那麼事件將不再進行默認處理
if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}
...省略默認處理的代碼
    return false;

首先判斷當前View是否允許點擊,該變量在後面需要用到。

然後判斷View是否是處於不可用狀態,如果不可用的話則直接將是否可點擊作爲是否消費事件返回。也就是那段註釋所說的:可單擊的禁用視圖仍然消耗觸摸事件,只是不響應它們。

最後將事件交給代理去處理,如果代理處理了事件,那麼事件分發到此結束。否則就繼續往下,使用默認的實現對事件進行處理。

這裏可以看到,如果需要干預事件的處理的話,可以對View設置一個代理,然後在代理中進行自己的處理邏輯。是否覆蓋默認邏輯則取決於在代理中是否返回了true。

下面看看,View中默認處理事件的邏輯是怎麼樣的。

首先要注意的是,要進入默認處理的邏輯需要滿足以下條件之一:

  1. 當前View可點擊
  2. View.flag爲TOOLTIP(長按或懸浮時會出現工具條提示)

一般來說滿足第一個即可。然後會根據不同的動作進行處理。下面將按照ACTION_DOWNACTION_MOVEACTION_UPACTION_CANCEL來進行分析。

ACTION_DOWN
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
// 注意這裏將這個變量設爲false了,在ACTION_UP會用到這個變量
mHasPerformedLongPress = false;

if (!clickable) {
    // 不允許點擊,那麼這裏就去觸發長按事件
    // 前面知道,這裏進入的條件只有兩個,既然不滿足可點擊
    // 那麼這裏肯定就是長按的時候會出現工具條提示
    checkForLongClick(0, x, y);
    break;
}

if (performButtonActionOnTouchDown(event)) {
    // 一般情況下都返回false
    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 |= PFLAG_PREPRESSED;
    if (mPendingCheckForTap == null) {
        mPendingCheckForTap = new CheckForTap();
    }
    mPendingCheckForTap.x = event.getX();
    mPendingCheckForTap.y = event.getY();
    // 延遲100ms後再觸發反饋,即else的代碼
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
    // Not inside a scrolling container, so show the feedback right away
    // 立刻顯示反饋
    setPressed(true, x, y);
    checkForLongClick(0, x, y);
}

ACTION_DOWN中,主要是涉及到了長按的邏輯。checkForLongClick()CheckForTap都是和長按有關的。它們都是通過向當前View的Handler發送一個Runnable去執行長按的。

private void checkForLongClick(int delayOffset, float x, float y) {
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        mHasPerformedLongPress = false;

        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.setAnchor(x, y);
        mPendingCheckForLongPress.rememberWindowAttachCount();
        mPendingCheckForLongPress.rememberPressedState();
        // post一個Runnable,延遲500ms,然後Runnable會觸發長按performLongClick()
        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
}

    private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick(mX, mY)) {// 觸發長按事件
                    // 注意,這裏將變量設爲了true,即表示觸發了長按事件
                    mHasPerformedLongPress = true;
                }
            }
        }
    }

首先發送一個延遲Runnable,在Runnable中去觸發長按,即performLongClick()。這個延遲時間就是View判斷當前事件是否是長按的一個閾值,500毫秒。

這種通過Handler的延時機制來執行延時操作的方法在Android源碼中很多地方都有用到。

ACTION_MOVE
if (clickable) {
    // 將當前觸摸位置傳遞給background、foreground
    drawableHotspotChanged(x, y);
}

// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {// 划着划着滑出視圖範圍了
    // Outside button
    // Remove any future long press/tap checks
    // 移除之前添加的的長按、按壓等runnable
    // 這裏不移除click的原因是,click只有在ACTION_UP的時候纔會添加
    removeTapCallback();
    removeLongPressCallback();
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);// 取消按壓狀態
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}

基本都在註釋裏面了,大意是當滑出當前View的範圍後,之前在ACTION_DOWN中添加了長按、按下等Runnable都會被從隊列中移除,並且即使後來又滑入了當前View的範圍,因爲沒有走ACTION_DOWN,所以也不會觸發長按了。

ACTION_UP

由於代碼過長,因此分段進行分析。

if ((viewFlags & TOOLTIP) == TOOLTIP) {
    handleTooltipUp();// 關閉工具提示
}
if (!clickable) {
    // 移除可能在down的時候發送的Runnable
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    break;
}

再次提醒,前面說到的代碼都是在允許點擊或有ToolTip的情況下才會執行的。

首先會判斷是否有ToolTip,如果有的話則去post一個ToolTip的Runnable。關於ToolTip,這是個在SDK≥26後出現的一個功能。調用setToolTipText()可以設置提示文本,當長按的時候會彈出一個小窗口顯示文本信息。那麼這個功能和長按監聽器如何處理衝突呢?這裏不具體分析,結論是:當onLongClickListener()返回false的時候,將會去顯示ToolTip,返回true則不顯示。

注意上面if(!clickable)中使用了break,所以下面的代碼都是在可點擊的情況下才執行的!

boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    boolean focusTaken = false;
    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
        focusTaken = requestFocus();// 獲取焦點
    }

   	...省略部分代碼	
    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
		// 這是個點擊事件,移除長按runnable
        removeLongPressCallback();

        if (!focusTaken) {
            if (mPerformClick == null) {
                mPerformClick = new PerformClick();
            }
            if (!post(mPerformClick)) {// post失敗的情況下,直接觸發click
                performClickInternal();
            }
        }
    }

    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();
    }

    // 移除按壓,檢查長按的runnable
    removeTapCallback();
}
mIgnoreNextUpEvent = false;

整個代碼主要執行了兩個動作,移除非點擊相關的Runnable,以及Post一個點擊Runnable。

需要滿足以下幾個條件纔會當成點擊事件處理:

  1. mHasPerformedLongPress爲false,該變量在觸發長按,即performLongClick()後爲true。
  2. mIgnoreNextUpEvent爲false,該變量僅在非Touch事件才有可能爲true。
  3. 當前View能夠獲取到焦點。

這裏有個問題,那就是爲什麼不直接調用performClick()去觸發點擊,而是要使用Handler去post呢?根據源碼註釋,官方是這樣解釋的:

This lets other visual state of the view update before click action start.

谷歌翻譯:這樣,在單擊操作開始之前,View的其他視覺狀態就會更新。

ACTION_CANCEL
// 執行一些清除操作
if (clickable) {
    setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

移除所有可能添加的Runnable,重置狀態。

總結

  1. onTouchListener中返回了true的話,事件將不再傳遞到onTouchEvent(),即事件被消費了。
  2. 可單擊的禁用View依然會消費事件,但是不會觸發onClickListener()
  3. 如果設置了代理,且在代理的onTouchEvent()返回true的話,事件將不再進行默認的處理。
  4. 如果按下之後,進行滑動,並且滑出了當前View的範圍,那麼將不再觸發長按事件,即使後面又滑入當前View的範圍。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章