android事件分發機制從入門到入土

首先來舉一個通俗的小例子:一個公司接到一個項目,首先老闆是肯定不會親自做的,於是交給經理,經理也不想做,就交給組長,組長感覺太簡單就交給組員來做,(在這個從上往下的過程中老闆、經理、組長都可以自己來做這個項目,這樣就不會通知到後面的人),組員拿到項目後如果覺得自己能做就會自己來做,如果覺得自己做不了就會告訴組長,這個項目太難了我做不了,組長一看這個項目還真的挺難的自己也做不了,於是告訴經理這個項目他也做不了,經理覺得你們都不會做那我也不會做就告訴老闆,老闆沒辦法,自己的公司只能硬着頭皮做了。(在這個從下往上的過程中組長、經理、老闆都可以自己來做這個項目,這樣就不會通知上面的人了)

通常總結都是寫在最後,這次我寫在最前面,因爲我感覺將總結寫在前面可以讓讀者心中先有一個概念,這樣看後面的內容時就會心中有數。
總結:當觸摸到一個控件,首先觸摸事件找到到最上層的dispatchTouchEvent方法(事件是通過dispatchTouchEvent方法分發的),然後觸摸事件從上往下分發,在分發期間上層可以截斷對下層的分發,如果沒有截斷,最下層會接受並處理觸摸事件,處理完後會選擇繼續處理還是交給上層的處理。

