从源码分析MotionEvent分发机制

380 阅读9分钟

阅读提示

  开篇强调,事件手势动作(点击、滑动等)是有区别,例如:一个滑动事件以ACTION_DOWN事件开始,经历多个ACTION_MOVE事件,最后以ACTION_UP事件结束。而分发机制讲述的主体是事件,没这点的意识的话,理解起来估计就没完没了。还有要理解super关键字并不是父ViewGroup(大多时候ViewGroup中的super很有可能指的就是View这个java类)。

关键函数

说到点击事件的分发怎么能不提到这三个函数:

  public boolean dispatchTouchEvent(MotionEvent ev)

  public boolean onInterceptTouchEvent(MotionEvent ev)

  public boolean onTouchEvent(MotionEvent ev)

主体内容

dispatchTouchEvent来源

  其中最难理解的也就是dispatchTouchEvent了,而且dispachTouchEvent描述了整个事件的分发与处理流程。

  当点击屏幕时,事件最先传递给ActivitydispatchTouchEvent(MotionEvent ev),下面我们开始分析源码:

  源码: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);
    }


    ......
    /**
     * Called whenever a key, touch, or trackball event is dispatched to the
     * activity.  Implement this method if you wish to know that the user has
     * interacted with the device in some way while your activity is running.
     * This callback and {@link #onUserLeaveHint} are intended to help
     * activities manage status bar notifications intelligently; specifically,
     * for helping activities determine the proper time to cancel a notfication.
     *
     * <p>All calls to your activity's {@link #onUserLeaveHint} callback will
     * be accompanied by calls to {@link #onUserInteraction}.  This
     * ensures that your activity will be told of relevant user activity such
     * as pulling down the notification pane and touching an item there.
     *
     * <p>Note that this callback will be invoked for the touch down action
     * that begins a touch gesture, but may not be invoked for the touch-moved
     * and touch-up actions that follow.
     *
     * @see #onUserLeaveHint()
     */
    public void onUserInteraction() {
    }

  显然,从内容上看onUserInteraction();并不影响事件的传递,它只是可以在一个新手势开始(ACTION_DOWN事件发生)时,在事件传递开始前做最优先的事情。

  下面会执行`getWindow().superDispatchTouchEvent(ev)`操作,那么`getWindow()`又是啥?

    ......
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    ......

     /**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
    public Window getWindow() {
        return mWindow;
    }

再进入AndroidXRef,找到PhoneWindow.java:

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

咋样?是不是想到了传说种的Activity->PhoneWindow->DecorView。且慢,我们的目的时“事件分发机制”。那么重新找到DecorView.java:

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

  而DecorView extends FrameLayout,现在知道为啥有的文章说“所有的布局都在一个FrameLayout里面”了吧?其实就是DecorView,FrameLayout extends ViewGroup,简单梳理就会知道这个dispatchTouchEvent(event)就是在ViewGroup种实现的。

  最终还是看ViewGroup怎么实现这个dispatchTouchEvent(event)ViewGroup#dispatchTouchEvent(MotionEvent ev),最终返回true的话,Activity#dispatchTouchEvent(MotionEvent ev)函数就直接返回了,Activity#onTouchEvent(ev)也不会被调用,如果最终返回为false的话则Activity#onTouchEvent(ev)执行,为什么要加“最终”二字。请听我娓娓道来,前方高能。

源码解析

dispatchTouchEvent源码

  没错,源码(为了便于理解,这段源码有一点点的删除,阅读的时候可直接跳过这一段源码,因为我会在后面具体分析):ViewGroup#dispatchTouchEvent(MotionEvent ev):


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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            //第一段-----------------------------------------------------------START
            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;
            }
            //第一段-----------------------------------------------------------END
            //第二段-----------------------------------------------------------START
            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();
                    }
                }
            }
            //第二段-----------------------------------------------------------END
            //第三段-----------------------------------------------------------START
            // 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;
                        }
                        
                    }
                    predecessor = target;
                    target = next;
                }
            }
            //第三段-----------------------------------------------------------END
        return handled;
    }

年轻人,我知道你不想看源码,我也很无奈。

第一段

    if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

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

  从备注与函数名上理解,开始只是做一些重置初始工作(99.9%情况下,我们要对Google的工程师足够的相信,不然你会越陷越深)。

  然后,会在判断里面看到onInterceptTouchEvent(ev),嗯,终于看到第二个函数了。那么什么情况下会调用这个函数呢?disallowIntercept从字面和后面的赋值理解,这是通过mGroupFlags字段,不允许ViewGroup拦截,通过语句判断如果ACTION_DOWN不予许拦截成立的话(!disallowIntercept == false),所以intercepted = false,(分析时避免情况复杂化,我们要猜一些变量或者函数的意义,比如:!canceled应该就时动作没被取消,正常情况下该条件是成立的)所以,会进入执行第二段。

第二段

  接下来分析一下这个"mChildrenCount":

 // Child views of this ViewGroup
    private View[] mChildren;
    // Number of valid children in the mChildren array, the rest should be null or not
    // considered as children
    private int mChildrenCount;

  现在,大概可以看出什么意思了吧?mChildren是指ViewGroup里面的所有子View,而mChildrenCount指的是能所有有效的ViewGroup里子View的数量,好了,我们接着执行第二段。可以看出,如果没有有效的子View,则会直接进入第三段。如果有有效的子View,且满足传递的要求(这一点可以通过备注看出,各种函数名也表面),则会在该子View执行dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign),再看看这个函数名:


 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits){
                //各种判断各种处理以后都会成为这种结构
                if (child == null) {
                        handled = super.dispatchTouchEvent(event);
                    } else {
                        handled = child.dispatchTouchEvent(event);
                    }
                    return hanled;
            }

啥意思?子View不为空,则子View继续分发事件,否则执行super(赌一包辣条,这里的super就是View)的dispatchTouchEvent,所以接下来看看ViewdispatchTouchEvent函数: 源码:View#dispatchTouchEvent

/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {

        if (onFilterTouchEventForSecurity(event)) {
            //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;
    }

  熟悉不?你有没有OnTouchListener啊,要是有的话,你onTouch返回了啥东东啊?给我看看,我好决定怎么处理onTouchEvent,这里就清楚onTouchonTouchEvent了吧!!!关于传递--->处理的流程就到这里了。接下来看看返回值对后续的过程影响。

  很明显,onTouchonTouchEvent任意一个返回true(从代码可见得:onTouch返回true后,onTouchEvent是没有机会执行的),都将成为其所在的ViewGroupdispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)的返回值,如果返回false则,会使得mFirstTouchTarget == null,直接执行第三段的handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);。明显进入[onTouch], onTouchEvent过程handle的返回值将直接依赖这两个(或没有设置onTouch监听就是一个)的返回值,从这里我们知道父ViewGroup中的[onTouch], onTouchEvent也调用了。如果返回true,将会走进addTouchTarget(child, idBitsToAssign):   源码:ViewGroup#addTouchTarget

  /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

第三段

   mFirstTouchTarget == null 时,上面已经分析。
   但是在上述条件不成立的情况下这里mFirstTouchTarget被赋值后。这里有一段不是很理解,还请各位指教,但是能确定handle = true成为必然。

    // 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;
            }
            
        }
        predecessor = target;
        target = next;
    }

  大致流程就是这样分析的,未完待续。。。。。。

小结

  这篇文章是看完《Android开发艺术探索》后自己跟着源码一步步来走的,看完书以后其实只是一个轮廓,然后自己看源码后映像深刻了些,但总是感觉不放心,编程路上一位导师曾经跟我说过:当你觉得你懂了得时候,你就该写下来。当你觉得你能写明白得时候,去找一个人然后把他教会。这时候你才是真正得懂。
  遗憾由于水平有限,连写都没能写得明白,但是写得过程中确实也有了不少得收获,但也不该只满足于此。
  有人说对于源码得追究其实不该太过钻牛角,然而还是希望尽量满足自己得好奇心,希望得到大家指导,待续。。。