Android 事件分發和 View 的滑動沖突

Android 事件分發和滑動沖突都是開發中經常遇到的難點問題,遇到問題時可能會通過 Google 或者 StackOverflow 按照別人的經驗解決了問題,但每次遇到這種問題都去 Google 也是非常不合適的事情。本篇文章將從 Android 事件分發的源碼入手,首先分析源碼,當我們了解了源碼,不但能從源碼中總結到常見問題的解決方式,并且遇到更加深入問題時也能冷靜的從源碼入手來解決問題,做到知其然更知其所以然。滑動沖突問題究其根本其實就是事件分發問題,了解了事件分發,我們也就能從一定的高度來解決滑動沖突問題,并總結出解決滑動沖突問題的模式。

一、Android UI 界面架構

要了解 Android 事件分發,我們要先來了解一下 Android 的 UI 界面架構,因為事件的分發流程是以 Android 界面架構為基礎的,以一張圖來介紹
Android UI 界面框架

如圖所示,每一個 Activity 都包含一個 Window,Android 中 Window 的實現類是 PhoneWindow;PhoneWindow 中包含一個 DecorView,也就是一個界面布局的根 View,一般是一個 FrameLayout;DecorView 中有一個 ContentView 實際上是一個 ViewGroup,看名字很熟悉,其實這個 ViewGroup 就是 Activity 中我們要顯示布局的 View 對象的父容器,一般是一個 FrameLayout,在 Activity 的 onCreate() 中通過 setContentView 方法將要顯示的布局的 View 對象放入該 ContentView;ContentView 中的 ViewGroup 就是我們界面要顯示的布局 ViewGroup;View 則是界面中每一個需要顯示的 View 控件,這就是 Android UI 界面框架的簡單模型。

二、Android 事件分發

接著說事件分發,一個觸摸事件的產生是由屏幕、Native 層、Framework 層產生的,產生之后會通過 Framework 層傳遞到 Activity 中,剛才提到了事件分發過程是以 Android 界面架構為基礎的,怎么理解呢,就是說事件的分發流程是以 Activity 開始,經過 PhoneWindow、DecorView、ViewGroup、View,整個過程正好與界面架構的層級匹配。

由于 DecorView 也是 GroupView,為了簡單,在分析整個事件分發流程時我們可以把 DecorView、ContentView、GroupView 合一,直接分析 Activity、PhoneWindow、ViewGroup、View 四個層級

注意:每個事件都會經歷以下所說的所有流程,并且一個事件執行結束后才開始執行下一事件,例如一次點擊:ACTION_DOWN ACTION_MOVE ACTION_UP,這里就會產生三個事件,這三個事件屬于同一個事件系列,三個事件都會經歷以下所說的所有流程

事件在 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法開始,來看一下該方法

// Acitivty
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction(); // 是個空方法
    }
    if (getWindow().superDispatchTouchEvent(ev)) { // 將事件傳遞到 PhoneWindow
        return true;
    }
    return onTouchEvent(ev); // 如果 Window 沒有處理則 Activity 自己處理
}

// Activity 的處理邏輯是默認關閉事件,如果需要 Activity 處理,則需要開發者重寫該方法
public boolean onTouchEvent(MotionEvent event) {

    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

// PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event); // mDecor 即為 DecorView
}

先稍微梳理一下,Activity 的 dispatchTouchEvent 方法中會調用 PhoneWindow 中的 superDispatchTouchEvent 方法,如果該方法返回 true 那么事件處理結束,如果該方法返回 false ,那么調用 Activity 的 onTouchEvent 方法 Activity 自己來處理事件。

PhoneWindow 中的 superDispatchTouchEvent 方法中直接調用 mDecor.superDispatchTouchEvent(event) ,這里的 mDecor 就是界面框架中的 DecorView,DecorView 的 superDispatchTouchEvent 方法直接調用 ViewGroup 的 dispatchTouchEvent 方法,這里就講事件傳遞到了 ViewGroup 中,下面的內容就是本篇文章的重中之重,現在開始吧。

1. ViewGroup 中的事件分發和處理

