1. 程式人生 > >Android View 的事件分發原理解析

Android View 的事件分發原理解析

pri ping res 參考文獻 方法 處理 llb tst ancestor

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

1、 View 事件認識

1.1 MotionEvent 事件

當你用手指輕觸屏幕,這個過程在 Android 中主要可以分為以下三個過程:

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

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

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

從 ACTION_DOWN 開始到 ACTION_UP 結束我們稱為一個事件序列

正常情況下,無論你手指在屏幕上有多麽騷的操作,最終呈現在 MotionEvent 上來講無外乎下面兩種動作。

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

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

1.2 理論知識

  • public boolean dispatchTouchEvent(MotionEvent ev)

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

    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。

2、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;

            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 也沒用了。

另外,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、Android View的事件分發機制和滑動沖突解決

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

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

Android View 的事件分發原理解析