事件分發機制的詳解及原始碼分析
阿新 • • 發佈:2019-02-03
事件分發機制詳解
MotionEvent 主要分為以下幾個事件型別:
ACTION_DOWN 手指開始觸控到螢幕的那一刻響應的是DOWN事件
ACTION_MOVE 接著手指在螢幕上移動響應的是MOVE事件
ACTION_UP 手指從螢幕上鬆開的那一刻響應的是UP事件
所以事件順序是: ACTION_DOWN -> ACTION_MOVE -> ACTION_UP
事件分發機制的三個主要方法:
public boolean dispatchTouchEvent(MotionEvent event) —— 分發事件 作用是用來進行事件的分發。一般在這個方法裡必須寫 return super.dispatchTouchEvent 。如果不寫super.dispatchTouchEvent,而直接改成 return true 或者 false,則事件傳遞到這裡時便終止了,既不會繼續分發也不會回傳給父元素。 public boolean onInterceptTouchEvent(MotionEvent event) —— 攔截事件 只有ViewGroup才有這個方法。View只有dispatchTouchEvent和onTouchEvent兩個方法。因為View沒有子View,所以不需要攔截事件。而ViewGroup 裡面可以包裹子View,所以通過onInterceptTouchEvent方法,ViewGroup可以實現攔截,攔截了的話,ViewGroup就不會把事件繼續分發給子View了 ,也就是說在這個ViewGroup中的子View都不會響應到任何事件了。onInterceptTouchEvent 返回true時,表示ViewGroup會攔截事件。 public boolean onTouchEvent(MotionEvent event) —— 消費事件 onTouchEvent 返回true時,表示事件被消費掉了。一旦事件被消費掉了,其他父元素的onTouchEvent方法都不會被呼叫。如果沒有人消耗事件,則最終 當前Activity會消耗掉。則下次的MOVE、UP事件都不會再傳下去了。 需要注意的一些事項: 一般我們在自定義ViewGroup時不會攔截Down事件,因為一旦攔截了Down事件,那麼後續的Move和Up事件都不會再傳遞下去到子元素了,事件以後都會 只交給ViewGroup這裡。 一個Down事件分發完了之後,還有回傳的過程。因為一個事件分發包括了Action_Down、Action_Move、Action_Up這幾個動作。當手指觸控到螢幕的那一刻 ,首先分發Action_Down事件,事件分發完後還要回傳回去,然後繼續從頭開始分發,執行下一個Aciton_Move操作,直到執行完Action_Up事件,整個事 件分發過程便到此結束。
事件分發機制的流程
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <me.anany.ViewGroupA android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_blue_bright"> <me.anany.ViewGroupB android:layout_width="300dp" android:layout_height="300dp" android:background="@android:color/holo_green_dark"> <me.anany.CustomView android:id="@+id/btn" android:text="Button" android:background="@android:color/holo_red_dark" android:layout_width="100dp" android:layout_height="100dp" /> </me.anany.ViewGroupB> </me.anany.ViewGroupA> </RelativeLayout>
1.點選View區域但View不消耗事件 流程圖解析: 事件分發 當在螢幕上點選一個View時,首先執行到的是MainActivity的dispatchTouchEvent方法,這裡便是事件分發的起點。紅色箭頭流向便是事件分發的流向。 事件傳遞到ViewGroupA時,因為它不攔截事件,所以它要先去問它的子控制元件ViewGroupB是否要消費事件,然後將事件分發給ViewGroupB。事件到了ViewGroupB 時,它不攔截事件,所以它也要先去問它的子控制元件們要不要消費事件,然後將事件分發給View。事件到了View時開始執行dispatchTouchEvent,因為已經到了最 底層了,View接下來便開始執行onTouchEvent方法來決定是否消費事件。 事件回傳 由於View沒有消費事件,所以它開始回傳資訊,(紫色箭頭的流向便是事件回傳方向),以告訴ViewGroupB我不消費事件了,view 的 onTouchEvent 便return false 。然後ViewGroupB才開始有權利決定我是否要開始消費事件(因為它已經問過它的子控制元件是否要消費事件了,而它的子控制元件並沒有消費),所以開始執行ViewGroupB的 onTouchEvent方法,由於ViewGroupB也不消費事件,所以它也 return false 。事件繼續回傳給ViewGroupA,這個時候它終於開始有權利決定我是否要消費事件了, 所以開始執行ViewGroupA的onTouchEvent方法,由於ViewGroupA也不感興趣不消費事件,所以它也return false。最終你們這些孩兒們都不消費事件,那事件最終只能 扔給MainActivity去消費了。
2.點選View區域且View消耗事件
流程圖解析:
事件分發
當在螢幕上點選一個View時,首先執行到的是MainActivity的dispatchTouchEvent方法,這裡便是事件分發的起點。紅色箭頭流向便是事件分發的流向。
事件傳遞到ViewGroupA時,因為它不攔截事件,所以它要先去問它的子控制元件ViewGroupB是否要消費事件,然後將事件分發給ViewGroupB。事件到了ViewGroupB時,
它不攔截事件,所以它也要先去問它的子控制元件們要不要消費事件,然後將事件分發給View。事件到了View時開始執行dispatchTouchEvent,因為已經到了最底層了,
View接下來便開始執行onTouchEvent方法來決定是否消費事件。
事件回傳
由於View消費了事件,所以它開始回傳,(紫色箭頭的流向便是事件回傳方向),以告訴ViewGroupB我已經消費事件了,view 的 onTouchEvent 便return true。然後
ViewGroupB 收到了View return true 就知道事件已經被View消費掉了,所以不會執行ViewGroupB的onTouchEvent方法,只能往上回傳 return true 去告訴
ViewGroupA事件已經被消費掉了,你沒機會了 。然後事件繼續回傳給ViewGroupA,A收到return true 便知道 事件被消費了,所以它也return true。最終事件回傳到了
MainActivity,由於事件被消費了,所以不會執行MainActivity的onTouchEvent方法。接下來又開始執行Move事件了,流程又和之前的一樣重新開始處理。
3 點選ViewGroupB區域但不消耗事件
很顯然在這裡點選的ViewGroupB區域,並不在View的範圍內,所以事件也不會分發到View。
4 點選View區域,View消耗事件,但設定了View.onTouchListener
View的mOnTouchListener.onTouch方法優先於View的onTouchEvent方法被執行。
事件分發機制原始碼分析
原始碼妥妥的是最新版5.0: 我們先從Activity.dispatchTouchEveent()說起:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
程式碼一看能感覺出來DOWN事件比較特殊。我們繼續走到onUserInteraction()程式碼中.
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
但是該方法是空方法,沒有具體實現。 我們往下看getWindow().superDispatchTouchEvent(ev).
getWindow()獲取到當前Window物件,表示頂層視窗,管理介面的顯示和事件的響應;每個Activity 均會建立一個PhoneWindow物件, 是Activity和整個View系統互動的介面,但是該類是一個抽象類。 從文件中可以看到The only existing implementation of this abstract class is android.policy.PhoneWindow, which you should instantiate when needing a Window., 所以我們找到PhoneWindow類,檢視它的superDispatchTouchEvent()方法。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
該方法又是呼叫了mDecor.superDispatchTouchEvent(event), mDecor是什麼呢? 從名字中我們大概也能猜出來是當前視窗最頂層的DecorView, Window介面的最頂層的View物件。
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
講到這裡不妨就提一下DecorView.
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
...
}
它整合子FrameLayout所有很多時候我們在用佈局工具檢視的時候發現Activity的佈局FrameLayout的。就是這個原因。
好了,我們接著看DecorView中的superDispatchTouchEvent()方法。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
是呼叫了super.dispatchTouchEveent(),而DecorView的父類是FrameLayout所以我們找到FrameLayout.dispatchTouchEveent(). 我們看到FrameLayout中沒有重寫dispatchTouchEveent()方法,所以我們再找到FrameLayout的父類ViewGroup.看ViewGroup.dispatchTouchEveent()實現。 新大陸浮現了...
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// Consistency verifier for debugging purposes.是除錯使用的,我們不用管這裡了。
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
boolean handled = false;
// onFilterTouchEventForSecurity()用安全機制來過濾觸控事件,true為不過濾分發下去,false則銷燬掉該事件。
// 方法具體實現是去判斷是否被其它視窗遮擋住了,如果遮擋住就要過濾掉該事件。
if (onFilterTouchEventForSecurity(ev)) {
// 沒有被其它視窗遮住
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 下面這一塊註釋說的很清楚了,就是在`Down`的時候把所有的狀態都重置,作為一個新事件的開始。
// 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.
cancelAndClearTouchTargets(ev);
resetTouchState();
// 如果是`Down`,那麼`mFirstTouchTarget`到這裡肯定是`null`.因為是新一系列手勢的開始。
// `mFirstTouchTarget`是處理第一個事件的目標。
}
// 檢查是否攔截該事件(如果`onInterceptTouchEvent()`返回true就攔截該事件)
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 標記事件不允許被攔截, 預設是`false`, 該值可以通過`requestDisallowInterceptTouchEvent(true)`方法來設定,
// 通知父`View`不要攔截該`View`上的事件。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 判斷該`ViewGroup`是否要攔截該事件。`onInterceptTouchEvent()`方法預設返回`false`即不攔截。
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
// 子`View`通知父`View`不要攔截。這樣就不會走到上面`onInterceptTouchEvent()`方法中了,
// 所以父`View`就不會攔截該事件。
intercepted = false;
}
} else {
// 註釋比較清楚了,就是沒有目標來處理該事件,而且也不是一個新的事件`Down`事件(新事件的開始),
// 我們應該攔截下他。
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// Check for cancelation.檢查當前是否是`Cancel`事件或者是有`Cancel`標記。
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed. 這行程式碼為是否需要將當前的觸控事件分發給多個子`View`,
// 預設為`true`,分發給多個`View`(比如幾個子`View`位置重疊)。預設是true
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
// 儲存當前要分發給的目標
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果沒取消也不攔截,進入方法內部
if (!canceled && !intercepted) {
// 下面這部分程式碼的意思其實就是找到該事件位置下的`View`(可見或者是在動畫中的View), 並且與`pointID`關聯。
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
// 遍歷找子`View`進行分發了。
final View[] children = mChildren;
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);
// `canViewReceivePointerEvents()`方法會去判斷這個`View`是否可見或者在播放動畫,
// 只有這兩種情況下可以接受事件的分發
// `isTransformedTouchPointInView`判斷這個事件的座標值是否在該`View`內。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 找到該`View`對應的在`mFristTouchTarget`中的儲存的目標, 判斷這個`View`可能已經不是之前`mFristTouchTarget`中的`View`了。
// 如果找不到就返回null, 這種情況是用於多點觸控, 比如在同一個`View`上按下了多跟手指。
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child View已經接受了這個事件了
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
// 找到該View了,不用再迴圈找了
break;
}
resetCancelNextUpFlag(child);
// 如果上面沒有break,只有newTouchTarget為null,說明上面我們找到的Child View和之前的肯定不是同一個了,
// 是新增的, 比如多點觸控的時候,一個手指按在了這個`View`上,另一個手指按在了另一個`View`上。
// 這時候我們就看child是否分發該事件。dispatchTransformedTouchEvent如果child為null,就直接該ViewGroup出來事件
// 如果child不為null,就呼叫child.dispatchTouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 如果這個Child View能分發,那我們就要把之前儲存的值改變成現在的Child View。
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 賦值成現在的Child View對應的值,並且會把`mFirstTouchTarget`也改成該值(mFristTouchTarget`與`newTouchTarget`是一樣的)。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 分發給子`View`了,不用再繼續迴圈了
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
if (preorderedList != null) preorderedList.clear();
}
// `newTouchTarget == null`就是沒有找到新的可以分發該事件的子`View`,那我們只能用上一次的分發物件了。
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// DOWN事件在上面會去找touch target
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// dispatchTransformedTouchEvent方法中如果child為null,那麼就呼叫super.dispatchTouchEvent(transformedEvent);否則呼叫child.dispatchTouchEvent(transformedEvent)。
// `super.dispatchTouchEvent()`也就是說,此時`Viewgroup`處理`touch`訊息跟普通`view`一致。普通`View`類內部會呼叫`onTouchEvent()`方法
// No touch targets so treat this as an ordinary view. 自己處理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} 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;
// 找到了新的子`View`,並且這個是新加的物件,上面已經處理過了。
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 否則都呼叫dispatchTransformedTouchEvent處理,傳遞給child
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 正常分發
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果是onInterceptTouchEvent返回true就會遍歷mFirstTouchTarget全部給銷燬,這就是為什麼onInterceptTouchEvent返回true,之後所有的時間都不會再繼續分發的了。
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
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);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
接下來還要說說dispatchTransformedTouchEvent()方法,雖然上面也說了大體功能,但是看一下原始碼能說明另一個問題:
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
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();
// 這就是為什麼時間被攔截之後,之前處理過該事件的`View`會收到`CANCEL`.
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
// 子`View`去處理,如果子`View`仍然是`ViewGroup`那還是同樣的處理,如果子`View`是普通`View`,普通`View`的`dispatchTouchEveent()`會呼叫`onTouchEvent()`.
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
上面講了ViewGroup的dispatchTouchEveent()有些地方會呼叫super.dispatchTouchEveent(),而ViewGroup的父類就是View,接下來我們看一下View.dispatchTouchEveent()方法:
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// 除錯用
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
// 判斷該`View`是否被其它`View`遮蓋住。
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// 先執行`listener`.
result = true;
}
if (!result && onTouchEvent(event)) {
// 執行`onTouchEvent()`.
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
通過上面的分析我們看到View.dispatchTouchEvent()裡面會呼叫到onTouchEvent()來消耗事件。那麼onTouchEvent()是如何處理的呢?下面我們看一下 View.onTouchEvent()原始碼:
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
// 對disable按鈕的處理,註釋說的比較明白,一個disable但是clickable的view仍然會消耗事件,只是不響應而已。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
// 關於TouchDelegate,文件中是這樣說的The delegate to handle touch events that are physically in this view
// but should be handled by another view. 就是說如果兩個View, View2在View1中,View1比較大,如果我們想點選
// View1的時候,讓View2去響應點選事件,這時候就需要使用TouchDelegate來設定。
// 簡單的理解就是如果這個View有自己的時間委託處理人,就交給委託人處理。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
// 這個View可點選
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
// 最好先看DOWN後再看MOVE最後看UP。
// PFLAG_PREPRESSED 表示在一個可滾動的容器中,要稍後才能確定是按下還是滾動.
// PFLAG_PRESSED 表示不是在一個可滾動的容器中,已經可以確定按下這一操作.
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// 處理點選或長按事件
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused())
// 如果現在還沒獲取到焦點,就再獲取一次焦點
focusTaken = requestFocus();
}
// 在前面`DOWN`事件的時候會延遲顯示`View`的`pressed`狀態,使用者可能在我們還沒有顯示按下狀態效果時就不按了.我們還是得在進行實際的點選操作時,讓使用者看到效果。
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.
setPressed(true, x, y);
}
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();
}
// PerformClick就是個Runnable,裡面執行performClick()方法。performClick()方法中怎麼執行呢?我們在後面再說。
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
// 取消按下狀態,UnsetPressedState也是個Runnable,裡面執行setPressed(false)
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;
// performButtonActionOnTouchDown()處理滑鼠右鍵選單,有些View顯示右鍵選單就直接彈選單.一般裝置用不到滑鼠,所以返回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.
// 就是遍歷下View層級,判斷這個View是不是在一個能scroll的View中。
if (isInScrollingContainer) {
// 因為使用者可能是點選或者是滾動,所以我們不能立馬判斷,先給使用者設定一個要點選的事件。
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
// 傳送一個延時的操作,用於判斷使用者到底是點選還是滾動。其實就是在tapTimeout中如果使用者沒有滾動,那就是點選了。
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// 設定成點選狀態
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
// 檢查是否是長按,就是過一段時間後如果還在按住,那就是長按了。長按的時間是ViewConfiguration.getLongPressTimeout()
// 也就是500毫秒
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
// 取消按下狀態,移動點選訊息,移動長按訊息。
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons, 檢查是否移動到View外面了。
if (!pointInView(x, y, mTouchSlop)) {
// 移動到區域外面去了,就要取消點選。
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
上面講了Touch事件的分發和處理,隨便說一下點選事件:
我們平時使用的時候都知道給View設定點選事件是setOnClickListener()
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
// `getListenerInfo()`就是判斷成員變數`mListenerInfo`是否是null,不是就返回,是的話就初始化一個。
getListenerInfo().mOnClickListener = l;
}
那什麼地方會呼叫mListenerInfo.mOnClickListener呢?
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}