Android 事件分发源码解析

事件体系简介

既然是View的事件分发,那么首先我们应该知道什么是Android体系中的事件,它在code世界中又是如何定义的,下面我们简单介绍一些MotionEvent的知识。

MotionEvent

Android体系中将所有的输入事件都放到了MotionEvent中,实际上MotionEvent已经发展到十分复杂,由于本文的重点不在这里,所以就单点触控事件进行介绍。

单点触控

单点触控中有以下几个事件:

  • ACTION_DOWN 手指初次接触到屏幕上时触发,简单地说就是手指按下触摸到屏幕的时候
  • ACTION_UP 手指离开屏幕时触发,及手指抬起离开屏幕时
  • ACTION_MOVE 手指在屏幕上滑动时触发,当手指在屏幕上连续一段距离后被识别为滑动,滑动在一次事件序列中可能会被触发多次,并不唯一
  • ACTION_CANCEL 当事件传递下来后又被上层收回时触发
  • ACTION_OUTSIDE 手指触控的区域不在控件上方时触发

一次单点触控流程可以大概描述为手指按下(ACTION_DOWN)——>手指在屏幕上滑动(ACTION_MOVE)——>最后手指离开屏幕(ACTION_UP),这是用户一般使用的简化过程。了解过这些基础事件后,我们再看几个MotionEvent常用API:

  • getAction() 获取上述事件类型
  • getX() 获取触摸点相对于View的X轴坐标
  • getY() 获取触摸点相对于View的Y轴坐标
  • getRewX() 获取触摸点相对屏幕的X轴坐标
  • getRewY() 获取触摸点相对屏幕的Y轴坐标

上面介绍的知识中首先要强调的是ACTION_MOVE在一次事件流程中可能触发的次数不唯一,其次就要介绍一下比较冷门的两个事件ACTION_CANCEL,ACTION_OUTSIDE。

  • ACTION_CANCEL

ACTION_CANCEL顾名思义取消事件,既然叫做取消那么就肯定存在先获取然后被收回这样一个过程,举个例子:你收到了一个会议消息,然后你又被告知这个会议被取消了;对应于View事件分发就是指事件序列分发到了某个环节,但他的上层又收回了这个事件序列,这时这个环节就会收到上层给他的ACTION_CANCEL事件.

  • ACTION_OUTSIDE

对于这个事件可能大家会觉得不可思议,明明触摸事件的发生点都不在该控件的所在区域,看起来不相关的两者是如何联系到一起的呢。实际上这种情况的确存在而且也具有它的意义。比如我们常用的Dialog,是不是会有这样的情况,我们点击它本身的关闭或取消按钮,而是点了一下它周围的其他区域,Dialog依然能够关闭;这种情况很多弹窗上出现的很多,就是这个ACTION_OUTSIDE事件的作用。

对于Dialog是如何收到这个ACTION_OUTSIDE事件的,这主要是通过WindowManager的布局参数的flags设置为FLAG_WATCH_OUTSIDE_TOUCH,这样Daialog就具有了观察并处理超出本身范围的事件的能力

正文

Android事件分发从当前Activity开始,在到该Activity持有的Window,又由Window将事件传递给DecorView,最后又由DecorView再将事件分发给子View;如果子View并不消费该事件,那么该事件序列又会被交给该View的父容器,再到DecorView,再到Window,最后又回到了Activity。这意味着如果一直没有View消费该事件序列,那么事件会回到Activity并被处理掉。

当不考虑事件序列中途被拦截并消费,只考虑事件分发到View的情况,比如一个Button或者一个TextView。若该View并没有消费该事件的话,那么最多就会出现上述流程

Activity的事件分发

Activity的事件分发在其dispatchTouchEvent方法里进行了处理,我们看源码:

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

这里我们可以看到Activity直接将事件交给了Window处理,如果Window的superDispatchTouchEvent方法返回false,那么Activity就会调用他的onTouchEvent方法,这个里如何具体实现我们后面再看。那么事件已经到了Window了。

Window的事件分发

我们知道Window是抽象类,唯一实现类是PhoneWindow,又因为该superDispatchTouchEvent是抽象方法,那么我们只有去PhoneWindow里去看该方法的实现,看源码:

public boolean superDispatchTouchEvent(MotionEvent ev){
    return mDecor.dispatchTouchEvent(ev);
}

相比于Activity将事件传到Window来说,Window的做法则要更加简单粗暴,这里直接传递到了DecorView,而这个DecorView就是我们一般说的顶级View,我们在layout包里写的xml文件就是它的子View。一般来说这个DecorView都是一个ViewGroup,所以事件序列来到了ViewGroup,我们继续看。

ViewGroup的事件分发

ViewGroup大家应该已经比熟悉了,一般来说ViewGroup的很多逻辑
处理都跟其自身和其子View息息相关,就像View的measure、draw和layout方法类似,这里也很相似,首先我们来看看几个比较重要的方法:

public boolean dispatchTouchEvent(MotionEvent ev)

用于事件的分发。当事件传递到了该View后,该方法一定会被调用,上面关于Activity等的事件分发时,大家应该都看到了这个方法。该方法的返回值由其自身的onTouchEvent()方法和子View的dispatchTouchEvent方法共同决定。

public boolean onInterceptTouchEvent(MotionEvent ev)

此方法会在上述方法内部被调用(View为ViewGroup时),具体的View不会调用该方法。此方法用于判断是否拦截某个事件,返回结果表示是否拦截当前事件。当同一个事件序列中的某个事件被拦截后,此方法不会被再次调用。

public boolean onTouchEvent(MotionEvent ev)

用于处理点击事件,返回结果用于判断是否消费事件,如果不消费,那么此view不会再收到该事件序列的其他事件。

下面我们结合ViewGroup的源码来分析它的事件分发机制,首先看dispatchTouchEvent方法,源码内容很长,我们一段一段的分析:

 boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down. 注释1
            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.注释2
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // 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.注释3
                intercepted = true;
            }

这里定义了一个名为handled的布尔值,根据命名我们可以猜测为是否处理的,显然事件还没有被处理,这也跟我们说的viewGroup的dispatchEvent方法默认不拦截事件相应证,然后再看注释1处,如果当前事件是ACTION_DOWN(下文简称down事件)的话,会调用两个方法,又看命名,第一个方法应该是清除并取消触摸的目标,第二个方法应该是重置触摸状态,那么到底与我们的猜测是否一致呢,通过查看这两个方法的源码和注释,与我们的猜测完全一致:

/**
     * Cancels and clears all touch targets.
     * 取消并清除所有触摸目标
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
    //mFirstTouchTarget 一个成员变量,初始为null,如果事件被子view处理完后
    //这个值将被赋值并指向子view,
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }
            //遍历单链表
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            //清除触摸目标 进行了单链表的删除操作
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }

再看另一个方法:

/**
     * Resets all touch state in preparation for a new cycle.
     * 重置所有状态以进入新周期
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

再次回,到dispatchTouchEvent方法上来,看注释2处又做了什么,这里有定义了一个布尔值,表示是否拦截事件,然后的判断内容是如果事件down或者mFirstTouchTarget不为空的话则执行onInterceptTouchEvent方法,是不是down事件很好判断,前面说到mFirstTouchTarget在子view处理了事件后会被赋值,而想要交给viewgroup处理时,显然它等于null。所以onInterceptTouchEvent方法只会执行一次在这里就可以看出,并且当down事件被拦截后,之后的整个事件序列都会交给他处理(注释3)。但是在调用onIntercepTouchEvent方法之前还有一个叫disallowIntercept的布尔值,用于判断是否禁止拦截,主要受FLAG_DISALLOW_INTERCEPT这个标志位的影响,因为这个标志位子view可以通过一定方法来改变,使viewgroup不拦截该事件并将它传递到子view上,这听起来似乎可以用来解决滑动冲突,但是这得要求这个事件不是down事件,上文已经分析过了,因为此时会进行一系列重置工作,包括标记位。如果down事件并没有拦截呢,那么这个时候就要将事件传递给子View了(这可能会有点长):

                    //获得所有子view的数量
                    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.
                        //创建了一个用于向子view分发touch事件的list
                        final ArrayList preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //遍历子view
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //获取子view的脚标
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            //通过脚标拿到子view对象
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                            //如果view在播放动画或者点击事件的坐标不在该view区域内 则遍历下一个 
                            //否则交由它处理
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            //从单链表中拿到这个值为child的TouchTarget对象
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            //调用dispatchTransformedTouchEvent方法,用于判断子view是否拦截事件
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                //记录时间
                                mLastTouchDownTime = ev.getDownTime();
                                //记录子view的脚标
                                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();
                                //为新触摸目标赋值 将child添加进链表并返回值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //已经分发给新的触摸目标设置为true
                                alreadyDispatchedToNewTouchTarget = true;
                                //跳出循环,因为处理事件的view已经有了
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        //清空存放view的list
                        if (preorderedList != null) preorderedList.clear();
                    }

上面这一大片代码主要完成了遍历子View,然后判断子view是否能够接受到事件,如果能则将事件分发给子view,如果子view拦截该事件,就记录下它然后设置为newToucthTarget。判断子view是否拦截是dispatchTransformedTouchEvent这个方法来完成的,我们看看是怎么实现的:

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

这里只截取了最主要的一段代码,可以看到如果子view不为空,就调用子view的dispatchTouchEvent方法,否则调用父类View的dispatchTouchEvent方法,最后返回结果。如果子view拦截了事件返回true,那么viewgroup就会记录下它,并为newTouchTarget赋值,然后跳出循环不再遍历,这一点参见上面代码中的注释。到这里还会出现另一种情况,那就是子View的dispatchTouchEvent都返回了false,整个遍历过程都没有找到view来处理事件怎么办呢,显然这时候只能viewgroup自己来处理事件了,看看它是如何处理的:

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
    TouchTarget.ALL_POINTER_IDS);
}

这里又调用了dispatchTransformedTouchEvent方法,需要注意的是传入的child参数为null,根据上面的分析,这里会将事件交给View来处理,这就是Viewgroup的处理逻辑,下面再看view的事件处理过程是怎样的。

View的事件分发

view相比ViewGroup没有onInterceptTouchEvent方法,一点有事件传递到它,那么它的onTouchEvent方法就一定会被调用。view的onTouchEvent方法默认会消费事件,除非它是不可点击的,clickable和longClickable同时为false,所有view的longClickable默认都为false,,clickable就各不相同了,比如Button的clickable默认为true,TextView默认为false;但是当我们为TextView设置点击事件监听的时候,它依然能够消费事件,这主要是因为setOnClickListener的时候,会将clickable设置为true。下面来看看view的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;

        ...
        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不为null,并且是可以点击的则执行它的onTouch方法,如果onTouch返回true,那么事件处理就结束了,就不会执行下面的onTouchEvent方法,显然onTouchListener优先级高于onTouchEvent。然后我们再看onTouchEvent:

 public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {//注释1
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        //注释2
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
}

看到注释1处,当view处于不可用状态下,依然能处理点击事件,就像源码注释写的那样,这些view并不会响应,但依然消费了触摸事件。注释2处是view的代理来处理点击事件,处理机制与OnTouchListener相似,就不分析了。现在来看看在onTouchEvent中对具体的事件是如何处理的:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    //如果根本就是不可点击 那么设置的相关callback都会被移除
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                       ...

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 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)) {
                                    performClickInternal();
                                }
                            }
                        }
                        ...
                    }
                    ...
                    break;
    }            
    ...
    return true;
}

从上面的代码可以看出只要CLICKABLE和LONG_CLICKABLE有一个为true,即为可点击状态,那么它就会消费这个事件。点击事件的触发在action_up这个事件,这个事件里可以看出主要调用了performClickInternal方法,其实这个方法也主要是调用的performClick方法,如果view设置了onClickListener,那么就会调用它的onClick方法:

private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }

 public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

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

一开始我们就说过,在设置clickListener的时候,clickable会被设置为true,所以当我们为View设置OnClickListener或OnClickListener的时候,只要事件传递到它这里来,那么就会执行我们设置的逻辑。到这里View的事件分发也就讲完了,对事件分发的清楚认识对于滑动冲突的处理和自定义view都有极大的帮助。

你可能感兴趣的:(Android 事件分发源码解析)