這里先來稍微介紹一下事件如何在 ViewGroup 中分發,然后在根據源碼來理解即可,點擊事件到達頂級 View(一般是 ViewGroup) 后,會調用 ViewGroup 的 dispatchTouchEvent 來進行事件分發,邏輯如下:

  • 簡單來說,ViewGroup 的 dispatchTouchEvent 方法首先根據 onInterceptTouchEvent 方法以及一些其他條件來判斷是否攔截該事件,如果 ViewGroup 攔截該事件,那么調用 ViewGroup 的 onTouchEvent 來處理事件,并且不會將事件傳遞到子 View,如果不攔截則將事件傳遞到子 View 來處理。不管是 ViewGroup 自己處理該事件還是傳遞到子 View 處理,dispatchTouchEvent 都會接收事件的處理結果,并將事件的處理結果返回到上一層

  • ViewGroup.dispatchTouchEvent() 完成事件分發,并接收事件處理結果,最后將事件的處理結果返回到上一層

  • ViewGroup.onInterceptTouchEvent() 方法判斷當前 ViewGroup 是否攔截此事件

  • View.onTouchEvent() 事件處理,并將事件的處理結果返回

好啦,下面就從源碼開始來一點點分析,由于 ViewGroup 的 dispatchTouchEvent 方法比較長,所以下面會一段一段來分析



// ViewGroup.dispatchTouchEvent
{
    // Handle an initial down. ACTION_DOWN 事件時將 FLAG_DISALLOW_INTERCEPT 標記重置為關閉
    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();
    }
    
    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;
    }
}

// ViewGroup
public boolean onInterceptHoverEvent(MotionEvent event) {
    return false;
}

上面這段代碼的作用就是確定 ViewGroup 是否需要攔截事件

先來解釋一下里面的對象,mFirstTouchTarget 默認為 null,在有子 View 處理了 ACTION_DOWN 事件時會賦值,一個事件系列中第一個事件到來時 mFirstTouchTarget 肯定為 null

FLAG_DISALLOW_INTERCEPT,一個標記,通過子 View 中調用 getParent().requestDisallowInterceptTouchEvent() 方法修改此標記,如果開啟此標記,表示 ViewGroup 不會攔截事件,該標記在 ACTION_DOWN 事件到來時會重置此標記為關閉狀態。

由代碼來分析,一個事件系列中,第一次來的總是 ACTION_DOWN,此時 mFirstTouchTarget 也為 null,且 FLAG_DISALLOW_INTERCEPT 為關閉,此時會調用 onInterceptTouchEvent(ev) 方法來判斷是否攔截, ViewGroup 默認不攔截任何事件。

如果事件不是 ACTION_DOWN ,且 mFirstTouchTarget 為 null,說明在 ACTION_DOWN 事件時沒有子 View 處理了事件或者是 ViewGroup 攔截了 ACTION_DOWN 事件,那么同一系列事件都不會再傳遞到子 View,那么 ViewGroup 直接攔截事件;此時會調用 dispatchTransformedTouchEvent 方法,該方法中會調用 super.dispatchTouchEvent(event) ,該方法為父類的 dispatchTouchEvent ,其中會調用處理事件的方法,下面會分析源碼。最后將處理結果返回上一層。

如果 mFirstTouchTarget 則說明有處理了 ACTION_DOWN 事件的 View ,則會繼續通過 onInterceptTouchEvent 來判斷是否需要攔截事件,在判斷是還會受 FLAG_DISALLOW_INTERCEPT 的影響,如果 FLAG_DISALLOW_INTERCEPT 開啟,那么只要 mFirstTouchTarget 有值,ViewGroup 都不會攔截事件,如果 mFirstTouchTarget 關閉則根據 onInterceptTouchEvent 方法的返回值來決定是否攔截。

上面確定了是否需要攔截事件,接著看 ViewGroup 的 dispatchTouchEvent 方法的源碼中在攔截和非攔截情況下事件是怎么處理的

