Android的事件分发笔记(结论+图+源码)

版权声明:出于感谢(如果有收获)或对知识的尊重,未经允许禁止转载 https://blog.csdn.net/bendan50/article/details/85704645

参考文献:

https://www.jianshu.com/p/e6ceb7f767d8

https://www.jianshu.com/p/d3758eef1f72

https://www.cnblogs.com/linjzong/p/4191891.html

https://www.jianshu.com/p/8236278676fe

https://www.jianshu.com/p/1378b334ee85

自篇仅作为学习笔记的总结,

1、Android的事件分发是指用户在手机屏幕上产生的交互事件的传递,比如点击(包含按下down和抬起up)、移动(move)等。而在Android系统中使用类MotionEvent表示这些产生的交互事件。

2、需要分发的事件(对象)有了,接下来就是做事件分发的主体了,即谁来进行MotionEvent的事件分发,并分发(传递)给谁?

要回答这个问题,就涉及了三个主要的类及其各自相应的方法:

扫描二维码关注公众号,回复: 4966006 查看本文章

三个类:Activity     ViewGroup       View

方法:public boolean dispatchTouchEvent(MotionEvent ev);

           public boolean onInterceptTouchEvent(MotionEvent ev);

           public boolean onTouchEvent(MotionEvent event);

ViewGroup包含了上述三个函数,Activity和View不包含onInterceptTouchEvent方法。单从函数名称可以推断各自所执行的功能:分发事件、拦截事件、处理事件。

现在需要关注两个问题:一、三个类的七个相关函数执行的先后顺序;二、其返回值所代表的意思。

对于问题一,三个类的七个相关函数不一定会在一次交互事件中全部执行到,是否会执行,这取决于上一个执行函数的返回值。所以,对于问题二,可以提前给出结论:返回Ture表示MotionEvent事件被当前类(准确来说是Activity或视图)消费掉,不再向下传递分发;返回False表示MotionEvent事件目前还没有被消费(传递过来的路上没有被消费,而且当前的Activity或视图也不消费它),那么MotionEvent事件将继续传递分发,按照问题一的结果(执行先后顺序)继续。

问题再次回归:七个相关函数的执行顺序!

下面引用参数文献中的示例图进行说明:

根据这个图再来谈顺序就显得简单了许多:ViewGroup对事件进行分发,也进行拦截(当然可选事件类型,如MotionEvent.ACTION_DOWN或者MotionEvent.ACTION_UP等),子View对事件进行分发(其实就是给它自己),多然后执行事件处理;如果不能处理(及不能消费掉该事件,准确来说,该事件不应该由该子View处理),其返回值必为False。此时由ViewGroup接管,执行ViewGroup的事件处理函数(onTouchEvent函数)。

现在执行顺序就很清晰了。最先执行的是Activity的dispatchTouchEvent方法,最后执行的是Activity的onTouchEvent方法,中间穿插着ViewGroup和View的相关方法。而这中间的顺序,正如上段文字描述的那样,由ViewGroup分发,并做拦截,如果ViewGroup进行了拦截,则没有子View什么事,直接执行ViewGroup的onTouchEvent方法;如果ViewGroup没有拦截,那么事件继续向下传递。然后由其子View分发并做处理,如果子View不消费,就要由ViewGroup来处理,执行onTouchEvent方法(儿子收拾不了的烂摊子,当然要由父母来处理)。【这里引用父子比喻是指容器的关系,而非继承关系(ViewGroup继承View)】

3、再谈下MotionEvent的事件。触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。这里补充下其实UP事件是可能为0个的。

基本上文字的结论性描述可以告一段落了。作为文字结论的结尾,再引用一下参考文献里给出的图,然后再谈下源码。

上图可以作为文字结论的图形说明。

得出这些结论,是基于对源码的分析,再展开源码分析之前,我在这里先记录下查看源码的小插曲——Android Studio没法查看源码的解释方案。

当我想看源码,发现跟进去显示的是.class文件。百度了方案,可以概括为以下几点:

首先,检查android sdk目录下的sources文件夹是否包含相应API的文件。其次,核对C:\Users\用户名\.AndroidStudio3.0\config\options目录下jdk.table.xml文件下对应的内容,是否指向了对应的<root><sources></sources></root>。然后打开Android Studio的SDK设置,进行安装关联,如图:

基本上就可以解决了。最后开启源码之旅。


既然提到了Activity,那么就先从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);
    }

当MotionEvent对象的值是ACTION_DOWN时,会执行onUserInteraction();方法,该方法是一个空的方法,没有任何实现。参考文献中指出:该函数的作用是屏保,当此 Activity 在栈顶的时候,触屏点击 Home、Back、Recent 键等都会触发这个方法。那么接下来的重点就是两个不同的Return返回值。这两个返回值正常反映了上面的文字结论描述:最先执行Activity的分发函数,最后执行Activity的onTouchEvent函数。

当我们跟着if语句追下去时,就到了ViewGroup的dispatchTouchEvent()方法。而这个过程有必要提及一下(我只看了第一篇参考文献时,并不清楚为什么会就到了ViewGroup的dispatchTouchEvent()方法)。

getWindow获得的是Window类(android.view.Window)对象,该类的定义是一个虚拟类,superDispatchTouchEvent方法也是一个虚拟方法,我们现在找到它的实现。看下Window类的源码:

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
...
}

上段截的代码,只有一句有价值(对于找到方法的实现):存在唯一一个实现类PhoneWindow。自然,我们可以找到这个类,也就找到了该类对superDispatchTouchEvent方法的实现。

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

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

