天天看點

View的事件體系_View的事件分發機制

點選事件的傳遞規則:

當一個MotionEvent産生後,系統需要把這個事件傳遞給一個具體的View,而這個過程就是事件的分發過程,這個過程由3個很重要的方法組成,dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent,

public boolean dispatchTouchEvent(MotionEvent ev)(ViewGroup裡面有的方法)

用來進行事件的分發.如果事件能夠傳遞到目前的View,那麼此方法一定會被調用,傳回結果受目前View的onTouchEvent和下級子View的dispatchTouchEvent方法的影響,表示是否消耗目前事件

public boolean onInterceptTouchEvent(MotionEvent ev)(ViewGroup裡面有的方法)

在上面的dispatchTouchEvent方法裡面調用,用來判斷是否攔截某個事件,如果目前View攔截了某個事件(例如down事件),那麼在同一個事件序列當中,此方法不會被再次調用,傳回結果表示是否攔截目前事件

說明:

同一個事件 序列是指:從手機接觸螢幕的那一刻起,到手機離開螢幕的那一刻結束,在這個過程中所産生的一系列事件,這個事件序列以down事件開始,中間含有數量不定的move事件,最終以up事件結束

public boolean onTouchEvent(MotionEvent event)(View裡面有的方法)

在上面的dispatchTouchEvent方法裡面調用,用來處理點選事件,傳回結果表示是否消耗目前事件,如果不消耗(down事件傳回false),則在同一個事件序列當中,目前view無法再次接受到(move和up)事件

這3個方法的關系可以用下面的為代碼來表示:

public boolean dispatchTouchEvent(MotionEvent ev) {
   boolean consume=false;
    if (onInterceptTouchEvent(ev)) {
        //如果父類ViewGroup要攔截事件,那麼它就會調用自己的onTouchEvent方法來處理事件
        consume=this.onTouchEvent(ev);
    }else{
        //如果父類ViewGroup不要事件,那麼它就會讓自己的孩子去分發事件
        consume=child.dispatchTouchEvent(ev);
    }
    return consume;
}
           

當一個點選事件産生後,它的傳遞過程遵循如下順序:Activity–>window–>view,即事件總是先傳遞給Activity,Activity再傳遞給window,最後window再傳遞給頂級View

當一個View需要處理事件時,如果它設定了OnTouchListener,那麼OnTouchListener中的onTouch方法會被回調,如果onTouch方法傳回false,則目前的onTouchEven方法會被調用,如果傳回true,就不會調用onTouchEven方法,是以OnTouchListener的優先級大于

onTouchEven,在onTouchEvent方法中,如果目前設定的有onCLickListener,那麼它的onClick方法會被調用

優先級:OnTouchListener>onTouchEven(onClick()方法在這個此方法裡面)

正常情況下:一個事件序列隻能被一個View攔截且消耗

當一個事件傳到view上時:首先是觸發action_down方法,action_down傳回true表示此View要消耗這個事件,那麼同一個事件序列的action_move和action_up都會傳給它,action_down傳回false 表示此view不要這個事件,那麼同一個事件序列的action_move和action_up都不會傳到它那裡

事件的傳遞過程是由外向内,即事件總是先傳遞給父元素,然後再由父元素分發給子View,通過getParent().requestDisallowInterceptTouchEvent(boolean disallowIntercept);方法可以在子元素中幹預父元素的事件分發過程,但是父元素的action_down事件子元素幹預不了

事件分發的源碼解析:

1.Activity對點選事件的分發過程:

點選操作發生時,事件最先傳遞給目前的acivity,由activity的dispatchTouchEvent方法來進行分發,具體的工作是由activity内部的window來完成,window會把事件傳遞給decor view,decor view一般就是目前界面的底層容器(即setContentView所設定的View的父容器),通過Activity.getWindow.getDecorView()可以獲得.decor view繼承幀布局FrameLayout

源碼:Activity#dispatchTouchEvent(MotionEvent ev)

/**
     * 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) {
          //這個方法是個空實作 interaction:互相作用,互相影響
          //我們可以重寫添加我們的邏輯
            onUserInteraction();
        }
        //交由activity所屬的window來派發事件
        //傳回true;表示有View消耗了這個事件
        //傳回false;表示沒有View消耗事件,需要act自己來處理
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //如果沒有view消耗事件,那麼act自己的onTouchEven會調用
        return onTouchEvent(ev);
    }
           

2.window.對點選事件的分發過程:

接下來看一下window是如何把事件傳遞給ViewGroup的

/* <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {...}
           

源碼:window#superDispatchTouchEvent(MotionEvent event)

/**
     * Used by custom windows, such as Dialog, to pass the touch screen event
     * further down the view hierarchy. Application developers should
     * not need to implement or call this.
     */
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
           

