Android原始碼角度分析事件分發消費(徹底整明白Android事件)

Drummor發表於2019-02-26

引言

Android 事件分發網上有很多資料,大部分都是在dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent()三個方法中列印Log日誌,草草的得出,各個方法中返回true/false會呼叫哪個方法,結論良莠不齊。沒有從根本上理解這一塊的實現機制,實際運用的時候 還是一陣懵逼。從原始碼分析,吃透其中的程式碼邏輯才能靈活運用解決實際問題,廢話就講這麼多,進入主題。

View中的事件分發

系統預設情況下,View作為事件分發消費的終點,我們就先看下原始碼裡view在接受到事件時候怎麼處理的,view中跟事件分發相關的主要是兩個方法,一個是dispatchTouchEvent(),一個是onTouch(),先看View中dispatchTouchEvent()怎麼處理的;

dispatchTouchEvent()、onTouchEvent()返回值作用
  • 1、public boolean dispatchTouchEvent():預設情況下,這個方法就是把事件分發給目標view,而且這個目標view可能是自己;如果返回true就表示找到了要消費事件的目標view,而且事件被消費了 如果返回false就表示沒有找到
  • 2、onTouchEvent():這個方法是處理消費觸控事件的 如果返回true表示這個事件被消費了 如果返回false 表示沒有被消費
  • 3、注意 手指觸碰螢幕一個完整的觸碰動作包含最重要的三個事件:按下、滑動和抬起。View(ViewGroup)在做事件分發的時候,最開始接受到的是按下,當按下(ACTION_DOWN)如果沒有分發成功也就是沒有找到要消費的這個事件的view時,把整個觸碰事件的ACTION_DOW以後的ACTION_MOVE和ACTION_UP就不會做分發了

事不宜遲上程式碼,關鍵的地方加了我騷氣的中文註釋

View中的dispatchTouchEvent()方法

public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
//Flag1:如果設定了OnTouchListener監聽且onTouchListener監聽中的onTouch()方法為true把result設定為true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
//Flag2: 如果上一步判斷的結果為false,則進入執行view自身的onTouchEvent(MotionEvent e)方法,
        將result設定為true 如果onTouchEvent(e)方法返回true
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }


        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }複製程式碼

認閱讀上面這段程式碼,可以知道;

  • 通常情況下當我們給view設定了OnTouchListener也就是觸控監聽的時候會先執行我們的觸控監聽中的onTouch()方法
  • 並且當onTouch()方法返回false的時候才去執行view的onTouchEvent(),也就是說當onTouch()方法返回true的時候就onTouchEvent方法就不執行了;

View中的onTouchEvent()

簡單分了View的onDispatchTouchEvent方法之後,我們看你下View的onTouchEvent方法

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) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
//Flag3: disabled view 並且是clickable的情況下依然會消費這個事件,只是不做處理。
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
//Flag4
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {

                            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();
                                }
//Flag4:
                                if (!post(mPerformClick)) {
                                    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;
                    }


                    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:
                    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;
            }
//Flag5
            return true;
        }

        return false;
    }複製程式碼

關鍵點:

  • 敲黑白注意Flag4和Flag6處,當view為可點選的狀態下,View直接會返回true
  • Flag6處 當為手勢為抬起的時候會發生點選事件,結果View的dispatch方法我們知道,如果View設定了OnTouchListener並且在ontouch()中返回了true view的點選監聽事件就會失效
  • 這時候再看Flag3處當View為DISABLED的狀態下,如果View是CLICKABLE或者LONGCLICKABLE 就直接消費返回true ,也就是說當View為DISABLED狀態下設定的OnClickListener點選監聽會失效
  • 特別說明:當onTouchEvent返回true的時候就意味著,事件的分發找到了要消費他的地方,也就是本View,所以按下以後的滑動抬起這些動作都統統交到這裡來消費;如果返回false就表示本View大人不消費你,以後就不要來送事件了。

ViewGroup中的事件分發消費

ViewGroup中的dispatchTouchEvent()

程式碼比較長,方便檢視裡面的邏輯,做了適當的刪除,關鍵地方加了註釋

(程式碼來源android-24)

 public boolean dispatchTouchEvent(MotionEvent ev) {

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {

// 首先判斷是否對事件進行攔截 ,也就是給intercepted賦值
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//如果允許攔截,就交給intercepted()判斷
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
//如果不允許攔截,設定為false
                } else {
                    intercepted = false;
                }
            } else {
// 如果ev不是ACTION_DOWN型別而且也沒有找到消費目標 就直接返回true
                intercepted = true;
            }

            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
//如果沒有攔截事件是,就去找要消費這個事件的view,並把它放在newTouchTarget中
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                            newTouchTarget = getTouchTarget(child);
//找了的事件分發的目標 跳出迴圈
                            if (newTouchTarget != null) {                    
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            } 
//事件分發給child,如果child消費,並把newTouchTarget賦值給,mFirstTouchTarget跳出迴圈
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }                         
                        }             
                    }
//
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                    }
                }
            }

//如果沒有找到消費目標,就把該ViewGroup當做一個View處理,因為ViewGroup是View的子類,
//也就是說執行super.dispatchTouchEvent();也就是View的dispatchTouchEvent();
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
 //把事件分發給已經找到的消費目標
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
        return handled;
    }複製程式碼

