Android View 的事件分發原理解析

做爲一名 Android 開發者,天天接觸最多的就是 View 了。Android View 雖然不是四大組件,但其並不比四大組件的地位低。而 View 的核心知識點事件分發機制則是很多剛入門同窗的攔路虎,也是面試過程當中基本上都會問的。理解 View 的事件可以讓你寫出更好自定義 View 以及解決滑動衝突。java

一、 View 事件認識

1.1 MotionEvent 事件

當你用手指輕觸屏幕,這個過程在 Android 中主要能夠分爲如下三個過程:android

  • ACTION_DOWN:手指剛接觸屏幕,按下去的那一瞬間產生該事件面試

  • ACTION_MOVE:手指在屏幕上移動時候產生該事件ide

  • ACTION_UP:手指從屏幕上鬆開的瞬間產生該事件post

從 ACTION_DOWN 開始到 ACTION_UP 結束咱們稱爲一個事件序列動畫

正常狀況下,不管你手指在屏幕上有多麼騷的操做,最終呈如今 MotionEvent 上來說無外乎下面兩種動做。this

  • 點擊(點擊後擡起,也就是單擊操做):ACTION_DOWN -> ACTION_UPspa

  • 滑動(點擊後再滑動一段距離,再擡起):ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP.net

 

1.2  理論知識

  • public boolean dispatchTouchEvent(MotionEvent ev)

    return true: 表示消耗了當前事件,有多是當前 View 的 onTouchEvent 或者是子 View 的 dispatchTouchEvent 消費了,事件終止,再也不傳遞。 3d

    return false: 調用父 ViewGroup 或 Activity 的 onTouchEvent。 (再也不往下傳)。

    return super.dispatherTouchEvent: 則繼續往下(子 View )傳遞,或者是調用當前 View 的 onTouchEvent 方法;

總結:用來分發事件,即事件序列的大門,若是事件傳遞到當前 View 的 onTouchEvent 或者是子 View 的 dispatchTouchEvent,即該方法被調用了。 另外若是不消耗 ACTION_DOWN 事件,那麼 down, move, up 事件都與該 View 無關,交由父類處理(父類的 onTouchEvent 方法)

  • public boolean onInterceptTouchEvent(MotionEvent ev) 

    return true: ViewGroup 將該事件攔截,交給本身的onTouchEvent處理。

    return false: 繼續傳遞給子元素的dispatchTouchEvent處理。 

    return super.dispatherTouchEvent: 事件默認不會被攔截。

總結:在 dispatchTouchEvent 內部調用,顧名思義就是判斷是否攔截某個事件。(注:ViewGroup 纔有的方法,View 由於沒有子View了,因此不須要也沒有該方法) 。並且這一個事件序列(當前和其它事件)都只能由該 ViewGroup 處理,而且不會再調用該 onInterceptTouchEvent 方法去詢問是否攔截。

  • public boolean onTouchEvent(MotionEvent ev) 

    return true: 事件消費,當前事件終止。 

    return false: 交給父 View 的 onTouchEvent。 

    return super.dispatherTouchEvent: 默認處理事件的邏輯和返回 false 時相同。

總結:dispatchTouchEvent內部調用 

上面三個方法之間的調用關係能夠用下面的代碼表示:

public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//事件是否被消費
        if (onInterceptTouchEvent(ev)){//調用 onInterceptTouchEvent 判斷是否攔截事件
            consume = onTouchEvent(ev);//若是攔截則調用自身的onTouchEvent方法
        }else{
            consume = child.dispatchTouchEvent(ev);//不攔截調用子View的dispatchTouchEvent方法
        }
        return consume;//返回值表示事件是否被消費,true事件終止,false調用父View的onTouchEvent方法

 

1.3 事件傳遞順序

對於一個點擊事件,Activity 會先收到事件的通知,接着再將其傳給 DecorView(根 view),經過 DecorView 在將事件逐級進行傳遞。具體傳遞邏輯見下圖:

 

能夠看出事件的傳遞過程都是從父 View 到子 View。可是這裏有三點須要特別強調一下

  • 子 View 能夠經過 requestDisallowInterceptTouchEvent 方法干預父 View 的事件分發過程( ACTION_DOWN 事件除外),而這就是咱們處理滑動衝突經常使用的關鍵方法。

  • 對於 View(注意!ViewGroup 也是 View)而言,若是設置了onTouchListener,那麼 OnTouchListener 方法中的 onTouch 方法會被回調。onTouch 方法返回 true,則 onTouchEvent 方法不會被調用(onClick 事件是在 onTouchEvent 中調用)因此三者優先級是 onTouch->onTouchEvent->onClick

  • View 的 onTouchEvent 方法默認都會消費掉事件(返回 true),除非它是不可點擊的(clickable 和 longClickable 同時爲 false),View 的longClickable 默認爲 false,clickable 須要區分狀況,如 Button 的 clickable 默認爲 true,而TextView的 clickable 默認爲 false。

 

