Android的事件分发机制源码解析

为什么需要事件分发机制

我们的Android布局中是由各种ViewGroup和View组成的,ViewGroup包含着各种View(ViewGroup也是View),这些View可能互相重叠,当我们点击手机屏幕产生了点击事件,这个事件要由该点击区域的那个View来接收响应呢?是外层的ViewGroup,还是内层的子View,这时事件分发机制的作用就体现了。

概念

在介绍点击事件的传递规则之前,我们要明白这里要分析的对象就是MotionEvent,所谓点击事件的分发就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递过程就是事件分发过程。

首先介绍与事件分发相关的三个重要方法:

public boolean dispatchTouchEvent(MotionEvent ev);  

事件分发的入口,如果事件能传递给当前View,那么此方法一定会被调用,返回结果true则表示事件被消费。

public boolean onInterceptTouchEvent(MotionEvent ev);  

判断是否拦截事件,返回true则拦截事件。

public boolean onTouchEvent(MotionEvent ev);  

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否要消耗当前事件。

注意onInterceptTouchEvent是ViewGroup中独有的,因为ViewGroup才需要对传递到其内部的View进行拦截。上述三个方法的关系可以用下面的伪代码来表示:

public boolean dispatchTouchEvent(MotionEvent ev){  
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }

    return consume;
}

事件从哪来?

在分析事件分发机制之前,我们首先需要知道,当点击事件发生了以后,最先传递到的地方是哪里?我们才好从源头来分析事件的传递。有一种传统的说法是:

Activity->Window->DecorView->View  

这里关于Activity、Window、DecorView的知识介绍不在本篇介绍,到底事件是不是先到达Activit呢? 我们这里可以做个试验:

public class DispatchTouchEventActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Thread.dumpStack();
        return super.dispatchTouchEvent(ev);
    }
}

通过调用Thread.dumpStack();将堆栈信息打印出来:

java.lang.Exception: Stack trace  
at java.lang.Thread.dumpStack(Thread.java:1376)  
at cn.xdeveloper.customviewseries.view.event.basic.example.DispatchTouchEventActivity.dispatchTouchEvent(DispatchTouchEventActivity.java:21)  
at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:71)  
at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:373)  
at android.view.View.dispatchPointerEvent(View.java:10163)  
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4434)  

由下往上,从以上堆栈信息中可以看到,在View层完全不处理事件的情况下,事件的传递过程是:

ViewRootImpl->DecorView->Activity  

站在WindowManagerService的角度上来看,Android界面是由一个个DecorView,通过WindowManager添加Window组成的,在Window添加过程中都会创建一个ViewRootImpl来对应一个DecorView,由该ViewRootImpl来负责DecorView与各种系统ManagerService的交互,包括WindowManagerService和InputManagerService。

源码分析

关于ViewRootImpl之前的事件产生和传递我们这里不做深究,毕竟涉及另一个领域的研究,我们把握好整体的框架,从DecorView开始入手,看看事件是如何分发的:

DecorView.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

这里获取了Window.Callback,如果cb不为null则调用它的dispatchTouchEvent方法,那么这个Window.Callback是什么东西呢?我们可以看看PhoneWindow创建的地方:

Activity.java

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {

        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);

        ......

在Activity的attach方法中可以看到Activity实现了Window.Callback接口,mWindow.setCallback(this);由此可以知道Callback就是Activity,回到上段代码:

DecorView.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

这里就调用了Activity的dispatchTouchEvent方法,也证明我们上文中提到的事件传递顺序。接着看看Activity中dispatchTouchEvent:

Activitay.java

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

从这里的判断可以知道如果if (getWindow().superDispatchTouchEvent(ev))返回了true,则事件被View层消费掉了,就不会调用onTouchEvent(ev)方法了。这样的设计就是为了可以在View不处理事件的情况下,可以由Activity的onTouchEvent(ev)来处理。

接着看看window.superDispatchTouchEvent(ev)方法:

PhoneWindow.java

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

这里调用了DecorView的superDispatchTouchEvent(event),我们看代码:

DecorView.java

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

在这里调用了super.dispatchTouchEvent(event);因为DecorView是frameLayout的子类,所以这里调用的是ViewGroup的dispatchTouchEvent(event);开始进入事件分发流程。 从这里可以看到事件从DecorView的dispatchTouchEvent方法绕了一圈又回到DecorView的superDispatchTouchEvent,目的就是让Activity参与进来,在事件没有View处理的时候由Activity来处理。

接下来我们看看ViewGroup的dispatchTouchEvent方法,这个方法比较长,这里分段来说明:

ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev){


            // 如果是ACTION_DOWN事件的话
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //取消和清除状态
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 判断是否拦截事件
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {

                //判断FLAG_DISALLOW_INTERCEPT是否拦截事件
                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;
            }

   ... ...
}

这段代码首先判断是否是ACTION_DOWN事件,如果是则重置和清除状态,由此可知一个事件是从ACTION_DOWN开始的,到ACTION_UP结束的(或者ACTION_CANCEL,当你按下一个View然后移动手指到该View的区域之外的时候会产生CANCEL事件)。

接着判断是否拦截事件:

if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)  

这里的mFirstTouchTarget是什么呢?从后面的代码逻辑可以知道,当事件被ViewGroup的子元素处理的时候会被赋值并指向该子元素,这里就可以得出一个结论:
当ACTION_DOWN事件被ViewGroup处理的时候,以后的ACTION_MOVE和ACTION_UP都不会再调用onInterceptTouchEvent,并且事件都会交由ViewGroup处理。
因为ACTION_DOWN的时候如果ViewGroup处理了,则mFirstTouchTarget不会被赋值,后续事件便进不来if。

