天天看點

Android進階知識:事件分發與滑動沖突(二)

接着看代碼塊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

事件,那麼其他

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

事件沒有?

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

事件就會執行了?

A2:如A1中所說,

clickable

設定為

true

,View的

onTouchEvent

方法的傳回就會為

true

,消費了

DOWN

事件,就會建立一個

TouchTarget

插到單連結清單頭,

mFirstTouchTarget

就不會是空了,

MOVE

UP

事件到來時,就會由之前消費了

DOWN

事件的View來處理消費

MOVE

UP

事件。

Q3:

requestDisallowInterceptTouchEvent

方法是怎樣通知父View不攔截事件,為什麼連

onInterceptTouchEvent

方法也不執行了?

A3:源碼閱讀是有看到,

requestDisallowInterceptTouchEvent

方法時通過位運算設定标志位,在調用傳入參數為

true

後,事件在分發時

disallowIntercept

會為

true

!disallowIntercept

即為

false

,導緻事件攔截标記

intercepted

false

,不會進行事件攔截。

Q4:

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進階進階視訊教程

繼續閱讀