// ViewGroup.dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
    
    // 判斷是否攔截
    ... 
    
    // If intercepted, start normal event dispatch. Also if there is already
    // a view that is handling the gesture, do normal event dispatch.
    if (intercepted || mFirstTouchTarget != null) { 
        ev.setTargetAccessibilityFocus(false);
    }

    if (!canceled && !intercepted) { // 如果不攔截
        if (actionMasked == MotionEvent.ACTION_DOWN...) { // ACTION_DOWN 時該判斷為真,會執行其中的方法
            final int actionIndex = ev.getActionIndex(); // always 0 for down
            ...
            for (int i = childrenCount - 1; i >= 0; i--) { // 遍歷所有子 View 
                final int childIndex = customOrder
                        ? getChildDrawingOrder(childrenCount, i) : i;
                final View child = (preorderedList == null)
                        ? children[childIndex] : preorderedList.get(childIndex);
    
                // If there is a view that has accessibility focus we want it
                // to get the event first and if not handled we will perform a
                // normal dispatch. We may do a double iteration but this is
                // safer given the timeframe.
                if (childWithAccessibilityFocus != null) {
                    if (childWithAccessibilityFocus != child) {
                        continue;
                    }
                    childWithAccessibilityFocus = null;
                    i = childrenCount - 1;
                }
    
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
    
                newTouchTarget = getTouchTarget(child);
                if (newTouchTarget != null) {
                    // Child is already receiving touch within its bounds.
                    // Give it the new pointer in addition to the ones it is handling.
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                    break;
                }
    
                resetCancelNextUpFlag(child);
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 調用子 View 來處理事件
                    // Child wants to receive touch within its bounds.
                    mLastTouchDownTime = ev.getDownTime();
                    if (preorderedList != null) {
                        // childIndex points into presorted list, find original index
                        for (int j = 0; j < childrenCount; j++) {
                            if (children[childIndex] == mChildren[j]) {
                                mLastTouchDownIndex = j;
                                break;
                            }
                        }
                    } else {
                        mLastTouchDownIndex = childIndex;
                    }
                    mLastTouchDownX = ev.getX();
                    mLastTouchDownY = ev.getY();
                    newTouchTarget = addTouchTarget(child, idBitsToAssign); // 子 View 處理了事件,則將 mFirstTouchTarget 賦值并終止遍歷子 View
                    alreadyDispatchedToNewTouchTarget = true; // 將事件處理結果置為 true 表示已經有子 View 處理了事件
                    break; // 跳出循環
                }
    
                // The accessibility focus didn't handle the event, so clear
                // the flag and do a normal dispatch to all children.
                ev.setTargetAccessibilityFocus(false);
            }
            ...
        }
        
        if (mFirstTouchTarget == null) { // 如果 mFirstTouchTarget 為 null,說明是 ACTION_DOWN 事件且沒有子 View 處理事件,直接調用 ViewGroup 的 dispatchTransformedTouchEvent ,并且其中調用父類的 dispatchTouchEvent 方法處理事件并將該方法返回值賦值到處理結果
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        }else {
            
            // 執行到這里說明 ViewGroup 攔截了事件或者是 mFirstTouchTarget 不為 null
            
            // 如 ViewGroup 攔截事件就調用父類的 dispatchTouchEvent 方法處理事件并將該方法返回值賦值到處理結果 
            
            // 如果 ViewGroup 不攔截事件,mFirstTouchTarget 有值,通過 dispatchTransformedTouchEvent 方法調用調用 target.child 處理事件,并將該方法返回值賦值到處理結果
            
            // 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) { // 如果已經有子 View 處理了事件,則將 true 賦值處理結果
                    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;
            }
        }
    }
    
    // ViewGroup 有子 View 處理了 ACTION_DOWN 事件時為 mFirstTouchTarget 賦值
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target; // mFirstTouchTarget 賦值
        return target;
    }
    
    // ViewGroup
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        
        ...
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);  // 子 View 為空,調用父類 dispatchTouchEvent 方法處理事件
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            // 子 View 不為空,調用子 View 的 dispatchTouchEvent 方法
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }
}    