显然,我们又需要找DecorView的相应方法了。DecorView是当前界面的底层容器,它的方法是调用了super.dispatchTouchEvent方法。这里使用了super,自然要去它的父类去找,最终指向了ViewGroup。

下面就是两根难啃的骨头:ViewGroup的三个函数,以及View的两个函数。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

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

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

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

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

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

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // 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);
            } 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) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        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;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

长长的代码,可以进行拆分:最前面的二句if语句可以忽略掉(我所阅读的参考文献都没有提到这些语句的作用,我记下自己的理解,可以存在偏差)。mInputEventConsistencyVerifier变量是ViewGroup从View继承过来的,而在View中定义上面有一行注释:意思是用于调试一致性的。代码如下:

    //View.java
    /**
     * Consistency verifier for debugging purposes.
     * @hide
     */
    protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
            InputEventConsistencyVerifier.isInstrumentationEnabled() ?
                    new InputEventConsistencyVerifier(this, 0) : null;

紧接着第二个if语句其实也是不影响结果的,根据函数返回值为handled变量来看,这两句if判断语句执行时,handled变量还没定义呢。所以,许多文章对于进行了忽略。

既然,函数返回值是handled变量,那么,我们研读的方向自然是跟着handled变量的定义、使用、赋值等过程来追踪。

boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
    ...
}

这个大的IF语句通常一定会执行,因为内部才是其核心的处理流程,不执行这个IF里面的内容,那么,函数直接return刚定义了handled变量了。这个判断是一个安全过滤。当视图没有被覆盖时才会执行。(比如,正在的切换屏幕,有一部分存在遮挡之类的情况,是不执行的。)

内部的核心代码之一:

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

影响intercepted变量值的除了手动赋值外,就是我们提到的三个核心函数之一:onInterceptTouchEvent(ev);显然它并不是每次都会执行,简言之,它的执行是需要If条件的。由于onInterceptTouchEvent(ev)拦截函数代码比较简单,先提下这个函数的源码,然后再来理清它执行时所需要的条件。

/**
     * Implement this method to intercept all touch screen motion events.  This
     * allows you to watch events as they are dispatched to your children, and
     * take ownership of the current gesture at any point.
     *
     * <p>Using this function takes some care, as it has a fairly complicated
     * interaction with {@link View#onTouchEvent(MotionEvent)
     * View.onTouchEvent(MotionEvent)}, and using it requires implementing
     * that method as well as this one in the correct way.  Events will be
     * received in the following order:
     *
     * <ol>
     * <li> You will receive the down event here.
     * <li> The down event will be handled either by a child of this view
     * group, or given to your own onTouchEvent() method to handle; this means
     * you should implement onTouchEvent() to return true, so you will
     * continue to see the rest of the gesture (instead of looking for
     * a parent view to handle it).  Also, by returning true from
     * onTouchEvent(), you will not receive any following
     * events in onInterceptTouchEvent() and all touch processing must
     * happen in onTouchEvent() like normal.
     * <li> For as long as you return false from this function, each following
     * event (up to and including the final up) will be delivered first here
     * and then to the target's onTouchEvent().
     * <li> If you return true from here, you will not receive any
     * following events: the target view will receive the same event but
     * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
     * events will be delivered to your onTouchEvent() method and no longer
     * appear here.
     * </ol>
     *
     * @param ev The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
    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;
    }

长长的注释,而代码就两行(注释不翻译了)。这也是许多文章中提到,如果不重写,它的返回值在绝大多数都为false,即ViewGroup不进行拦截,后续执行子View的分发、处理事件的两个函数。(得出这一结论,当然也是基于源码,后面会分析intercepted变量的使用)

现在来清理执行它所需要的条件。一个条件判断if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)和一个disallowIntercept变量。

条件判断:

MotionEvent.ACTION_DOWN事件已经说明了,是Event的状态之一(还有move/up之类的);mFirstTouchTarget变量不为空,那么它代表着什么呢?分支代码(及不是事件分发主流程的代码)表示,当ViewGroup的点击事件被子View消费,mFirstTouchTarget变量就会指向该子View,此时值不为空。为什么要做这个判断?感觉还是有必要再提一下,核心代码上面的几行代码,如下:

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

其做的处理是,当为Down时,清空mFirstTouchTarget变量。因为Down是一个触摸事件的开始,所以对于新的MotionEvent事件要初始化一下它的状态。由这两行代码,我们就可以推测出IF判断的作用了,即回答为什么要做这个判断

如果是Down事件,那ViewGroup可能需要判断是否要拦截;如果不是Down事件,但是又想拦截它(比如是Move事件),那么if分支依然可能走到(如果消费了Down事件的子View存在,则此时mFirstTouchTarget变量在非空)。而如果子View没有消费Down事件,那么这一次的事件序列(如Move事件),子View也肯定不能消费,所以没必要向子View传递,直接返回True。

disallowIntercept变量:

需要关注一个标志位:FLAG_DISALLOW_INTERCEPT,禁止ViewGroup拦截除了DOWN之外的事件。一般通过子View的requestDisallowInterceptTouchEvent来设置。

如果没有被禁止拦截,当然是要做拦截处理的(即执行onInterceptTouchEvent函数)。

至此,内部核心代码之一分析完成。引出内部核心代码之二的就是前面提到的intercepted变量。

if (!canceled && !intercepted) {
   ...
}

没有被取消且没有被拦截,ViewGroup的dispatchTouchEvent函数执行的是子View的遍历。

想了下,子View遍历展开的话,文章有点长了,正好结尾,另起开头。

ViewGroup和View的源码见另一篇。

猜你喜欢

转载自blog.csdn.net/bendan50/article/details/85704645
今日推荐