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默认不拦截事件。

发布了65 篇原创文章 · 获赞 24 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/shanshui911587154/article/details/54948224