Android事件分發機制

前言

在咱們平時開發中,老是會遇到滑動衝突。那麼,若是要解決滑動衝突,首先就要求咱們理解Android中一個很是重要的知識點:事件分析機制。android

基礎知識準備

Android UI層級

在瞭解事件分發機制以前,咱們要知道Android佈局層級。咱們在寫Activity的時候一般是經過setContentView(int layoutResID)方法設置佈局,因此咱們先從這個方法入手看一下佈局層級。設計模式

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID); //1
    initWindowDecorActionBar(); //2
}

public Window getWindow() {
    return mWindow;
}
複製代碼

由上面的代碼咱們能夠看到,ActivitysetContentView方法其實是調用了WindowsetContentView方法,讓咱們看一下Window對象。bash

/** * Abstract base class for a top-level window look and behavior policy. An * instance of this class should be used as the top-level view added to the * window manager. It provides standard UI policies such as a background, title * area, default key processing, etc. * * 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是一個抽象類了,而且它的惟一一個子類是PhoneWindow,也就是說上面的方法調用其實是調用了PhoneWindow的setContent方法,咱們來看一下。app

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor(); //1
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor(-1); 
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor); //1
            ...
        }
}
複製代碼

在這裏我只挑出了主要的代碼,第一段代碼中咱們能夠看到經過installDecor方法去初始化DecorView,經過查看源碼咱們能夠知道DecorView其實是一個Framelayout。經過第二段代碼的1處咱們能夠看到,其調用了generateLayout方法,咱們來來看一下這裏面作了什麼。ide

protected ViewGroup generateLayout(DecorView decor) {
        ...
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }
        ...
        if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            // If no other features and not embedded, only need a title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
            } else {
                layoutResource = R.layout.screen_title; //註釋1
            }
            // System.out.println("Title!");
        }
        ...
}
複製代碼

上面的代碼省略了不少,咱們從代碼中看到generateLayout方法中會根據不一樣的Future來給layoutResource設置佈局。咱們看一下注釋1處的佈局文件。源碼分析

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
複製代碼

從佈局文件中咱們看到三個部分佈局

ViewStub:用來顯示ActionBar
第一個FrameLayout:用來顯示標題
第二個FrameLayout:用來顯示內容
複製代碼

經過上面的源碼咱們能夠看出:Activity中包含一個Window對象,這對象是由PhoneWindow來實現。PhoneWindow中又包含一個DecorView,並將其做爲跟佈局。這個DecorView又分紅兩個部分,咱們平時設置佈局時實際上是將佈局展現在Content區域。 post

由於DecorView是繼承自FrameLayout,而FrameLayout又繼承自ViewGroup。因此咱們能夠將事件的流向簡單的表示出來:Activity->ViewGroup->View。

事件概念

提了不少次事件,事件的具體概念是什麼?其實,這裏所說的事件,是指當手指從觸碰到手機屏幕到離開手指屏幕所產生的一系列Touch事件,其中也包括手指在手指在屏幕上滑動所對應的操做。這些Touch事件被封裝到一個MotionEvent對象中,接下來讓咱們看一下這個對象具體是作什麼的。
實際上,事件分發機制處理的對象是手指操做屏幕時所產生的一些列MotionEvent對象。動畫

MotionEvent事件分類和對應含義

事件分類 對應含義
MotionEvent.ACTION_DOWN A pressed gesture has started(一個按壓動做已經開始了)
MotionEvent.ACTION_MOVE A change has happened between ACTION_DOWN and ACTION_UP(在點擊和擡起事件中間的一些列操做)
MotionEvent.ACTION_UP A pressed gesture has finished(一個按壓動做已經結束)
MotionEvent.ACTION_CANCEL A movement has happened outside of the normal bounds of the UI element(在滑動的過程當中超出邊界)

上面的表格中只列舉了開發中最經常使用的幾種事件類型,其實還有不少事件類型,有興趣的小夥伴能夠自行查看。MotionEvent對象中不只封裝了事件類型,同時封裝了手指在操做過程當中的座標。ui

涉及到的方法

事件分發機制中主要涉及到三個方法:dispatchTouchEvent()、onTouchEvent()和onInterceptTouchEvent()。

方法名 方法的含義
dispatchTouchEvent() 被用來事件分發,若是事件傳遞到當前的View,該方法必定會被調用,默認返回false
onInterceptTouchEvent() 該方法被用來判斷是否攔截某個事件,在dispatchTouchEvent方法中調用 ,返回值表示是否攔截當前事件,一般存在於ViewGroup,通常View中沒有該方法
onTouchEvent() 該方法被用來處理點擊事件,返回值表示是否消耗當前的事件,若是不進行消耗,該系事件序列將不會被接受

小結

從上面的分析中咱們能夠進行一下小結:

當手指觸摸屏幕產生的一些列操做會被封裝在MotionEvent中,經過Activity、ViewGroup和View調用一系列方法,找到事件的接受者並處理該事件的一個過程。
複製代碼

源碼分析

從上面的分析咱們已經知道事件的大體流向:Activity->ViewGroup->View,接下來讓咱們從源碼的角度逐個分析。

Activity事件分發

既然事件最初是從Activity中進行傳遞,因此咱們首先到Activity中找到dispatchTouchEvent()方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction(); 
    }
    if (getWindow().superDispatchTouchEvent(ev)) { //1
        return true;
    }
    return onTouchEvent(ev);
}
複製代碼

註釋1處的getWindow()方法獲取的是Window對象,可是Window是一個抽象類,咱們看一下它的惟一子類PhoneWindowsuperDispatchTouchEvent()方法

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

DecorView.superDispatchTouchEvent public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
複製代碼

咱們看到在PhoneWindowsuperDispatchTouchEvent()方法實際上調用了DecorViewsuperDispatchTouchEvent()方法,這個方法內部調用了super.dispatchTouchEvent()。在基礎知識準備中咱們知道DecorView是繼承自Framelayout,在Framelayout中是沒有dispatchTouchEvent()的方法,可是Framelayout是繼承自ViewGroup。因此,最後仍是調用到了ViewGroupdispatchTouchEvent(),自此事件從Activity傳遞到了ViewGroup

ViewGroup事件分發

既然事件已經從Activity傳遞到了ViewGroup,讓咱們看一下ViewGroup具體作了什麼處理。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
         //--------------------1----------------------
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
         //--------------------2----------------------
         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);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        //--------------------3----------------------
        final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        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) {
                   ...
                    //--------------------4----------------------
                    for (int i = childrenCount - 1; i >= 0; i--) {
                    ...
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
                        //--------------------5----------------------
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        ...
                        }
                    }    
                }
            }
        }
    }
}

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        return;
    }
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    ...
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...
}
複製代碼

通過一番查找,終於把源碼摘清。我這裏是Android9.0的源碼,若是跟各位的有出入,以各位手上的源碼爲準。閒話少說,直接看源碼中都作了什麼。
上面的代碼共有五處註釋:

註釋1:這部分代碼主要是清除事件接收目標同時初始化事件狀態。
咱們看到先判斷是不是ACTION_DOWN事件,也就是手指按下的事件。
若是是,則調清除以前的事件接收目標,同時也初始化了點擊狀態。
這也很好理解,當觸發ACTION_DOWN時說明用戶從新對手機作了操做,
有可能點擊了不一樣的控件,以前事件接收目標及其點擊狀態都應該重置掉。
複製代碼
註釋2:這部分代碼主要是用來判斷是否要攔截事件。
首先判斷了是不是ACTION_DOWN事件,或者事件接收目標是否爲空。若是這兩個條件都不成立,將會標識攔截。
在判斷條件內咱們還看到了FLAG_DISALLOW_INTERCEPT標識,這個標識表示子View是否容許父控件攔截事件,
當子View調用requestDisallowInterceptTouchEvent(boolean)方法時設置的。
只有當requestDisallowInterceptTouchEvent(boolean)傳入值爲true時,
表示不容許父控件攔截(上面第二段代碼)。disallowIntercept默認值爲false,也就是子控件沒有設置禁止父控件攔截事件。
這時,會調用onInterceptTouchEvent()方法(第三段代碼)。
複製代碼
註釋3:這段代碼主要來判斷是否發生了ACTION_CANCEL事件。
這個事件在基礎知識準備中已經有所說明,這裏就再也不詳細講解。
複製代碼
註釋4:經過循環遍歷找到子View。
首先是經過倒序循環遍歷的方式找到每個子View,爲何是倒序呢,。。。。
找到子View以後會調用canViewReceivePointerEvents()和isTransformedTouchPointInView()方法來判斷子View的狀態。
這裏邊包括是否可見、是否正在播放動畫、點擊事件的座標是否落在子View的顯示範圍內。
複製代碼
註釋5:這部分代碼是ViewGroup事件分發的核心。
這裏會調用dispatchTransformedTouchEvent()方法(第四段代碼),在這個方法中咱們能夠看到,回去判斷是否有子View,
若是有,繼續調用子View的dispatchTouchEvent()方法;若是沒有,則去調用super.dispatchTouchEvent()方法。
複製代碼

咱們都知道ViewGroup是繼承自View的,因此接下來讓咱們看一下View的dispatchTouchEvent()方法。

View事件分發

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        ...
        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;
        }
    }
    ...
    return result;
}
複製代碼

從上面的代碼中咱們看到,首先會獲取OnTouchListener信息,也會調用OnTouchListeneronTouch()方法。若是OnTouchListener不爲空,而且onTouch()方法返回true以後,就不會再執行onTouchEvent()方法,不然的話還要去執行onTouchEvent()方法。從中咱們也能夠看出OnTouchListener中的onTouch()方法優先級要高於onTouchEvent(MotionEvent)方法。

onTouchEvent(MotionEvent)

View的事件分發過程當中調用了onTouchEvent(MotionEvent)方法,咱們來看一下。

public boolean onTouchEvent(MotionEvent event) {
    ...
    //----------------------1----------------------
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    //----------------------2----------------------
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return clickable;
    }
    ...
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                            //----------------------3----------------------
                                performClickInternal();
                            }
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_DOWN:
                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }
                boolean isInScrollingContainer = isInScrollingContainer();
                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;
            ...
        }
        return true;
    }
    return false;
}

private boolean performClickInternal() {
    ...
    return 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;
    }
    ...
    return result;
}
複製代碼

註釋1處檢查了該View是不是可點擊或者是長按點擊,並記錄下來;註釋2處咱們從英文註釋中能夠看到,雖然一個View是一個disabled狀態,可是隻要是clickable任然會消費事件,只是不作任何反饋而已。
以後會根據事件類型進行判斷,當處於ACTION_UP也就是說當手指離開時,會觸發performClickInternal方法,這個方法去執行內部點擊,具體調用方法在最後兩段代碼中。從最後一段代碼中咱們看到了很熟悉的代碼塊li.mOnClickListener.onClick(this),這裏的OnClickListener就是咱們本身設置的。可是咱們在ACTION_DOWN中沒有看到執行點擊的方法。因此,執行點擊的方法不是在觸摸時產生,而是在手指離開時產生。
從上面的源碼中咱們也能夠看到, 最後從網上找了一張事件分發機制流程圖,感謝各位大佬。

舉例說明

下面我會經過一個案列加以說明

public class MyActivity extends AppCompatActivity {
    private MyButton my_buttom;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        my_buttom = findViewById(R.id.my_buttom);
        my_buttom.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("EventDemo", "MyButton--->onClick");
            }
        });
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("EventDemo", "MyActivity--->dispatchTouchEvent"+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("EventDemo", "MyActivity--->onTouchEvent"+event.getAction());
        return super.onTouchEvent(event);
    }
}
複製代碼
public class MyLinearLayout extends LinearLayout {
    public MyLinearLayout(Context context) {
        super(context);
    }
    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("EventDemo", "MyLinearLayout--->dispatchTouchEvent" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("EventDemo", "MyLinearLayout--->onInterceptTouchEvent" + ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("EventDemo", "MyLinearLayout--->onTouchEvent" + event.getAction());
        return super.onTouchEvent(event);
    }
}
複製代碼
public class MyButton extends AppCompatButton {
    public MyButton(Context context) {
        super(context);
    }
    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("EventDemo", "MyButton--->dispatchTouchEvent" + event.getAction());
        return super.dispatchTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("EventDemo", "MyButton--->onTouchEvent" + event.getAction());
        return super.onTouchEvent(event);
    }
}
複製代碼
<com.example.eventdemo.MyLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    
    <com.example.eventdemo.MyButton
        android:id="@+id/my_buttom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="點我" />
        
</com.example.eventdemo.MyLinearLayout>
複製代碼

正常狀況

咱們直接默認設置,直接運行點擊看一下效果。

咱們看到這一次點擊一共產生了兩個事件:0表示ACTION_DOWN;1表示ACTION_UP。咱們能夠看到,事件首先是從Activity傳入到ViewGroup中。
第一步:事件首先傳遞到Activity中,調用了Activity的dispatchTouchEvent方法;
第二步:事件進過Activity以後,傳入了ViewGroup中,調用了dispatchTouchEvent方法。在這個方法中調用了onInterceptTouchEvent,查看是否要攔截事件,這裏返回的都是默認值,因此不進行攔截。
第三步:通過ViewGroup的事件分發後,將時間傳遞到了View中。首先仍是調用了View的dispatchTouchEvent方法。由於View中是沒有onInterceptTouchEvent方法的,因此會調用View的onTouchEvent方法,由於這裏的View其實是一個Button,而Button默認的是能夠點擊的,因此在onTouchEvent方法中返回true,這就代表該事件的接收者是該Button。
第四步:當手指擡起時,會產生一個ACTION_UP事件,其分發過程跟上面一致。可是當傳入到View中時會調用performClickInternal方法最終調用onClick方法。

比較OnTouchListener.onTouch()和onTouchEvent()方法的優先級

咱們在MyActivity中添加以下代碼:

my_buttom.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            Log.i("EventDemo", "MyButton--->onTouch");
            return false;
        }
});
複製代碼

咱們來看一下運行結果:

從運行結果來看,咱們能夠看到OnTouchListener.onTouch()方法的優先級要高於onTouchEvent()方法。

總結

寫了這麼多,讓咱們總結一下事件分發機制。

一、一般狀況下,事件的序列是ACTION_DOWN開始,到ACTION_UP結束。
二、當一個事件由一個View決定攔截時(ACTION_DOWN觸發時決定攔截),該事件序列都會由其接收。不然該事件序列將再也不被其接收。
三、從優先級上講:OnTouchListener.onTouch > onTouchEvent > onClickListener。
四、當一個View是disable狀態時,但它是可點擊狀態時,事件仍是由它來接收,只是不作任何反饋。
五、事件分發機制的核心設計模式是責任鏈設計模式,這個模式在okhttp的源碼中也能看到。
複製代碼

到這裏事件分發機制算是過了一遍,若是有什麼寫的不對的地方或者是有哪裏遺漏了,還請各位大佬批評指正。

相關文章
相關標籤/搜索