關鍵點

  • disallowIntercept 這個引數預設是false 表示允許攔截事件,若果為true表示不需要攔截攔截事件, 可以通過requestDisallowInterceptTouchEvent()進行設定,具體應用:ViewGroup的子view通過呼叫這個方法可以告訴他說大哥這個事件別攔截了交給我做吧
  • 是否攔截這個決定權交給ViewGroup自己的onInterceptTouchEvent()做判斷
  • 如果攔截了就直接交給 super.dispatchTouchEvent(event)進行消費
  • 如果沒有攔截就找到自己要”符合條件”的子View進行事件的分發,如果事件沒有分發出去也就是自己view的dispatchTouchEvent()返回了false 或者沒有找到子View做也會呼叫super.dispatchTouchEvent()處理
  • 畫圖

ViewGroup中的InterceptTouchEvent()

pubic boolean onInterceptTouchEvent(){
    return false;
}複製程式碼

預設情況下 返回false

Activity中的事件攔截和分發

  • Q1:嚴格的講,Activity不是View當然也不是ViewGroup,他的事件分發是消費怎麼執行的呢?
  • Q2:為什麼ac沒有onInterceptTouchEvent()?

    首先第一個問題:原始碼中找答案

Activity中dispatchTouchEvent():
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }複製程式碼
PhoneWindow中superDispatchTouchEvent()
   public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }複製程式碼
DecorView
   private final class DecorView extends FrameLayout{
         public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
   }複製程式碼

可見Activity的本質使用的View的dispatchTouchEvent();第二個問題迎刃而解View的dispatchTouchEvent()不需要onInterceptTouchEvent().

應用

經常遇到的問題

  • 為什麼只接受到了ACTION_DOWN 後續事件接收不到
    示例程式碼

//自定義ViewGroup
class MyViewGroup extends ViewGroup{
    public boolean dispatchTouchEvent(MotionEvent e){
    Log.d("李不凡","MyViewGroup   --- dispatchTouchEvent --- "+e..getAction());
        return  super.dispatchTouchEvent(e);
    }
    public boolean onInterceptTouchEvent(MotionEvent e){
      Log.d("李不凡","MyViewGroup   --- onInterceptTouchEvent --- "+e..getAction());
        retrun super.onInterceptTouchEvent(e)
    }
    public boolean onTouchEvent(MotionEvent e){
      Log.d("李不凡","MyViewGroup   --- dispatchTouchEvent --- "+e..getAction());
        retrun super.onTouchEvent(e)
    }

}

//自定義View
class MyViewGroup extends TextView{
    public boolean dispatchTouchEvent(MotionEvent e){
        return  super.dispatchTouchEvent(e);
    }
 public boolean onTouchEvent(MotionEvent e){
        retrun super.onTouchEvent(e)
    }

}

//xml佈局
<MyViewGroup
       android:background="#ffffff"
       android:layout_width="match_parent"
       android:layout_height="300dp">
            <MyView
                //--android:clickable="true" 
                android:background="#ff0000"
                android:text="MyButton"
                android:textSize="20sp"
                android:textColor="#00ff00"
                android:id="@+id/my_view"
                android:layout_width="match_parent"
                android:layout_height="100dp" />
</MyViewGroup>複製程式碼
現象

執行上面這段程式碼會發現,我們做個點選事件,檢視日誌,在ViewGroup 和View中只接受到了ACTION_DOWN 而沒有接收到後續的事件

原始碼

從上面的原始碼分析我們知道 預設情況下 事件分發過程的如果ViewGroup沒有攔截(也就是onInterceptTouchEvent()返回值為false)就會交給子view去做分發,執行ViewGrop的返回值就交給子view的dispatchTouchEvent()的返回值決定 ,預設情況下,view中的onTouchEvent()返回值為false ,導致ViewGroup交給自己的onTouchEvent去做處理預設也是false。最終的結果就是viewgroup的dispatch返回值為false,這個結果就說明viewgroup自己的和子view都不消費這個事件 所以後續的ACTION_MOVE 、ACTION_UP等事件都接收不到

解決
    1. MyView中重寫onTouchEvent()方法返回true。如下

      public boolean onTouchEvent(MotionEvent e){
      //super.ononTouchEvent(e)
         return true;
      }複製程式碼

      注意:如果這樣操作會雖然達到了接收完整事件的目的但MyView身上設定的點選事件會失效。why?提示:遮蔽了super.ononTouchEvent(e);這行程式碼導致的後果

    1. 把MyView設定成onclickable可點選。原始碼依據如下:
   //View中onTouchEvent原始碼(有化簡)
    public boolean onTouchEvent(){
       if ((viewFlags & ENABLED_MASK) == DISABLED) {

                return (((viewFlags & CLICKABLE) == CLICKABLE
                        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
            }

       if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
                break;
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            }
        return true;
    }
    retrun false;複製程式碼

需要注意的是:如果我們的View為DISABLED且ONCLICKABLE的時候事件也會被消費但不會做處理

    1. 給MyView設定OnTouchListener監聽並在onTouch()方法中返回true,依據參照上面的View中dispatchTouchEvent()分析,這種情況也會導致onTouchEvent不會執行,所以如果給view設定點選監聽也會失效。

下集預告

Android原始碼角度分析事件分發消費之應用篇

  • 怎麼解決事件衝突
  • 自定義一個簡單的Viewpager
  • RefreshSwipLayout怎麼進行事件消費和分發的

相關文章