這段代碼也很簡單,主要就是根據上面 ViewGroup 是否攔截此事件以及此事件的事件類型來處理事件,下面就按照源碼的流程來分析。

  1. 如果 ViewGroup 不攔截事件且事件為 ACTION_DOWN 時遍歷子 View,尋找出符合條件的子 View 來處理事件,尋找的條件也很簡單,主要就是觸摸事件是否落到該 View 所在區域與該 View 是否在播動畫 ,如果找到符合條件的子 View,就會調用 dispatchTransformedTouchEvent ,在該方法中調用子 View 的 dispatchTouchEvent 方法,將事件傳遞到子 View 中,如果子 Viwe 處理了該事件則為 mFirstTouchTarget 賦將子 View 是否處理了事件的標記 alreadyDispatchedToNewTouchTarget 置為 true,然后跳出循環。如果循環結束都沒有子 Viwe 處理事件則什么都不做。

  2. 接著判斷如果 mFirstTouchTarget 為 null,這里會調用 ViewGroup 的 dispatchTransformedTouchEvent 方法,其中會調用父類的 dispatchTouchEvent 方法處理事件并將處理結果返回。有兩種情況下 mFirstTouchTarget 為 null,第一種情況是如果事件是 ACTION_DOWN 并且遍歷所有子 View 后沒有子 View 處理事件從而導致 mFirstTouchTarget 為null;第二種情況是 ViewGroup 從 ACTION_DOWN 時就開始攔截事件所以沒有遍歷所有子 View 從而導致任何事件到來時執行到這里 mFirstTouchTarget 都為 null。

  3. 第 2 點可以說明如果 ViewGroup 在 ACTION_DOWN 事件時攔截,那么 ACTION_DOWN 事件 ViewGroup 會處理,并且同一事件系列中其他事件時,不管 ViewGroup 是否攔截這里都會調用 ViewGroup 的方法來處理事件。還可以說明 ACTION_DOWN 時如果 ViewGroup 不攔截但是所有子 View 沒有處理事件這時 ViewGroup 會處理事件。這也是唯一一種將事件傳遞到子 View 后子 View 沒處理但是 ViewGroup 會處理的情況

  4. 接著判斷 mFirstTouchTarget 不為 null 時,會先判斷子 View 已經處理了事件的標識是否為 true。因為只有 ACTION_DOWN 事件時且有子 View 處理了事件時才會在前面為該標記賦值為 true。如果為 true,說明這是 ACTION_DOWN 事件且 ViewGroup 不攔截并且遍歷子 View 處理事件時有子 View 處理了事件,則將事件處理結果賦值 true 。如果該標記為 false,說明不是 ACTION_DOWN 事件。這時會根據前面部分的 ViewGroup 是否攔截此事件來判斷,如果 ViewGroup 攔截則調用 ViewGroup 的方法處理事件并將處理結果返回,如果 ViewGroup 不攔截此事件,則由 mFirstTouchTarget 標記的 View 來處理事件,并將結果返回。

  5. 第 4 點可以看出,如果 ACTION_DOWN 事件被子 View 處理即 mFirstTouchTarget 不為 null 時,如果當前事件不是 ACTION_DOWN 且 ViewGroup 不攔截此事件,則會將事件傳遞到子 View 處理,然后不管子 View 是否處理了該事件, ViewGroup 都不會再處理,只會將處理結果返回到上一層,第 5 點與第 3 點是對比分析的。因為第 3 點中提到的情況是唯一一種將事件傳遞到子 View 后子 View 沒有處理了事件但 ViewGroup 會處理的情況。

分析到這里,ViewGroup 的事件分發和攔截過程就基本結束了,所有問題都指向了一個方法 View.dispatchTouchEvent() ,不管是 ViewGroup 處理事件還是子 View 處理事件都會執行該方法,我們接下來就分析這個方法干了什么。

2. Viwe 的 dispatchTouchEvent() 方法

首先來看源碼

// View
public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        // ListenerInfo是View的靜態內部類,用來定義一堆關于View的XXXListener等方法
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) { // 如果為 View 設置了 OnTouchListener 會首先調用 OnTouchListener.onTouch 方法
            result = true;
        }

        if (!result && onTouchEvent(event)) { // 如果 OnTouchListener.onTouch  方法返回 false ,則執行 onTouchEvent 方法
            result = true;
        }
    }

    ...
    return result;
}

// View
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    if ((viewFlags & ENABLED_MASK) == DISABLED) { // 如果 View 是 DISABLED 的,則直接返回該 View 是否可點擊 CLICKABLE
        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);
    }
    
    // 如果設置了代理,類似 OnTouchListener ,則會調用代理的 onTouchEvent 方法,如果該方法返回 true ,則直接返回處理結果 true
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { // 只要 View 時候 CLICKABLE 或者 LONG_CLICKABLE 都會判斷為 true
        switch (action) {
            case MotionEvent.ACTION_UP:
                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) { // PerformClick 為一個 Runnable 
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) { // 調用 performClick 方法,通過 post 方法和 Runnable 保證 performClick 執行在 UI 線程
                                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:
                mHasPerformedLongPress = false;

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                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);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }

        return true;
    }

    return false;
}

