相關方法介紹:
dispatchTouchEvent方法:事件的分發。返回值一般由裏面的onTouchEvent方法或者下架View的dispatchTouchEvent方法決定。
onInterceptTouchEvent:事件的攔截,如果Down事件一旦攔截了,那麼之後的move和up事件也會一起攔截。事件不會在向下傳遞。
onTouchEvent:事件的處理。如果在子View中設定了mOnTouchListener,那麼會優先調用mOnTouchListener的方法。如果返回true,
那麼不會在調用onTouchEvent。如果返回false,那麼會繼續走onTouchEvent。如果一個View的down事件沒有
消耗,直接返回的false,那麼其他事件都不會在傳遞到這個View上。事件開始向上傳遞。
事件傳遞源頭:Activity的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
由源碼可以知道,觸摸事件由getWindow().superDispatchTouchEvent(ev)傳遞下去,如果下面的UI控件沒有消耗該時間
由activity的onToucHEvent去消耗。getWindow().superDispatchTouchEvent(ev)的具體實現如下:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
由代碼可知,事件由整個窗口布局的根View DecorView負責進行分發。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在 DecorView中,直接調用了super.dispatchTouchEvent(event),而DecorView是一個FrameLayout,即該方法
走入了ViewGroup裏面的dispatchTouchEvent。接下來具體分析該方法
第一段: // 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事件,那麼初始化一些狀態。
第二段:
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
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;
}
這裏主要分析攔截動作。從代碼可以看出如果是DOWN事件則一定會進入到onInterceptTouchEvent方法判斷是否攔截
如果down事件被父控件攔截了,那麼mFirstTouchTarget必定會是null,所以onInterceptTouchEvent不會在執行,
默認後續的事件都返回intercepted = true;表示攔截。而如果不是DOWN事件,那麼DOWN事件必須由子控件響應過(mFirstTouchTarget!=null表示之前的DOWN事件被子空間消耗了
否則mFirstTouchTarget會依然是null)纔會繼續判斷是否攔截。如果子控件沒有響應過即子控件沒有消耗過DOWN事件,那麼
該地方不會在進行判斷,直接默認攔截,事件不會在傳遞到下層子控件
由此可以得出三個結論:
A、如果父控件攔截了down事件,那麼down事件不會在傳遞給子控件。所有其他事件都不會在傳遞下去並交由父控件處理
B、如果父控件沒有攔截down事件,並且子控件消耗了down事件,那麼後續的move up等事件父控件依然會判斷是否攔截,如果攔截那麼後續的事件不會在往下傳遞,如果攔截move事件,那麼第一個move事件會變成一個cancel事件傳遞到子控件,詳情參見第四段代碼分析
C、如果父控件沒有攔截down事件,並且子控件也沒有消耗down事件,那麼後續的事件不會在傳遞到子控件當中,將有父控件處理。因爲mFirstTouchTarget爲null
disallowIntercept該標誌位表示是否攔截,由子控件去調用父控件的requestDisallowInterceptTouchEvent方法設定,如果設定爲true,ViewGroup將無法攔截除了ACTION_DOWN以外的其它事件
1、子view不希望父view攔截事件可以調用mParent.requestDisallowInterceptTouchEvent(true)
2、viewgroup的mGroupFlags在下一個cycle來臨的時候,FLAG_DISALLOW_INTERCEPT標誌位會被清零
3、requestDisallowInterceptTouchEvent這個函數一般不是自己調用的,而是給子View調用的
4、requestDisallowInterceptTouchEvent是解決滑動衝突的大殺器,目前大部分原生控件都是使用
第三段:
if (!canceled && !intercepted) {
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 找出可以接受事件的子控件
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
第三段代碼是核心代碼,主要做了以下幾個事情,前提條件是父控件沒有進行攔截和取消事件:
1、從當前觸摸區域找出可以相應事件的子控件;
2、通過getTouchTarget方法找到觸摸的目標控件,如果之前DOWN事件觸摸過,那麼直接跳出循環,由後面的代碼進行事件傳遞。
3、如果getTouchTarget方法返回一個null,則之前沒有觸摸過,那麼則通過dispatchTransformedTouchEvent方法傳遞事件給對應區域的子控件。
dispatchTransformedTouchEvent方法是事件傳遞的關鍵方法,在該方法中,如果傳入的child爲null則表示沒有找到對應的子控件消耗。那麼會由父控件去消耗
但是在第三段代碼中,只有找到了子控件纔會調用dispatchTransformedTouchEvent方法,然後會繼續調用子控件的child.dispatchTouchEvent(event);進入下一個輪訓
如果子控件child.dispatchTouchEvent(event)方法返回false,表示子控件沒有消耗該事件,dispatchTransformedTouchEvent也會爲false,那麼newTouchTarget和mFirstTouchTarget都會爲null
這也就是爲什麼第二段代碼會由結論C了。
第四段:
if (mFirstTouchTarget == null) {
// 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;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
這裏代碼也很好理解,首先,如果在第三段代碼沒有找到能消耗事件的子控件,或者父控件進行了攔截沒有進入到第三段代碼中
那麼這裏會直接調用handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
child傳入會null,那麼裏面會直接調用super.dispatchTouchEvent(event);會走到View類中的dispatchTouchEvent方法進行事件傳遞
表示當前觸摸事件由當前父控件進行消耗。
而如果在第三段代碼中找到了消耗down事件,那麼在down事件走到這裏之後,會直接返回handled=true。而後續的move和up事件
因爲找到了mFirstTouchTarget,那麼不會再走進入第三段代碼,會調用
if (dispatchTransformedTouchEvent(ev, cancelChild,arget.child, target.pointerIdBits)) {
handled = true;
}
這裏注意,如果進行了MOVE這個觸摸事件攔截,那麼會觸發一個cancel的事件動作傳遞給之前消耗DOWN事件的子控件,並且在後面將mFirstTouchTarget置空
這樣後續的move事件就不會在傳遞給之前的子控件了。所以一旦父控件執行了onInterceptTouchEvent並且返回true,那麼後續的事件就不會再往下分發。
此時由於子控件已經消耗了down事件,如果後續的move和up事件在這裏沒有消耗,dispatchTransformedTouchEvent返回false,那麼handled也爲false,那麼後續的
事件不會在傳遞給父控件去消耗,而是傳遞給activity中的onTouchEvent方法去處理。
view對touch事件的處理
1、先看view如何分發事件:dispatchTouchEvent
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設置的enable爲true並且也設定了touchEvent的監聽器,那麼就會先響應touchEvent的監聽器。如果enable爲false或者是沒有設定監聽器,那麼會響應
view的onTouchEvent的方法
1、view的onTouchEvent的方法,如果view設置的enable爲false,那麼會進入下面代碼
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
這個時候,如果view的clickable和longClickable是true的話,依舊會返回true,也就是說view即使設置的enable爲false,還是能夠響應touchEvent事件的
如果view設置的enable爲true的話,那麼進入action的判斷狀態
switch (action) {
case MotionEvent.ACTION_UP:
if (!post(mPerformClick)) {
performClick();
}
break;
case MotionEvent.ACTION_DOWN:
checkForLongClick(0, x, y);
break;
case MotionEvent.ACTION_MOVE:
break;
}
在ACTION_UP的地方,會響應click事件;在ACTION_DOWM的地方,會根據時間響應longClick事件。