接着在if内部又根据FLAG_DISALLOW_INTERCEPT来判断时候调用onInterceptTouchEvent;这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中调用,一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他事件。为什么是ACTION_DOWN之外的其他事件呢?因为前面代码中我们看到在ACTION_DOWN的时候会重置状态,包括重置FLAG_DISALLOW_INTERCEPT标记位和mFirstTouchTarget。

接着继续看下面的代码:

TouchTarget newTouchTarget = null;  
boolean alreadyDispatchedToNewTouchTarget = false;  
if (!canceled && !intercepted) {

    //这里注意只判断ACTION_DOWN的情况,ACTION_MOVE和ACTION_UP不走这里
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {


        ... ...


        // 获取该ViewGroup包含的View和ViewGroup的数目
        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            final float x = ev.getX(actionIndex);
            final float y = ev.getY(actionIndex);
            final View[] children = mChildren;

            final boolean customOrder = isChildrenDrawingOrderEnabled();
            // 然后递归遍历ViewGroup的孩子,对触摸事件进行分发。
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = customOrder ?
                        getChildDrawingOrder(childrenCount, i) : i;
                final View child = children[childIndex];

                // 判断child可以接受触摸事件,并且触摸坐标(x,y)在child的可视范围之内;
                // child可接受触摸事件:是指child的是可见的(VISIBLE);或者虽然不可见,但是位于动画状态。
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    continue;
                }

                ... ...

                // 调用dispatchTransformedTouchEvent()将触摸事件分发给child。
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    // 如果child能够接受该触摸事件,即child消费或者拦截了该触摸事件的话;
                    // 则调用addTouchTarget()将child添加到mFirstTouchTarget链表的表头,并返回表头对应的TouchTarget
                    // 同时还设置alreadyDispatchedToNewTouchTarget为true。
                    mLastTouchDownTime = ev.getDownTime();
                    mLastTouchDownIndex = childIndex;
                    mLastTouchDownX = ev.getX();
                    mLastTouchDownY = ev.getY();
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
            }
        }
    }
}

这里首先判断是否取消和拦截,接着从上到下遍历Children,如果Child可以接受触摸事件,并且触摸坐标(x,y)在child的可视范围之内,则调用dispatchTransformedTouchEvent处理事件,如果返回true则表示Child处理了该事件,并调用addTouchTarget()将child添加到mFirstTouchTarget链表的表头,并返回表头对应的TouchTarget,同时还设置alreadyDispatchedToNewTouchTarget为true。

上述代码描述了ACTION_DOWN的情况,接着进一步的对触摸事件进行分发

if (mFirstTouchTarget == null) {  
    // 没有任何View处理事件,则由ViewGroup自己处理;这里的第3个参数是null
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;


        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            //ACTION_DOWN并处理了事件后会走这里,表示不需要再处理事件

            handled = true;
        } else {

            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            //这里将事件分发到Child
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}

从这里的可以知道,如果之前ACTION_DOWN事件没有Child处理的话,mFirstTouchTarget为null,则后续ACTION_MOVE和ACTION_UP的事件也不会分发给该Child;接着我们看看dispatchTransformedTouchEvent方法是如何分发事件的:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,  
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // 检测是否需要发送ACTION_CANCEL。
    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;
    }


    //处理多点触摸的事件转换...


    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }


    transformedEvent.recycle();
    return handled;
}

这个方法里面首先判断是不是ACTION_CANCEL,接着对多点触摸的事件进行转换,最后根据Child==null来判断是自己处理事件还是分发给Child处理:

if (child == null) {  
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }

到这里ViewGroup的事件分发过程就从ViewGroup传递到View了,接下来我们分析View的dispatchTouchEvent(MotionEvent e)

View.java

public boolean dispatchTouchEvent(MotionEvent event) {

    ... ...

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    ... ...

    return result;
}

这里我可以看到,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会调用,可以OnTouchListener的优先级高于OnTouchEvent,这样做的好处是方便在外界处理点击事件。

接着我们看看onTouchEvent的实现:

View.java

    public boolean onTouchEvent(MotionEvent event) {

        ... ...

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    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:
                    mHasPerformedLongPress = false;

                    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:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

从上面的if判断可以知道,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即返回true,不管它是不是DISABLE状态。接着会根据事件的不同ACTION去处理各自的逻辑,在MOVE_UP的处理中,会触发performClick方法,如果View设置了OnClickListener,那么在performClick中会调用它的onClick方法,我们可以看看:

    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);
        return result;
    }

总结

到这里,View的事件分发机制的过程大体就已经分析完了,我们总结一下:

1.首先事件是通过ViewRootImpl传递给DecorView的dispatchTouchEvent方法,之后会回调到Activity的dispatchTouchEvent方法,最后再回到DecorView的superDispatchTouchEvent,开始ViewGroup的事件分发;

2.在ViewGroup的dispatchTouchEvent方法中,会通过onInterceptTouchEvent进行事件拦截,如果拦截了则调用ViewGroup的onTouchEvent方法自己消费事件,否则会遍历Children,逐个调用chlid的dispatchTouchEvent方法,直到事件被消费;如果没有child消费事件,则由ViewGroup的onTouchEvent处理;如果ViewGroup也不处理事件的话,最终事件会到达Activity的onTouchEvent。

3.在View的dispatchTouchEvent中,首先会判断时候有设置OnTouchListener,有的话则调用它的onTouch方法,如果onTouch方法没有返回true则交由View的onTouchEvent对事件进行处理;在ACITON_UP的时候会响应OnClick事件。

以上就是我对Android事件分发机制的理解,有不对的地方,欢迎大家指正,谢谢。