// View
public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo; // onClickListenenr
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

以上呢就是 View 的 dispatchTouchEvent 方法的源碼,該方法會先判斷是否設置了 OnTouchListener ,如果設置了則會調用 OnTouchListener 的 onTouch 方法,如果 onTouch 方法返回 true 則不會調用 onTouchEvent 方法,如果 onTouch 返回 false 或者沒有設置 OnTouchListener 則會調用 onTouchEvent。這里看出 OnTouchListener.onTouch 方法調用的時機在 View 的 dispatchTouchEvent 方法之前,且 OnTouchListener.onTouch 的優先級是高于 onTouchEvent 方法的(也高于 onClickListener.onClick() 方法,接下來下面會分析)。

View 的 onTouchEvent() 方法中,根據 View 的是否可用即 Enable 或者 Disable 來判斷,如果是 Disable ,那么之接返回該 View 是否可點擊 CLICKABLE 或者支持 LONG_CLICKABLE,說明 Disable 不影響 View 是否消耗事件。

接下來,如果 View 設置了代理,類似 OnTouchListener ,則會調用代理的 onTouchEvent 方法,如果該方法返回 true ,則直接返回處理結果 true.

如果 View 是 Enable 的,且為 CLICKABLE 或者 LONG_CLICKABLE 或者 CONTEXT_CLICKABLE 的,則會真正調用 View 處理事件的方法,我們關注 ACTION_UP 事件,該事件中會通過 post 和 Runnable 來調用 performClick 方法,保證該方法執行在 UI 線程,performClick 中,如果 View 設置了 onClickListenenr 會調用 OnClickListenenr.onClick() 方法,該方法沒有返回值,最后 onTouchEvent 的執行結果。從這里可以得出 OnClickListener.onClick() 方法的執行時機在 onTouchEvent 方法中且該事件為 ACITON_UP,由此可以得出 ouTouchEvent 的優先級高于 OnClickListenenr.onClick() 方法

OnTouchListener.onTouch() > onTouchEvent() > OnClickListener.onClick()

這里還有一點,就是有關 View 是否是可點擊的,默認情況下所有 View 的 LONG_CLICKABLE 都為 false,而 CLICKABLE 屬性則和具體 View 有關,確切的說是如果 View 是可點擊的那么 CLICKABLE 默認是 true, 例如:Button。如果 View 是不可點擊的,那么 CLICKABLE 默認為 false,例如:TextView。并且 View 的 setOnClickListener 和 setOnLongClickListener 方法都會將對應屬性設置為 true。

到這里事件的基本內容就講完了,從整個流程可以看出,事件的分發過程是隧道式的也就是事件是從最外層的 Activity 一層一層傳遞到 View 中,而事件的處理則是冒泡式的,是從 View 一層一層傳遞到 Activity

下面將展示根據事件分發的源碼總結一些常見但是疑難的結論,并附加 AndroidStudio 的 Log,如果感興趣的可以自己敲一遍試試看。這些結論并不是全部的,所有人都可以在開發過程中根據源碼總結出自己的結論

3. 事件分發源碼歸納總結

3.1. 如果 ViewGroup 攔截某事件,則不管是什么事件,都會調用 ViewGroup 的處理事件的方法來處理事件

3.2. View 開始處理事件時,如果不消耗 ACTION_DOWN 即 dispatchTouchEvent() 返回 false ,事件會返回給父 View 處理,并且同一系列事件都不會交給它處理

  • ViewGroup 和 View 對 ACTION_DOWN 不處理,則 ACTION_MOVE 和 ACTION_UP 均不會傳遞給 ViewGroup 和 View

  • 如果是 ViewGroup 消耗,則不會傳遞給 View

  • 從源碼分析是因為,系列中其他事件到來時,ViewGroup 的 dispatchTouchEvent() 中 mFirstTouchTarget 為 null,默認攔截自己處理

    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
    11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 onTouchEvent
    11-29 09:15:03.720 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:15:03.720 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 onTouchEvent