二、View 事件分發源碼

先從 Activity 中的 dispatchTouchEvent 方法出發:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

Activity 將事件傳給父 Activity 來處理,下面看父 Activity 是怎麼處理的。

 /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

其中有個 onUserInteraction 方法,該方法是隻要用戶在 Activity 的任何一處點擊或者滑動都會響應,通常不使用。接下去看getWindow().superDispatchTouchEvent(ev) 所表明的具體含義。getWindow() 返回對應的 Activity 的 window。一個Activity 對應一個 Window 也就是 PhoneWindow, 一個 PhoneWindow 持有一個 DecorView 的實例, DecorView 自己是一個 FrameLayout。這句話必定要牢記。

/**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
    public Window getWindow() {
        return mWindow;
    }

Window 的源碼有說明 The only existing implementation of this abstract class is
android.view.PhoneWindow,Window 的惟一實現類是 PhoneWindow。那麼去看 PhoneWindow 對應的代碼。

public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

PhoneWindow 又調用了 DecorView 的 superDispatchTouchEvent 方法。而這個 DecorView 就是 Window 的根 View,咱們經過 setContentView 設置的 View 是它的子 View(Activity 的 setContentView,最終是調用 PhoneWindow 的 setContentView )

到這裏事件已經被傳遞到根 View 中,而根 View 其實也是 ViewGroup。那麼事件在 ViewGroup 中又是如何傳遞的呢? 

 

2.1 ViewGroup 事件分發

public boolean dispatchTouchEvent(MotionEvent ev) {
            ......

            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
       // 當有 down 操做,會把以前的target 以及標誌位都復位 if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);

                //清除 FLAG_DISALLOW_INTERCEPT,而且設置 mFirstTouchTarget 爲 null
                resetTouchState(){
                    if(mFirstTouchTarget!=null){mFirstTouchTarget==null;}
                    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
                    ......
                };
            }
            final boolean intercepted;//ViewGroup是否攔截事件

            // mFirstTouchTarget是ViewGroup中處理事件(return true)的子View
            //若是沒有子View處理則mFirstTouchTarget=null,ViewGroup本身處理
            if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//onInterceptTouchEvent
                    ev.setAction(action);
                } else {
                    intercepted = false;

                    //若是子類設置requestDisallowInterceptTouchEvent(true)
                    //ViewGroup將沒法攔截MotionEvent.ACTION_DOWN之外的事件
                }
            } else {
                intercepted = true;

                //actionMasked != MotionEvent.ACTION_DOWN而且沒有子View處理事件,則將事件攔截
                //而且不會再調用onInterceptTouchEvent詢問是否攔截
            }

            ......
            ......
}

先看標紅的代碼,這句話的意思是:當 ACTION_DOWN 事件到來時,或者有子元素處理事件( mFirstTouchTarget != null ),若是子 view 沒有調用 requestDisallowInterceptTouchEvent 來阻止 ViewGroup 的攔截,那麼 ViewGroup 的 onInterceptTouchEvent 就會被調用,來判斷是不是要攔截。因此,當子 View 不讓父 View 攔截事件的時候,即便父 View onInterceptTouchEvent 中返回true 也沒用了。

這裏須要注意的就是:onInterceptTouchEvent 默認返回 false。 當 ACTION_DOWN 事件到來時,此時 mFirstTouchTarget 爲 null,此時其實也還未收到子 view requestDisallowInterceptTouchEvent。因此這時候,只要父 view 把 ACTION_DOWN 事件給攔截了,那麼子 view 就收不到任何事件消息了。因此,通常在 ACTION_DOWN 的時候,父 view 不做攔截。

當 ACTION_MOVE 事件來臨時,知足某些條件,父 view 想攔截的時候,這時候子 view 能夠在 dispatchTouchEvent 中 ACTION_DOWN 事件來臨的時候,調用 requestDisallowInterceptTouchEvent 就能夠避免被父 view 攔截。

FLAG_DISALLOW_INTERCEPT 這個標記位就是經過子 View requestDisallowInterceptTouchEvent 方法設置的。 具體可參看以下代碼。

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

 同時,若是這個 ViewGroup 有父 View 的時候,還得讓父父 View 不能攔截。繼續看 ViewGroup 的 dispatchTouchEvent 方法。

 public boolean dispatchTouchEvent(MotionEvent ev) {
        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);

            ......

            if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) 
            {
                ev.setTargetAccessibilityFocus(false);
                //若是子View沒有播放動畫,並且點擊事件的座標在子View的區域內,繼續下面的判斷
                continue;
            }
            //判斷是否有子View處理了事件
            newTouchTarget = getTouchTarget(child);

            if (newTouchTarget != null) {
                //若是已經有子View處理了事件,即mFirstTouchTarget!=null,終止循環。
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                //點擊dispatchTransformedTouchEvent代碼發現其執行方法實際爲
                //return child.dispatchTouchEvent(event); (由於child!=null)
                //因此若是有子View處理了事件,咱們就進行下一步:賦值

                ......

                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                //addTouchTarget方法裏完成了對mFirstTouchTarget的賦值
                alreadyDispatchedToNewTouchTarget = true;

                break;
            }
        }
    }

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ......

            if (child == null) {
            //若是沒有子View處理事件,就本身處理
                handled = super.dispatchTouchEvent(event);
            } else {
           //有子View,調用子View的dispatchTouchEvent方法
                handled = child.dispatchTouchEvent(event);

            ......

            return handled;
    }

上面爲 ViewGroup 對事件的分發,主要有 2 點

  • 若是有子 View,則調用子 View 的 dispatchTouchEvent 方法判斷是否處理了事件,若是處理了便賦值 mFirstTouchTarget,賦值成功則跳出循環。

  • ViewGroup 的事件分發最終仍是調用 View 的 dispatchTouchEvent 方法,具體如上代碼所述。

 

2.2 View 的事件分發

public boolean dispatchTouchEvent(MotionEvent event) {  

        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 
        return onTouchEvent(event);  
  }

上述方法只有如下3個條件都爲真,dispatchTouchEvent() 才返回 true;不然執行 onTouchEvent()。

  •  mOnTouchListener != null

  •  (mViewFlags & ENABLED_MASK) == ENABLED

  •  mOnTouchListener.onTouch(this, event)

這也就說明若是調用了 setOnTouchListener 設置了 listener, 就會先調用 onTouch 方法。沒有的話纔會去調用 onTouchEvent 方法。接下去,咱們看 onTouchEvent 源碼。

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  

    if ((viewFlags & ENABLED_MASK) == DISABLED) {  
         
        return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    }  
  // 若是進行了事件代理,就會被攔截,不會在往下面走了
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 若該控件可點擊,則進入switch判斷中 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { // a. 若當前的事件 = 擡起View(主要分析) case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; ...// 通過種種判斷,此處省略 // 執行performClick() ->>分析1 performClick(); break; // b. 若當前的事件 = 按下View case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; // c. 若當前的事件 = 結束事件(非人爲緣由) case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; // d. 若當前的事件 = 滑動View case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } // 若該控件可點擊,就必定返回true return true; } // 若該控件不可點擊,就必定返回false return false; } /** * 分析1:performClick() */ public boolean performClick() { if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; // 只要咱們經過setOnClickListener()爲控件View註冊1個點擊事件 // 那麼就會給mOnClickListener變量賦值(即不爲空) // 則會往下回調onClick() & performClick()返回true } return false; }

從上面的代碼咱們能夠知道,當手指擡起的時候,也就是處於 MotionEvent.ACTION_UP 時,纔會去調用 performClick()。而 performClick 中會調用 onClick  方法。

也就說明了:三者優先級是 onTouch->onTouchEvent->onClick

至此 View 的事件分發機制講解完畢。

 

總結:

  1. 不要把攔截要作的事情放在 dispatchTouchEvent 中。若是 viewGroup 須要攔截事件,在 onInterceptTouchEvent 中在相應的事件返回 true 便可。攔截的操做寫在 onTouchEvent 中。

  2. 若是 viewGroup 在 dispatchTouchEvent 返回 true 。這會致使 viewGroup 中的 dispatchTouchEvent 不執行。引發事件混亂。

  3. 只要 viewGroup 攔截 ACTION_DOWN,就全部的事件都會被攔截,從而響應父 view 的點擊事件。

  4. 子 view 在 dispatchTouchEvent 中的 ACTION_DOWN 中調用 getParent().requestDisallowInterceptTouchEvent(true)  可避免被攔截。前提是父 view 不攔截 ACTION_DOWN 事件。

  5. 若是父 view 不攔截 ACTION_DOWN 事件,但攔截其餘幾個事件,這時候能夠在 onTouchEvent 中寫攔截後須要的操做。

  6. 父 view 開始不攔截 ACTION_DOWN 事件,而後在 ACTION_MOVE 的時候,進行攔截,這時候,子 view 的 dispatchTouchEvent 會收到 ACTION_CANCEL 事件,表示事件被終止了。

  7. 當父 view 攔截事件之後,其事件流程就跟普通的 view 一致了,不要在將其看作 ViewGroup。若是設置了 OnTouchListener 還會響應 onTouch 事件, 注意返回 false, 返回 true 將不會執行 onTouchEvent 。

  

參考文獻:

一、Android View的事件分發機制和滑動衝突解決

二、一文讀懂Android View事件分發機制

三、Android事件分發機制詳解:史上最全面、最易懂

相關文章
相關標籤/搜索