通過源碼和它的英文解釋,我們知道Window 是一個抽象類,window的唯一實作類是PhoneWindow

(如何進到PhoneWindow源碼裡面看具體代碼呢,這個有個小技巧:找到window裡面的一個抽象方法 例如這裡的getDecorView()方法,然後按住ctrl按鍵同時滑鼠點選getView(),出現如下圖的情況

View的事件體系_View的事件分發機制

找到PhoneWindow.java類,點選進去,就可以進到PhoneWindow.java類裡面了)

/** @hide */
public class PhoneWindow extends Window implements MenuBuilder.Callback {...}
           

源碼:PhoneWindow#superDispatchTouchEvent

//PhoneWindow實作window的superDispatchTouchEvent抽象方法
 public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
           

由上所知PhoneWindow把事件直接傳遞給了DecorView,至此window的事件就分發完成了

3.DecorView.對點選事件的分發過程:

首先我們看一下DecorView是什麼東西

源碼: phoneWindow#mDecor

// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
           
/** @hide */
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {...}
           

由以上的源碼和英文解釋可以知道DecorView 是頂級View即根View,它繼承與FrameLayout.通過getWindow().getDecorView()獲得,我們常在activity中見的setContentView所設定的View,就可以通過mDecorView.findViewById(android.R.id.content).getChildAt(0)來得到

我們大緻看一下ViewGroup對事件的分發:

源碼 ViewGroup#dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }
        ...代碼省略
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            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();
            }   
               
            // Check for interception.
            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;
            }
          ....代碼省略 
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 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;
                        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;
                }
            }
        ....代碼省略 
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
           

這裡我們再回顧一下viewGroup事件的分發過程:

點選事件到達頂級View(一般是一個ViewGroup)以後,會調用ViewGroup的DispatchTouchEvent方法,如果頂級View攔截事件即onInterceptTouchEven方法傳回true,則事件由頂級ViewGroup自己處理,這時候,如果ViewGroup的onTouchListener被設定了,則onTouchListener的onTouch()方法會被調用,否則onTouchEven方法會被調用,也就是說如果都提供的話,onTouchListener()方法會屏蔽onTouchEven方法(),在onTouchEven方法中如果設定了onClickListerner,則onClick方法會被調用.如果viewGroup不攔截事件,事件會分發給它的子View,這時子View的DispatchTouchEvent方法會被調用,到此事件已經從頂級VIewGroup傳給了它的子view,即下來的傳遞過程和頂級ViewGroup的過程是一樣的,如此循環完成整個事件的分發

說明:

1當子view通過設定父View不要攔截事件後即

getParent().requestDisallowInterceptTouchEvent(true);

父ViewGroup就攔截不了ACTION_MOVE和 ACTION_UP事件,但是 還是可以攔截ACTION_DOWN事件,

理由:ViewGrop會在ACTION_DOWN事件到來時做重置狀态的操作,resetTouchState()方法中會對FLAG_DISALLOW_INTERCEPT進行重置,是以子view設定requestDisallowInterceptTouchEvent(true)不能影響ViewGroup對ACTION_DOWN事件的處理,源碼如下

源碼 ViewGroup#dispatchTouchEvent

// Handle an initial down.
   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();
     }
           

4.View對點選事件的分發

源碼: view#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
  ...省略代碼
   if (onFilterTouchEventForSecurity(event)) {
      if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        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;
        }
    }
 ...省略代碼
}
           

首先View會判斷有沒有設定OnTouchListener,如果有,則執行li.mOnTouchListener.onTouch(this, event)傳回true,否則onTouchEvent(event)方法會被調用,我們在看一下onTouchEvent(event)方法裡面的源碼

源碼:view#onTouchEvent(MotionEvent event)

public boolean onTouchEvent(MotionEvent event) {
  ...省略代碼
   switch (action) {
       case MotionEvent.ACTION_UP:
        ...省略代碼
       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)) {
                     performClick();
                 }
             }
         }     
  ...省略代碼
}
           

由以上源碼可知在 MotionEvent.ACTION_UP事件裡 如果 if (!post(mPerformClick)) 條件為真的情況下,會執行performClick()方法

源碼view#performClick()

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;
    }
           

在performClick() 裡面如果 if (li != null && li.mOnClickListener != null) 條件為真的情況下,會執行 li.mOnClickListener.onClick(this);

至此,事件分發機制已經講完啦~(容我喝口水,吃跟辣條休息一下,精彩馬上繼續^- ^)

繼續閱讀