接着看代碼塊3,在這段很長的代碼裡,首先一個 if if DOWN DOWN dispatchTransformedTouchEvent addTouchTarget TouchTarget.obtain TouchTarget true
中判斷了該事件是否滿足沒有被攔截和被取消,之後第二個
判斷了事件類型是否為
,滿足了沒有被攔截和取消的
事件,接下來ViewGroup才會循環其子View找到點選事件在其内部并且能夠接受該事件的子View,再通過調用
方法将事件分發給該子View處理,傳回true說明子View成功消費事件,于是調用
方法,方法中通過
方法獲得一個包含這View的
節點并将其添加到連結清單頭,并将已經分發的标記設定為
。
接下來看代碼塊4:
// Dispatch to touch targets.
//走到這裡說明在循環周遊所有子View後沒有找到接受該事件或者事件不是DOWN事件或者該事件已被攔截或取消
if (mFirstTouchTarget == null) {
//mFirstTouchTarget為空說明沒有子View響應消費該事件
//所有調用dispatchTransformedTouchEvent方法分發事件
//注意這裡第三個參數傳的是null,方法裡會調用super.dispatchTouchEvent(event)即View.dispatchTouchEvent(event)方法
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// mFirstTouchTarget不為空說明有子View能響應消費該事件,消費過之前的DOWN事件,就将這個事件還分發給這個View
// 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;
//這裡傳入的是target.child就是之前響應消費的View,把該事件還交給它處理
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;
}
}
之前在代碼塊3中處理分發了沒被攔截和取消的
DOWN
事件,那麼其他
MOVE
、
UP
等類型事件怎麼處理呢?還有如果周遊完子View卻沒有能接受這個事件的View又怎麼處理呢?代碼塊4中就處理分發了這些事件。首先判斷
mFirstTouchTarget
是否為空,為空說明沒有子View消費該事件,于是就調用
dispatchTransformedTouchEvent
方法分發事件,這裡注意
dispatchTransformedTouchEvent
方法第三個參數View傳的
null
,方法裡會對于這種沒有子View能處理消費事件的情況,就調用該ViewGroup的
super.dispatchTouchEvent
方法,即View的
dispatchTouchEvent
,把ViewGroup當成View來處理,把事件交給ViewGroup處理。具體看
dispatchTransformedTouchEvent
方法中的這段代碼:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
dispatchTransformedTouchEvent
方法中
child
即傳入的View為空則調用
super.dispatchTouchEvent
方法分發事件,就是View類的分發方法,不為空則調用子View方法,即
child.dispatchTouchEvent
分發事件,是以歸根結底都是調用了View類的
dispatchTouchEvent
方法處理。
至此,ViewGroup中的分發過流程結束,再來總結一下這個過程:首先過濾掉不安全的事件,接着如果事件類型是
DOWN
事件認為是一個新的事件序列開始,就清空
TouchTarget
連結清單重置相關标志位(代碼塊一),然後判斷是否攔截該事件,這裡有兩步判斷:一是如果是
DOWN
事件或者不是
DOWN
事件但是
mFirstTouchTarget
不等于
null
(這裡
mFirstTouchTarget
如果等于
null
說明之前沒有View消費
DOWN
事件,在代碼塊三末尾,可以看到如果有子View消費了
DOWN
事件,會調用
addTouchTarget
方法,獲得一個儲存了該子View的
TouchTarget
,并将其添加到
mFirstTouchTarget
連結清單頭),則進入第二步禁止攔截标記的判斷,否則直接設定為需要攔截,進入第二步判斷設定過禁止攔截标記為
true
的就不攔截,否則調用ViewGroup的
onInterceptTouchEvent
方法根據傳回接過來決定是否攔截(代碼塊二)。接下來如果事件沒被攔截也沒被取消而且還是
DOWN
事件,就循環周遊ViewGroup中的子View找到事件在其範圍内并且能接受事件的子View,通過
dispatchTransformedTouchEvent
方法将事件分發給該子View,然後通過
addTouchTarget
方法将包含該子View的
TouchTarget
插到連結清單頭(代碼塊三)。最後如果沒有找到能夠接受該事件的子View又或者是
MOVE
UP
類型事件等再判斷
mFirstTouchTarget
是否為空,為空說明之前沒有View能接受消費該事件,則調用
dispatchTransformedTouchEvent
方法将事件交給自身處理,不為空則同樣調用
dispatchTransformedTouchEvent
方法,但是是将事件分發給該子View處理。
ViewGroup的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;
}
在ViewGroup的
dispatchTouchEvent
中沒設定過禁止攔截的事件預設都會通過
onInterceptTouchEvent
方法來決定是否攔截,
onInterceptTouchEvent
方法裡可以看到預設是傳回
false
,隻有在事件源類型是滑鼠并且是
DOWN
事件是滑鼠點選按鈕和是滾動條的手勢時才傳回
true
。是以預設一般ViewGroup的
onInterceptTouchEvent
方法傳回都為
false
,也就是說預設不攔截事件。
ViewGroup的onTouchEvent方法:
ViewGroup中沒有覆寫
onTouchEvent
方法,是以調用ViewGroup的
onTouchEvent
方法實際上調用的還是它的父類View的
onTouchEvent
方法。
View的dispatchTouchEvent方法:
在ViewGroup中将事件無論是分發給子View的時候還是自己處理的,最終都會執行預設的View類的
dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
......
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;
}
}
......
return result;
}
這裡同樣省略一些代碼隻看關鍵的,首先同樣和ViewGroup一樣,做了事件安全性的過濾,接着先判斷了
mOnTouchListener
是否為空,不為空并且該View是
ENABLED
可用的,就會調用
mOnTouchListener
的
onTouch
方法,如果
onTouch
方法傳回
true
說明事件已經被消費了,就将
result
标記修改為
true
,這樣他就不會走接下來的
if
了。如果沒有設定
mOnTouchListener
或者
onTouch
false
,則會繼續調用
onTouchEvent
方法。這裡可以發現
mOnTouchListener
onTouch
方法的優先級是在
onTouchEvent
之前的,如果在代碼中設定了
mOnTouchListener
監聽,并且
onTouch
傳回
true
,則這個事件就被在
onTouch
裡消費了,不會在調用
onTouchEvent
//這個mOnTouchListener就是經常在代碼裡設定的View.OnTouchListener
mMyView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//這裡傳回true事件就消費了,不會再調用onTouchEvent方法
return true;
}
});
View的onTouchEvent方法:
public boolean onTouchEvent(MotionEvent event) {
/---------------代碼塊-1-------------------------------------------------------------------
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
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;
}
/---------------代碼塊-1------完-------------------------------------------------------------
/---------------代碼塊-2-------------------------------------------------------------------
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
/---------------代碼塊-2------完-------------------------------------------------------------
/---------------代碼塊-3-------------------------------------------------------------------
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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();
}
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 && !mIgnoreNextUpEvent) {
// 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)) {
//調用了OnClickListener
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();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
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 |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
/---------------代碼塊-3------完-------------------------------------------------------------
return false;
}
onTouchEvent
方法裡的代碼也不少,不過大部分都是響應事件的一些邏輯,與事件分發流程關系不大。還是分成三塊,先看第一個代碼塊:
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//這裡CLICKABLE、CONTEXT_CLICKABLE和CONTEXT_CLICKABLE有一個滿足,clickable就為true
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//這裡先判斷目前View是否可用,如果是不可用進入if代碼塊
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//如果是UP事件并且View處于PRESSED狀态,則調用setPressed設定為false
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.
//這裡如果View是不可用狀态,就直接傳回clickable狀态,不做任何處理
return clickable;
}
代碼塊1中首先獲得View是否可點選
clickable
,然後判斷View如果是不可用狀态就直接傳回
clickable
,但是沒做任何響應。View預設的
clickable
為
false
,
Enabled
ture
,不同的View的
clickable
預設值也不同,
Button
預設
clickable
true
TextView
預設為
false
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
代碼塊2中會對一個
mTouchDelegate
觸摸代理進行判斷,不為空會調用代理的
onTouchEvent
響應事件并且傳回
true
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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();
}
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 && !mIgnoreNextUpEvent) {
// 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)) {
//調用了OnClickListener
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();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
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 |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
代碼塊3中首先判斷了
clickable || (viewFlags & TOOLTIP) == TOOLTIP
滿足了這個條件就傳回
true
消費事件。接下來的
switch
中主要對事件四種狀态分别做了處理。這裡稍微看下在
UP
事件中會調用一個
performClick
方法,方法中調用了
OnClickListener
onClick
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
最後看到
onTouchEvent
的最後一行預設傳回的還是
false
,就是說隻有滿足上述的條件之一才會傳回
ture
至此事件分發的相關源碼就梳理完了,我畫了幾張流程圖,能更清新的了解源碼邏輯。
ViewGroup的dispatchTouchEvent邏輯:
View的dispathTouchEvent邏輯:
事件分發整體邏輯
4、事件分發機制相關問題
閱讀了源碼之後,先來解決之前提到的三個問題。
Q1:為什麼日志Demo中隻有 ACTION_DOWN
事件有完整的從Activity到ViewGroup再到View的分發攔截和響應的運作日志,為什麼 ACTION_MOVE
和 ACTION_UP
事件沒有?
ACTION_DOWN
ACTION_MOVE
ACTION_UP
A1:日志Demo代碼所有事件傳遞方法都是預設調用
super
父類對應方法,是以根據源碼邏輯可知當事件序列中的第一個
DOWN
事件來臨時,會按照
Activity-->MyViewGroupA-->MyViewGroupB-->MyView
的順序分發,ViewGroup中
onInterceptTouchEvent
方法預設傳回
false
不會攔截事件,最終會找到合适的子View(這裡即MyView)
dispatchTransformedTouchEvent
方法,将事件交給子View的
dispatchTouchEvent
處理,在
dispatchTouchEvent
方法中預設會調用View的
onTouchEvent
方法處理事件,這裡因為MyView是繼承View的,是以預設
clickable
false
,而
onTouchEvent
方法中當
clickable
false
時預設傳回的也是
false
。最終導緻ViewGroup中
dispatchTransformedTouchEvent
方法傳回為
false
。進而導緻
mFirstTouchTarget
為空,是以後續
MOVE
UP
事件到來時,因為
mFirstTouchTarget
為空,事件攔截标記直接設定為
true
事件被攔截,就不會繼續向下分發,最終事件無人消費就傳回到Activity的
onTouchEvent
方法。是以就會出現這樣的日志輸出。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
//mFirstTouchTarget為空intercepted為true且不會調用onInterceptTouchEvent方法
intercepted = true;
}
Q2:為什麼将設定 clickable="true"
之後 ACTION_MOVE
ACTION_UP
事件就會執行了?
clickable="true"
ACTION_MOVE
ACTION_UP
A2:如A1中所說,
clickable
設定為
true
,View的
onTouchEvent
方法的傳回就會為
true
,消費了
DOWN
事件,就會建立一個
TouchTarget
插到單連結清單頭,
mFirstTouchTarget
就不會是空了,
MOVE
UP
事件到來時,就會由之前消費了
DOWN
事件的View來處理消費
MOVE
UP
事件。
Q3: requestDisallowInterceptTouchEvent
方法是怎樣通知父View不攔截事件,為什麼連 onInterceptTouchEvent
方法也不執行了?
requestDisallowInterceptTouchEvent
onInterceptTouchEvent
A3:源碼閱讀是有看到,
requestDisallowInterceptTouchEvent
方法時通過位運算設定标志位,在調用傳入參數為
true
後,事件在分發時
disallowIntercept
會為
true
!disallowIntercept
即為
false
,導緻事件攔截标記
intercepted
false
,不會進行事件攔截。
Q4: View.OnClickListener
onClick
方法與 View.OnTouchListener
onTouch
執行順序?
View.OnClickListener
onClick
View.OnTouchListener
onTouch
A4::
View.OnClickListener
onClick
方法是在View的
onTouchEvent
中
performClick
方法中調用的。 而
View.OnTouchListener
onTouch
方法在View的
dispatchTouchEvent
方法中看到是比
onTouchEvent
方法優先級高的,并且隻要
OnTouchListener.Touch
傳回為
true
,就隻會調用
OnTouchListener.onTouch
方法不會再調用
onTouchEvent
方法。是以
View.OnClickListener
onClick
方法順序是在
View.OnTouchListener
onTouch
之後的。
5、滑動沖突
關于滑動沖突,在
《Android開發藝術探索》中有詳細說明,我這裡把書上的方法結論與具體執行個體結合起來做一個總結。
1.滑動沖突的場景
常見的場景有三種:
- 外部滑動與内部滑動方向不一緻
- 外部滑動與内部滑動方向一緻
- 前兩種情況的嵌套
2.滑動沖突的處理規則
不同的場景有不同的處理規則,例如上面的場景一,規則一般就是當左右滑動時,外部View攔截事件,當上下滑動時要讓内部View攔截事件,這時候處理滑動沖突就可以根據滑動是水準滑動還是垂直滑動來判斷誰來攔截事件。場景而這種同個方向上的滑動沖突一般要根據業務邏輯來處理規則,什麼時候要外部View攔截,什麼時候要内部View攔截。場景三就更加複雜了,但是同樣是根據具體業務邏輯,來判斷具體的滑動規則。
推薦閱讀:
終于有人把 【移動開發】 從基礎到實戰的全套視訊弄全了3.滑動沖突的解決方法
- 外部攔截法
- 内部攔截法
外部攔截法是從父View着手,所有事件都要經過父View的分發和攔截,什麼時候父View需要事件,就将其攔截,不需要就不攔截,通過重寫父View的
onInterceptTouchEvent
方法來實作攔截規則。
private int mLastXIntercept;
private int mLastYIntercept;
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (滿足父容器的攔截要求) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
按照以上僞代碼,根據不同的攔截要求進行修改就可以解決滑動沖突。
内部攔截法的思想是父View不攔截事件,由子View來決定事件攔截,如果子View需要此事件就直接消耗掉,如果不需要就交給父View處理。這種方法需要配合
requestDisallowInterceptTouchEvent
方法來實作。
private int mLastX;
private int mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此類點選事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
//父View的onInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
這裡父View不攔截
ACTION_DOWN
方法的原因,根據之前的源碼閱讀可知如果
ACTION_DOWN
事件被攔截,之後的所有事件就都不會再傳遞下去了。
4.滑動沖突執行個體
執行個體一:ScrollView與ListView嵌套
這個執行個體是同向滑動沖突,先看布局檔案:
<?xml version="1.0" encoding="utf-8"?>
<cScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ScrollDemo1Activity">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.example.sy.eventdemo.MyView
android:layout_width="match_parent"
android:layout_height="350dp"
android:background="#27A3F3"
android:clickable="true" />
<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:background="#E5F327"
android:layout_height="300dp"></ListView>
<com.example.sy.eventdemo.MyView
android:layout_width="match_parent"
android:layout_height="500dp"
android:background="#0AEC2E"
android:clickable="true" />
</LinearLayout>
</cScrollView>
這裡MyView就是之前列印日志的View沒有做任何其他處理,用于占位使ScrollView超出一屏可以滑動。
運作效果:
可以看到ScrollView與ListView發生滑動沖突,ListView的滑動事件沒有觸發。接着來解決這個問題,用内部攔截法。
首先自定義ScrollView,重寫他的
onInterceptTouchEvent
方法,攔擊除了
DOWN
事件以外的事件。
public class MyScrollView extends ScrollView {
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onTouchEvent(ev);
return false;
}
return true;
}
}
這裡沒有攔截
DOWN
事件,是以
DOWN
事件無法進入ScrollView的
onTouchEvent
事件,又因為ScrollView的滾動需要在
onTouchEvent
方法中做一些準備,是以這裡手動調用一次。接着再自定義一個ListView,來決定事件攔截,重寫
dispatchTouchEvent
package com.example.sy.eventdemo;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;
/**
* Create by SY on 2019/4/22
*/
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private float lastY;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
getParent().getParent().requestDisallowInterceptTouchEvent(true);
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
if (lastY > ev.getY()) {
// 這裡判斷是向上滑動,而且不能再向上滑了,說明到頭了,就讓父View處理
if (!canScrollList(1)) {
getParent().getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
} else if (ev.getY() > lastY) {
// 這裡判斷是向下滑動,而且不能再向下滑了,說明到頭了,同樣讓父View處理
if (!canScrollList(-1)) {
getParent().getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
}
}
lastY = ev.getY();
return super.dispatchTouchEvent(ev);
}
}
判斷是向上滑動還是向下滑動,是否滑動到頭了,如果滑到頭了就讓父View攔截事件由父View處理,否則就由自己處理。将布局檔案中的空間更換。
<?xml version="1.0" encoding="utf-8"?>
<com.example.sy.eventdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ScrollDemo1Activity">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.example.sy.eventdemo.MyView
android:layout_width="match_parent"
android:layout_height="350dp"
android:background="#27A3F3"
android:clickable="true" />
<com.example.sy.eventdemo.MyListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:background="#E5F327"
android:layout_height="300dp"></com.example.sy.eventdemo.MyListView>
<com.example.sy.eventdemo.MyView
android:layout_width="match_parent"
android:layout_height="500dp"
android:background="#0AEC2E"
android:clickable="true" />
</LinearLayout>
</com.example.sy.eventdemo.MyScrollView>
運作結果:
執行個體二:ViewPager與ListView嵌套
這個例子是水準和垂直滑動沖突。使用V4包中的ViewPager與ListView嵌套并不會發生沖突,是因為在ViewPager中已經實作了關于滑動沖突的處理代碼,是以這裡自定義一個簡單的ViewPager來測試沖突。布局檔案裡就一個ViewPager:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
tools:context=".ScrollDemo2Activity">
<com.example.sy.eventdemo.MyViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>
</LinearLayout>
ViewPager的每個頁面的布局也很簡單就是一個ListView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
tools:context=".ScrollDemo2Activity">
<com.example.sy.eventdemo.MyViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>
</LinearLayout>
開始沒有處理滑動沖突的運作效果是這樣的:
看到現在隻能上下滑動響應ListView的滑動事件,接着我們外部攔截發解決滑動沖突,核心代碼如下:
case MotionEvent.ACTION_MOVE:
int gapX = x - lastX;
int gapY = y - lastY;
//當水準滑動距離大于垂直滑動距離,讓父view攔截事件
if (Math.abs(gapX) > Math.abs(gapY)) {
intercept = true;
} else {
//否則不攔截事件
intercept = false;
}
break;
onInterceptTouchEvent
中當水準滑動距離大于垂直滑動距離,讓父view攔截事件,反之父View不攔截事件,讓子View處理。
這下沖突就解決了。這兩個例子分别對應了上面的場景一和場景二,關于場景三的解決方法其實也是一樣,都是根據具體需求判斷事件需要由誰來響應消費,然後重寫對應方法将事件攔截或者取消攔截即可,這裡就不再具體舉例了。
6、總結
- Android事件分發順序:Activity-->ViewGroup-->View
- Android事件響應順序:View-->ViewGroup-->Activity
- 滑動沖突解決,關鍵在于找到攔截規則,根據操作習慣或者業務邏輯确定攔截規則,根據規則重新對應攔截方法即可。
Android進階架構腦圖詳細位址
Android進階進階視訊教程