接著看代碼塊3,在這段很長的代碼里,首先一個if中判斷了該事件是否滿足沒有被攔截和被取消,之後第二個if判斷了事件類型是否為DOWN,滿足了沒有被攔截和取消的DOWN事件,接下來ViewGroup才會循環其子View找到點擊事件在其內部並且能夠接受該事件的子View,再通過調用dispatchTransformedTouchEvent方法將事件分發給該子View處理,返回true說明子View成功消費事件,於是調用addTouchTarget方法,方法中通過TouchTarget.obtain方法獲得一個包含這View的TouchTarget節點並將其添加到鏈表頭,並將已經分發的標記設置為true

接下來看代碼塊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事件,那麼其他MOVEUP等類型事件怎麼處理呢?還有如果遍歷完子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又或者是MOVEUP類型事件等再判斷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可用的,就會調用mOnTouchListeneronTouch方法,如果onTouch方法返回true說明事件已經被消費了,就將result標記修改為true,這樣他就不會走接下來的if了。如果沒有設置mOnTouchListener或者onTouch方法返回false,則會繼續調用onTouchEvent方法。這裡可以發現mOnTouchListeneronTouch方法的優先順序是在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 doesnt 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 dont 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 were 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 doesnt respond to them.
//這裡如果View是不可用狀態,就直接返回clickable狀態,不做任何處理
return clickable;
}

代碼塊1中首先獲得View是否可點擊clickable,然後判斷View如果是不可用狀態就直接返回clickable,但是沒做任何響應。View默認的clickablefalseEnabledture,不同的View的clickable默認值也不同,Button默認clickabletrueTextView默認為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 dont 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 were 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方法,方法中調用了OnClickListeneronClick方法。

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_MOVEACTION_UP事件沒有?

A1:日誌Demo代碼所有事件傳遞方法都是默認調用super父類對應方法,所以根據源碼邏輯可知當事件序列中的第一個DOWN事件來臨時,會按照Activity-->MyViewGroupA-->MyViewGroupB-->MyView的順序分發,ViewGroup中onInterceptTouchEvent方法默認返回false不會攔截事件,最終會找到合適的子View(這裡即MyView)dispatchTransformedTouchEvent方法,將事件交給子View的dispatchTouchEvent處理,在dispatchTouchEvent方法中默認會調用View的onTouchEvent方法處理事件,這裡因為MyView是繼承View的,所以默認clickablefalse,而onTouchEvent方法中當clickablefalse時默認返回的也是false。最終導致ViewGroup中dispatchTransformedTouchEvent方法返回為false。進而導致mFirstTouchTarget為空,所以後續MOVEUP事件到來時,因為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_MOVEACTION_UP事件就會執行了?

A2:如A1中所說,clickable設置為true,View的onTouchEvent方法的返回就會為true,消費了DOWN事件,就會創建一個TouchTarget插到單鏈表頭,mFirstTouchTarget就不會是空了,MOVEUP事件到來時,就會由之前消費了DOWN事件的View來處理消費MOVEUP事件。

Q3:requestDisallowInterceptTouchEvent方法是怎樣通知父View不攔截事件,為什麼連onInterceptTouchEvent方法也不執行了?

A3:源碼閱讀是有看到,requestDisallowInterceptTouchEvent方法時通過位運算設置標誌位,在調用傳入參數為true後,事件在分發時disallowIntercept會為true!disallowIntercept即為false,導致事件攔截標記interceptedfalse,不會進行事件攔截。

Q4:View.OnClickListeneronClick方法與View.OnTouchListeneronTouch執行順序?

A4::View.OnClickListeneronClick方法是在View的onTouchEventperformClick方法中調用的。 而View.OnTouchListeneronTouch方法在View的dispatchTouchEvent方法中看到是比onTouchEvent方法優先順序高的,並且只要OnTouchListener.Touch返回為true,就只會調用OnTouchListener.onTouch方法不會再調用onTouchEvent方法。所以View.OnClickListeneronClick方法順序是在View.OnTouchListeneronTouch之後的。

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高級架構腦圖詳細地址

關於FAndroid進階知識的全部學習內容,我們這邊都有系統的知識體系以及進階視頻資料,有需要的朋友可以加群免費領取安卓進階視頻教程,源碼,面試資料,群內有大牛一起交流討論技術;點擊鏈接加入群聊【騰訊@Android高級架構】(包括自定義控制項、NDK、架構設計、混合式開發工程師(React native,Weex)、性能優化、完整商業項目開發等)

Android高級進階視頻教程

推薦閱讀:

相关文章