3.3. ViewGroup 如果 攔截并消耗 了 ACTION_DOWN 事件,那么同一事件系列中其他事件會直接交給該 ViewGroup 處理,不會再調用該 ViewGroup 的 onInterceptTouchEvent() 方法

  • 源碼分析:系列中其他事件到來時,ViewGroup 的 dispatchTouchEvent() 中 mFirstTouchTarget 為 null,默認攔截自己處理
    11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
    11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent

3.4. 如果是子 View 消耗了事件,除非 View 調用了父 View 的 requestDisallowInterceptTouchEvent 方法設置不讓父 View 攔截,否則同一系列中其他事件來臨時在其父 ViewGroup 中還是會判斷是否攔截

  • 源碼分析:即使 mFirstTouchTarget 不為 null,在事件到來時 ViewGroup 的 dispatchTouchEvent 方法中還是會調用 onInterceptTouchEvent 方法來判斷是否攔截

  • 如果 ViewGroup 的 FLAG_DISALLOW_INTERCEPT 標記開啟,則不會攔截事件

    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent

    -------------------我是分割線----------------
    
    11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent

3.5. 當 View 消耗了 ACTION_DOWN ,如果其父 Viwe 不攔截事件,那么同系列中的其他事件還會傳遞到 View ,這是即使 View 不消耗事件系列中其他事件,其父 View 的 onTouchEvent() 事件也不會被調用,事件會傳遞到 Activity 的 onTouchEvent() 方法處理

  • 因為在 View 消耗了 ACTION_DOWN 時, mFirstTouchTarget 被賦值,ViewGroup 不攔截情況下,同一系列中其他事件到來時還是會傳遞到該 View

  • 在 GroupView 不攔截事件時只有事件為 ACTION_DOWN 且所有子 View 都沒有處理了事件時才調用其父類也就是本身 View 的 dispatchTouchEvent() 方法來處理事件

  • Activity 中只要 DecorView 的 dispatchTouchEvent() 方法返回 false 就會調用自己的 onTouchEvent() 方法處理事件。

    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
    11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent

3.6. 除 ACTION_DOWN 以外,子 View 可以通過設置 FLAG_DISALLOW_INTERCEPT 標記位來影響 GroupView 是否攔截事件

  • 前提是 ACTION_DOWN 時 ViewGroup 沒有攔截,并且 View 消耗了 ACTION_DOWN 時的事件 mFirstTouchTarget 被賦值。該
    不為 null 時,在 ViewGroup 的 dispatchTouchEvent() 中會根據該標記位判斷是否需要調用 onInterceptTouchEvent() 方法,如果該標記位為 true ,則不會調用 onInterceptTouchEvent() 方法,即不會攔截
    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 {
        intercepted = true;
    }

整個事件分發的流程還是比較清晰的,只有真正了解了源碼,再遇到事件分發問題時才能得心應手,下面就開始本篇文章的第二個重點,滑動沖突問題的解決

二、View 的滑動沖突

在應用中加了滑動效果后,簡單的滑動效果是不會有什么大問題的。不過要是添加復雜的滑動效果,或者滑動是嵌套的情況時,滑動沖突問題就出現了。這里會先說一下滑動沖突的類型,再根據事件分發的原理找到統一的滑動沖突問題的解決方式

1. 滑動沖突的種類

  1. 外部跟內部滑動的方向不一致,例如 ViewPager 嵌套 ListView 的情況,ViwePager 是左右滑動,ListView 是上下滑動,這樣在有滑動事件時便會出現滑動沖突。當然 ViewPager 默認幫我們解決了滑動沖突

  2. 外部跟內部滑動方向一致,例如 ScrollView 嵌套 ListView 的情況,ScrollView 可以上下滑動,ListView 也可以上下滑動,這時候如果有滑動事件系統將不知道用戶到底想滑動哪一層,會出現第二種滑動沖突

  3. 上面兩種情況的嵌套,例如 QQ 的側滑菜單,主頁,聯系人列表三個 View,側滑菜單跟首頁的 ViewPager 都可以左右滑動,聯系人列表 ListView 和 ViewPager 也會產生滑動沖突

下面來看滑動沖突的處理規則

2. 滑動沖突的處理規則