這篇文章就不使用標題了,我們從事件分發的源頭講到結尾。首先一個觸摸事件的起點在哪裏?當然是屏幕了,屏幕接收到觸摸事件會傳遞到Linux的驅動層,然後在傳到java層,Activity的dispatchTouchEvent方法會收到這個事件,我們看一下這個方法:

    /**
     * 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);
    }

關鍵代碼是getWindow().superDispatchTouchEvent(ev),getWindow()是PhoneWindow的一個實現類,我們看一下PhoneWindow中的superDispatchTouchEvent(ev)方法:

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

這裏調用了DecorView的superDispatchTouchEvent方法,我們在跟進看一下這個方法:

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

這裏調用了父類的dispatchTouchEvent方法,我們知道DecorView繼承自FrameLayout,而FrameLayout又繼承自ViewGroup,所以這裏調用的是ViewGroup的dispatchTouchEvent方法,到這裏就到了重點了,講重點之前我們先做個小結;
用戶觸摸屏幕——>觸摸事件傳到Linux驅動層——>傳到Java層——>Activity的dispatchTouchEvent()方法——>PhoneWindow的superDispatchTouchEvent()方法——>DecorView的superDispatchTouchEvent()方法——>ViewGroup的dispatchTouchEvent()方法

下面來看事件分發的重點和難點ViewGroup的dispatchTouchEvent方法:

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

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

            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            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 float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        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);
                            
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                }
            }
        }
        return handled;
    }

上面的代碼經過刪減只留下了關鍵代碼,首先看第20到33行的代碼,這裏有一個判斷,當觸摸事件爲ACTION_DOWN或者mFirstTouchTarget不等於空的時候進入判斷,爲什麼會有這個判斷呢,我們先看一下判斷裏面的代碼,注意第24行的代碼,這裏調用了onInterceptTouchEvent()方法用於攔截事件,再來看判斷的內容,當觸摸事件爲ACTION_DOWN或mFirstTouchTarget不等於空時一定可以攔截事件。再來看看onInterceptTouchEvent():

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

這個方法是ViewGroup用來攔截事件的,如果不重寫他,他一般是返回false,也就是不攔截,我們可以在自定義ViewGroup中重寫並加入自己的攔截邏輯。
接着看54行有一個for循環遍歷當前ViewGroup的所有子view(注意:一個ViewGroup的子view可能也是一個ViewGroup,但是不包括這個ViewGroup裏的View;舉個例子:ViewGroup1的子view是ViewGroup2,ViewGroup2的子view是view1、view2,這時ViewGroup1的子view不包括view1、view2),注意他的循環方式是倒序,也就是先調用最外層的子view,也就是說子view的事件從最外層向裏分發,爲什麼這裏要加一個“子view”呢?因爲事件是先經過ViewGroup才能到View的。在循環裏做了什麼呢,我們看一下第60行的代碼是一個判斷,判斷裏調用了dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign),我們進入這個方法看一下:

    /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        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;
        }
        // Done.
        transformedEvent.recycle();
        return handled;
    }

第18行調用了子view的dispatchTouchEvent(event)方法,如果子view是一個ViewGroup又會繼續重複上面的代碼,如果是一個View就會調用View的dispatchTouchEvent(event)方法。在看View的dispatchTouchEvent(event)方法之前看一下上面代碼的返回值handled,如果子view攔截事件就會返回true,那麼就會進入60行的判斷裏,在這個判斷裏最終會調用break跳出循環就不會繼續遍歷其他的子view了,其他的子view自然也就接收不到觸摸事件。

接下來看View的dispatchTouchEvent(event)方法:

    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

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

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

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

這裏面的代碼還是比較簡單的,首先看30到34行,這裏的if語句首先判斷view是否註冊了Touch監聽事件,view是否可用,調用View的onTouch方法並判斷返回值,從這裏可以看出觸摸事件傳到view首先調用的是onTouch方法,如果onTouch方法返回true,那麼就會進入if內給result賦值爲true;接着看第36行的if判斷,首先判斷!result,如果result是true就不會繼續運行後面的判斷,也就是不會調用onTouchEvent方法,這裏做個小結,當View的onTouch方法返回true攔截了事件就不會再調用onTouchEvent方法。onTouch和onTouchEvent都已經調用了,那麼onClick呢?我們看一下onTouchEvent方法:

    public boolean onTouchEvent(MotionEvent event) {
      
        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) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            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();
                                }
                                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:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    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:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }
            return true;
        }
        return false;
    }

在第三行的判斷中判斷只有clickable 的View才能進入if內的代碼,這個很重要,因爲進入到if內就一定會返回true,也就是一定會攔截事件,而沒有進入就一定會返回false,就不會攔截事件。
這個方法會分別處理ACTION_UP、ACTION_DOWN、ACTION_MOVE、ACTION_CANCEL事件,我們主要看一下ACTION_UP,第48行調用了performClick()方法:

    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    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;
    }

看到第14行的代碼了嗎?這下知道onclick在那調用的了,那麼View的觸摸事件傳遞順序爲onTouch——>onTouchEvent——>onClick。

到這裏事件分發的基本流程已經講完了,肯定還有很多人云裏霧裏,因爲只是講了一下流程,還有很多細節沒有講,下面我們會寫一個例子詳細講解。

首先創建一個ViewGroup和一個View並重寫一些方法(注意這些方法都是默認的實現,並沒有修改其中的邏輯)

public class MyLayout1 extends LinearLayout implements View.OnTouchListener {
    private static final String TAG = "MyLayout1";

    public MyLayout1(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.setOnTouchListener(this);
    }

    /**
     * @param ev
     * @return 攔截事件的分發
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

    /**
     * @return 攔截點擊事件
     */
    @Override
    public boolean performClick() {
        return super.performClick();
    }

    /**
     * @param event
     * @return 接收處理觸摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: layout1");
        return super.onTouchEvent(event);
    }

    /**
     * @param ev
     * @return 分發觸摸事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
//        for (int i = 0; i < getChildCount(); i++) {
//            Log.e(TAG, "dispatchTouchEvent1: "+getChildAt(i).toString() );
//        }
        Log.e(TAG, "dispatchTouchEvent: layout1");
        return super.dispatchTouchEvent(ev);
    }

    /**
     * @param v
     * @param event
     * @return 接收處理觸摸事件,在onTouchEvent之前調用
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e(TAG, "onTouch: layout1");
        return false;
    }
}
public class MyView1 extends Button implements View.OnTouchListener {
    private static final String TAG = "MyView1";

    public MyView1(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnTouchListener(this);
    }

    /**
     * @param event
     * @return 接收處理觸摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: view1");
        return super.onTouchEvent(event);
    }

    /**
     * @return 分發觸摸事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent: view1");
        return super.dispatchTouchEvent(event);
    }

    /**
     * @param v
     * @param event
     * @return 接收處理觸摸事件,在onTouchEvent之前調用
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e(TAG, "onTouch: view1");
        return false;
    }
}

在佈局中加入這兩個View:

<?xml version="1.0" encoding="utf-8"?>
<com.shenhesoft.myapplication.MyLayout1 xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.shenhesoft.myapplication.MyView1
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#000" />
</com.shenhesoft.myapplication.MyLayout1>

在MainAcitity中也重寫一些方法,也是不修改邏輯:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test_touch);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: MainActivity" );
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "dispatchTouchEvent: MainActivity" );
        return super.dispatchTouchEvent(ev);
    }
}

現在運行程序界面如下:
這裏寫圖片描述

測試開始,點擊一下MyView1打印的log如下:

-----------------------------------------按下-------------------------------------
09-12 06:46:23.726 30457-30457/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 06:46:23.726 30457-30457/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
09-12 06:46:23.726 30457-30457/com.shenhesoft.myapplication E/MyView1: dispatchTouchEvent: view1
    onTouch: view1
    onTouchEvent: view1
----------------------------------------鬆開------------------------------------
09-12 06:46:23.827 30457-30457/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 06:46:23.827 30457-30457/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
09-12 06:46:23.827 30457-30457/com.shenhesoft.myapplication E/MyView1: dispatchTouchEvent: view1
    onTouch: view1
    onTouchEvent: view1

在按下屏幕的時候出觸發一個ACTION_DOWN事件,事件傳遞如下:MainActivity的dispatchTouchEvent——>MyLayout1的dispatchTouchEvent——>MyView1的dispatchTouchEvent——>MyView1的onTouch——>MyView1的onTouchEvent;
到這裏ACTION_DOWN的事件傳遞結束了,鬆開屏幕的時候ACTION_UP的事件傳遞以一樣的,我們知道事件傳遞到View後如果View沒有攔截就會交給上層的ViewGroup處理,然而這裏並沒有交給上層而是在MyView1的onTouchEvent方法中終止了,那我們是不是可以理解爲MyView1的onTouchEvent方法攔截了事件呢?還記得上面講到onTouchEvent方法時有一個if判斷嗎,我們的MyView1是繼承自Button也就是clickable爲true,那麼onTouchEvent一定會返回true的,這就解釋了事件爲什麼被攔截了,如果我們強行在MyView1中返回false會怎麼樣呢?

public class MyView1 extends Button implements View.OnTouchListener {
    private static final String TAG = "MyView1";

    public MyView1(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnTouchListener(this);
    }

    /**
     * @param event
     * @return 接收處理觸摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: view1");
//        return super.onTouchEvent(event);
        //強行返回false
        return false;
    }

    /**
     * @return 分發觸摸事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent: view1");
        return super.dispatchTouchEvent(event);
    }

    /**
     * @param v
     * @param event
     * @return 接收處理觸摸事件,在onTouchEvent之前調用
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e(TAG, "onTouch: view1");
        return false;
    }
}

同樣點擊MyView1,打印的log如下:

-------------------------------------按下----------------------------------
09-12 07:06:12.588 30457-30457/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 07:06:12.588 30457-30457/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
09-12 07:06:12.588 30457-30457/com.shenhesoft.myapplication E/MyView1: dispatchTouchEvent: view1
    onTouch: view1
    onTouchEvent: view1
09-12 07:06:12.588 30457-30457/com.shenhesoft.myapplication E/MyLayout1: onTouch: layout1
    onTouchEvent: layout1
09-12 07:06:12.588 30457-30457/com.shenhesoft.myapplication E/MainActivity: onTouchEvent: MainActivity
-------------------------------------鬆開------------------------------------------
09-12 07:06:18.387 30457-30457/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
    onTouchEvent: MainActivity

當我們按下的時候ACTION_DOWN傳到MyView1的onTouchEvent後並沒有攔截而是向上(ViewGroup)傳遞,調用了MyLayout1的onTouch——>MyLayout1的onTouchEvent,由於我們在MyLayout1中沒有攔截所以事件繼續向上(Activity)傳遞,最終傳遞到MainActivity的onTouchEvent方法;當我們鬆開時觸發ACTION_UP事件,由於之前沒有攔截,所以事件不會在向MyView1和MyLayout1傳遞,所以直接到MainActivity的onTouchEvent方法。

如果我們點擊MuLayout1會怎樣呢?我把log放上就不解釋了:

-------------------------------------按下----------------------------------
09-12 07:30:01.884 30457-30457/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 07:30:01.884 30457-30457/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
    onTouch: layout1
    onTouchEvent: layout1
09-12 07:30:01.884 30457-30457/com.shenhesoft.myapplication E/MainActivity: onTouchEvent: MainActivity
-------------------------------------鬆開------------------------------------------
09-12 07:30:02.891 30457-30457/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
    onTouchEvent: MainActivity

看了上面的那麼多相信你已經對安卓的事件分發機制有了比較深入的理解,下面我們在添加兩個View:

public class MyLayout2 extends FrameLayout implements View.OnTouchListener {
    private static final String TAG = "MyLayout2";

    public MyLayout2(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.setOnTouchListener(this);
    }

    /**
     * @param ev
     * @return 攔截事件的分發
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

    /**
     * @return 攔截點擊事件
     */
    @Override
    public boolean performClick() {
        return super.performClick();
    }

    /**
     * @param ev
     * @return 分發觸摸事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
//        for (int i = 0; i < getChildCount(); i++) {
//            Log.e(TAG, "dispatchTouchEvent2: "+getChildAt(i).toString() );
//        }
        Log.e(TAG, "dispatchTouchEvent: layout2");
        return super.dispatchTouchEvent(ev);
    }

    /**
     * @param event
     * @return 接收處理觸摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: layout2");
        return super.onTouchEvent(event);
    }

    /**
     * @param v
     * @param event
     * @return 接收處理觸摸事件,在onTouchEvent之前調用
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e(TAG, "onTouch: layout2");
        return false;
    }
}

public class MyView2 extends Button implements View.OnTouchListener {
    private static final String TAG = "MyView2";

    public MyView2(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnTouchListener(this);
    }

    /**
     * @param event
     * @return 接收處理觸摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: view2");
        return super.onTouchEvent(event);
//        return false;
    }

    /**
     * @return 分發觸摸事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent: view2");
        return super.dispatchTouchEvent(event);
    }

    /**
     * @param v
     * @param event
     * @return 接收處理觸摸事件,在onTouchEvent之前調用
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e(TAG, "onTouch: view2");
        return false;
    }
}

<?xml version="1.0" encoding="utf-8"?>
<com.shenhesoft.myapplication.MyLayout1 xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.shenhesoft.myapplication.MyLayout2
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="@color/colorPrimary">

        <com.shenhesoft.myapplication.MyView1
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:background="#000" />

        <com.shenhesoft.myapplication.MyView2
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@color/colorAccent" />
    </com.shenhesoft.myapplication.MyLayout2>
</com.shenhesoft.myapplication.MyLayout1>

修改後運行程序界面如下:
這裏寫圖片描述

這次我們點擊一下MyView2,也就是最上層的View,打印的log如下:

--------------------------------------------按下------------------------------------
09-12 08:25:53.475 30457-30457/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 08:25:53.475 30457-30457/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
09-12 08:25:53.475 30457-30457/com.shenhesoft.myapplication E/MyLayout2: dispatchTouchEvent: layout2
09-12 08:25:53.475 30457-30457/com.shenhesoft.myapplication E/MyView2: dispatchTouchEvent: view2
    onTouch: view2
    onTouchEvent: view2
---------------------------------------------鬆開-----------------------------------
09-12 08:25:56.346 30457-30457/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 08:25:56.347 30457-30457/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
09-12 08:25:56.347 30457-30457/com.shenhesoft.myapplication E/MyLayout2: dispatchTouchEvent: layout2
09-12 08:25:56.347 30457-30457/com.shenhesoft.myapplication E/MyView2: dispatchTouchEvent: view2
    onTouch: view2
    onTouchEvent: view2

和之前的對比多了一些東西,首先在調用MyLayout1的dispatchTouchEvent後調用的是MyLayout2的dispatchTouchEvent,其次這裏只調用了MyView2的dispatchTouchEvent方法,爲什麼呢?還記得ViewGroup的dispatchTouchEvent方法中循環遍歷子View的dispatchTouchEvent方法時是倒序遍歷的嗎,也就是首先調用的最外層的View,前面說了我們的View繼承的是Button會消費掉事件,所以事件就不會傳到MyView1。

如果MyView2不攔截事件會怎麼樣呢?

public class MyView2 extends Button implements View.OnTouchListener {
    private static final String TAG = "MyView2";

    public MyView2(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnTouchListener(this);
    }

    /**
     * @param event
     * @return 接收處理觸摸事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: view2");
//        return super.onTouchEvent(event);
        //強制不攔截
        return false;
    }

    /**
     * @return 分發觸摸事件
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent: view2");
        return super.dispatchTouchEvent(event);
    }

    /**
     * @param v
     * @param event
     * @return 接收處理觸摸事件,在onTouchEvent之前調用
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e(TAG, "onTouch: view2");
        return false;
    }
}
--------------------------------------------按下------------------------------------
09-12 08:48:55.451 32160-32160/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 08:48:55.451 32160-32160/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
09-12 08:48:55.451 32160-32160/com.shenhesoft.myapplication E/MyLayout2: dispatchTouchEvent: layout2
09-12 08:48:55.451 32160-32160/com.shenhesoft.myapplication E/MyView2: dispatchTouchEvent: view2
    onTouch: view2
    onTouchEvent: view2
09-12 08:48:55.451 32160-32160/com.shenhesoft.myapplication E/MyView1: dispatchTouchEvent: view1
    onTouch: view1
    onTouchEvent: view1
---------------------------------------------鬆開-----------------------------------
09-12 08:48:56.658 32160-32160/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 08:48:56.658 32160-32160/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
09-12 08:48:56.658 32160-32160/com.shenhesoft.myapplication E/MyLayout2: dispatchTouchEvent: layout2
09-12 08:48:56.658 32160-32160/com.shenhesoft.myapplication E/MyView1: dispatchTouchEvent: view1
    onTouch: view1
    onTouchEvent: view1

可以看到ACTION_DOWN事件首先到MyView2,由於MyVIew2沒有攔截所以事件傳到MyView1,MyView1攔截了所以後續的事件也傳到那裏。

如果都不攔截呢?

--------------------------------------------按下------------------------------------
09-12 08:54:26.455 32160-32199/com.shenhesoft.myapplication E/Surface: getSlotFromBufferLocked: unknown buffer: 0xae5b3dc0
09-12 08:54:27.939 32160-32160/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
09-12 08:54:27.939 32160-32160/com.shenhesoft.myapplication E/MyLayout1: dispatchTouchEvent: layout1
09-12 08:54:27.939 32160-32160/com.shenhesoft.myapplication E/MyLayout2: dispatchTouchEvent: layout2
09-12 08:54:27.939 32160-32160/com.shenhesoft.myapplication E/MyView2: dispatchTouchEvent: view2
    onTouch: view2
    onTouchEvent: view2
09-12 08:54:27.939 32160-32160/com.shenhesoft.myapplication E/MyView1: dispatchTouchEvent: view1
    onTouch: view1
    onTouchEvent: view1
09-12 08:54:27.939 32160-32160/com.shenhesoft.myapplication E/MyLayout2: onTouch: layout2
    onTouchEvent: layout2
09-12 08:54:27.939 32160-32160/com.shenhesoft.myapplication E/MyLayout1: onTouch: layout1
    onTouchEvent: layout1
09-12 08:54:27.939 32160-32160/com.shenhesoft.myapplication E/MainActivity: onTouchEvent: MainActivity
---------------------------------------------鬆開-----------------------------------
09-12 08:54:28.706 32160-32160/com.shenhesoft.myapplication E/MainActivity: dispatchTouchEvent: MainActivity
    onTouchEvent: MainActivity

這裏我就不多說了,和之前的類似。到這裏這個例子也基本上講完了,其實裏面還有一些其他的細節你們可以自己研究,下面來做一個總結吧。

一個觸摸事件從用戶觸摸屏幕開始首先傳遞到Activity,Activity通過dispatchTouchEvent方法傳遞到最上層的ViewGroup;ViewGroup通過dispatchTouchEvent方法開始分發,ViewGroup可以通過onInterceptTouchEvent方法攔截事件,如果不攔截事件就會繼續向下傳遞(通過倒序遍歷子View的dispatchTouchEvent傳遞);
如果下層是一個ViewGroup就會繼續上面的操作,如果是一個View就會調用View的dispatchTouchEvent方法,在View的dispatchTouchEvent方法中會調用onTouch和OnTouchEvent方法,他們都可以攔截事件(如果一個View的clickable爲true那麼OnTouchEvent就一定會攔截事件),onClick在OnTouchEvent的ACTION_UP事件中調用;
如果到這裏事件都沒有被攔截那麼事件又會向上傳遞(這裏的向上傳遞並不是在View中調用ViewGroup中的方法,而是View中的方法返回false就會使ViewGroup中的方法繼續向下執行),首先調用ViewGroup的onTouch和OnTouchEvent方法(其實也是在ViewGroup的dispatchTouchEvent方法中調用的),如果不攔截就會回傳到Activity的OnTouchEvent方法,這樣就結束了。

Tip:ImageView默認是不可點擊的(clickable爲false),所以ImageView的onTouchEvent默認不攔截事件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章