對于上面提到的第一種滑動沖突,它的處理規則比較簡單,當用戶左右滑動時讓外部的 View 攔截事件,當用戶上下滑動時讓內部的 View 攔截事件。也就是根據滑動的特征來解決滑動沖突。至于如何判斷用戶是左右滑動還是上下滑動,我們可以根據用戶滑動過程中左右偏移量和上下偏移量的對比來確定,哪個方向的偏移量大判定為哪個方向的滑動。除了通過偏移量對比,還可以使用速度、滑動方向跟水平方向的夾角等來確定。確定了是哪個方向滑動就能決定讓相應 View 來響應滑動事件

對于第二種和第三種沖突,我們不能通過速度、偏移量、夾角等來判斷,但是一般可以在業務上找到突破點,比如業務上規定當處于某種狀態時內部相應,當處于另一種狀態時外部相應,這樣就根據業務確定了相應的處理規則。有了相應處理規則就可以決定讓相應的 View 來響應滑動事件

3. 滑動沖突的解決方式

上面提到了三種滑動沖突場景,并且根據每種場景都提出了相應的處理原則,當處理原則確定之后我們就可以找到一種不依賴具體滑動規則的通用解決辦法,并且在每種沖突場景時修改有關滑動規則的處理邏輯即可。

解決方式主要有外部攔截法內部攔截法 兩種,下面一一來介紹

外部攔截法

外部攔截法是指所有的事件都需要經過外部 ViewGroup 的判斷,如果外部 ViewGroup 需要此事件就攔截,如果外部 ViewGroup 不需要此事件就不攔截,外部攔截法需要重寫外部 ViewGroup 的 onInterceptTouchEvent 方法,在內部根據相應規則確定是否攔截即可。

    // MyViewGroup
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                return false;
            case MotionEvent.ACTION_MOVE:
                return intercept();
                break;
            case MotionEvent.ACTION_UP:
                return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

以上代碼即為外部攔截法的模板代碼,在 ACTION_DOWN 時外部 ViewGroup 的 onInterceptTouchEvent 方法必須返回 false,否則內部 View 不能接收到事件,這里注意,內部 View 處理 ACTION_DOWN 事件必須返回 true,否則將接收不到之后的事件,ACTION_DOWN 之后的事件 ViewGroup 都會進行是否攔截的判斷,intercept() 方法用來判斷是否需要攔截,如果根據處理規則判定為需要攔截就返回 true 然后外部 ViewGroup 來處理事件;如果處理規則判定為不需要攔截就返回 false,讓內部 View 來處理事件

內部攔截法

內部攔截法是指所有的事件外部 ViewGroup 都不攔截,所有事件都傳遞給內部 View,內部 View 如果需要此事件就直接處理,否則就通過 requestDisallowInterceptTouchEvent 方法來讓外部 ViewGoup 攔截事件,內部攔截法較外部攔截發稍微復雜一點

    // MyViewGroup
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN)
            return false;
        else return true;
    }


    // MyView
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;
            case MotionEvent.ACTION_MOVE:
                if (!intercept())
                    getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }

以上代碼即為內部攔截法的模板代碼,其中外部 ViewGroup 的 onInterceptTouchEvent 方法中除了 ACTION_DOWN 需要返回 false,其他的事件都必須返回 true,這樣才能再內部 View 中通過 requestDisallowInterceptTouchEvent 方法來控制外部 ViewGoup 攔截事件

內部 View 中的 onInterceptTouchEvent 方法中 ACTION_DOWN 事件必須返回 true,且需要調用 requestDisallowInterceptTouchEvent 方法設置外部 ViewGoup 不攔截其他事件,當內部 View 不需要其他事件時再次調用 requestDisallowInterceptTouchEvent 方法設置外部 ViewGroup 攔截事件。

注意:一旦內部 View 設置外部 ViewGroup 攔截事件,那么同一事件序列中之后的事件都不會再到達內部 View

以上就是外部攔截法和內部攔截法的大體結構,其中外部攔截法比較簡單,實現的功能也比較全,內部攔截法有一定的缺點,所以在使用時最好選擇外部攔截法。

到這里 Android 事件分發和 View 的滑動沖突的內容就結束啦,看起來很簡單的流程,居然寫了整整五個小時又改了三個小時。盡量表達的清晰,也盡量將整個事件分發過程描述清楚。希望可以幫到大家。如果有問題可以留言我們來一起討論。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容