基于源码分析 Android View 事件分发机制

基于 Android 28 源码分析java

所谓点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了之后,系统须要把这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。android

三个重要方法

首先咱们须要介绍在点击事件分发过程当中很重要的三个方法:数据结构

dispatchTouchEvent

用来进行事件的分发。若是事件可以传递给当前 View,那么此方法必定会被调用,返回结果受当前 ViewonTouchEvent 和 下级 ViewdispatchTouchEvent 方法的影响,表示是否消耗当前事件。app

onInterceptTouchEvent

dispatchTouchEvent 内部调用,用来判断是否拦截某个事件,若是当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。ide

onTouchEvent

dispatchTouchEvent 内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,若是不消耗,则在同一事件序列中,当前 View 没法再次接受到事件。函数

其实它们的关系能够用以下伪代码表示:源码分析

public boolean dispatchTouchEvent(MotionEvent ev) {

    if (onInterceptTouchEvent(ev)) {
        return onTouchEvent(ev);
    }
    
    return child.dispatchTouchEvent(ev);
}
复制代码

对于一个根 ViewGroup 来讲,点击事件产生后,首先会传递给它的 dispatchTouchEvent 方法,若是这个 ViewGrouponInterceptTouchEvent 返回为 true, 就表示它要拦截当前事件,接着事件就会交给该 ViewGrouponTouchEvent 方法去处理。若是 onInterceptTouchEvent 返回为 false,就表示它不拦截当前事件,这是当前事件就会传递给它的子元素,接着由子元素的 dispatchTouchEvent 来处理点击事件,如此反复直到事件被最终处理。post

事件分发的源码分析

当一个点击事件发生后,它的传递过程遵循以下顺序:Activity -> Window -> View, 即事件老是先传递给 ActivityActivity 再传递给 Window, 最后 Window 再传递给顶级 View。 顶级 View 接受到事件后,就会按照事件分发机制去分发事件。动画

Activity 对点击事件的分发过程

// Activity.java

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

分析上面的代码,点击事件用 MotionEvent 来表示,当一个点击操做发生时,由当前 ActivitydispatchTouchEvent 来进行事件分发,具体的工做由 Activity 内部的 Window 来完成的。若是返回 true,整个事件循环就结束了,返回 false 意味着事件没人处理,全部 ViewonTouchEvent 都返回了 false, 那么 ActivityonTouchEvent 就会被调用。ui

Window 对点击事件的分发过程

接下来看 Window 是如何将事件传递给 ViewGroup 的。看源码会发现,Window 是个抽象类,而 WindowsuperDispatchTouchEvent 方法也是个抽象方法,所以必须找到 Window 的实现类才行。经过注释能够发现 Window 的惟一实现类是 PhoneWindow,所以接下来看一下 PhoneWindow 是如何处理点击事件的。

// PhoneWindow.java

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
复制代码

PhoneWindow 将事件直接传递给了 DecorView,咱们知道经过 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0) 这种方式就能够获取到 Activity 中所设置的 View, 这个 mDecor 显然就是 getWindow().getDecorView() 返回的 View,而咱们经过 setContentView 设置的 View 是它的一个子 View。因为 DecorView 继承子 FrameLayout 且是 父 View,因此最终事件会传递给 View。从这里开始,事件已经传递到顶级 View 了,即在 Activity 中经过 setContentView 所设置的 View顶级 View 通常来讲都是 ViewGroup

顶级 View 对点击事件的分发过程

首先看 ViewGroup 对点击事件的分发过程,其主要实如今 ViewGroupdispatchTouchEvent 方法中,这个方法代码量不少,分段进行说明。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
        
            // Check for interception.
            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 或者 mFirstTouchTarget != null 这两种状况下来判断是否要拦截当前事件。ACTION_DOWN 事件容易理解,那么 mFirstTouchTarget != null 是什么意思呢? 这个从后面的代码逻辑能够看出来,当事件由 ViewGroup 的子元素成功处理时,mFristTouchTarget 就会被赋值指向子元素,那也就是说当事件是被当前 ViewGroup 拦截来处理而不交给子元素处理时,mFristTouchTarget == null ,那么当 ACTION_MOVEACTION_UP 事件到来时,因为 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 这个条件为 false ,将致使 ViewGrouponInterceptTouchEvent 不会再被调用,而且同一序列中的其余事件都会默认交给该 ViewGroup 来处理。

这里还有一种特殊状况,那就是 FLAG_DISALLOW_INTERCEPT 标记位,这个标记位是经过 requestDisallowInterceptTouchEvent 方法来设置的,通常用于子 View 中。 FLAG_DISALLOW_INTERCEPT 一旦设置后,ViewGroup 将没法拦截除了 ACTION_DOWN 之外的其余点击事件。为何是除了 ACTION_DOWN 之外的事件呢? 这是由于 ViewGroup 在分发事件时,若是是 ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将致使子 View 中设置的这个标记位无效。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
        
            // 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(); // 重置 FLAG_DISALLOW_INTERCEPT 标记位
            }

            // Check for interception.
            final boolean intercepted;
            
            ...
    }
复制代码

上面的代码中, ViewGroup 会在 ACTION_DOWN 事件到来时作重置状态的操做,而在 resetTouchState 方法中会对 FLAG_DISALLOW_INTERCEPT 进行重置,所以子 View 调用 requestDisallowInterceptTouchEvent 方法并不会影响 ViewGroupACTION_DOWN 事件的处理。

经过上面能够得出结论:当 ViewGroup 决定拦截事件后,那么后续的点击事件将会默认交给它处理而且再也不调用它的 onInterceptTouchEvent 方法。因此 onIntecepterTouchEvent 不是每次事件都会被调用的,若是咱们想提早处理全部的点击事件,要选择 dispatchTouchEvent 方法,只有这个方法能保证每次都会被调用,固然前提是事件可以传递到当前的 ViewGroup 中。

接着来看 ViewGroup 不拦截事件的时候,事件会向下分发交由它的子 View 进行处理

// ViewGroup.java

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
        
            if (!canceled && !intercepted) {

                // If the event is targeting accessibility 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--) { // 遍历 ViewGroup 的全部子元素 判断子元素是否可以接受到点击事件
                            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)) { // 实际调用的就是子元素的 dispatchTouchEvent 方法
                                // 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();
                                // mFirstTouchTarget 被赋值而且跳出 for 循环
                                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;
                    }
                }
            }
            ...
    }
复制代码

上面代码的逻辑是,首先遍历 ViewGroup 的全部子元素,而后判断子元素是否可以接受到点击事件。是否可以接受点击事件主要由两点来衡量:

  • 子元素是否在播动画
  • 点击事件的坐标是否落在子元素的区域内

若是子元素知足这两个条件,那么事件就会传递给它来处理。传递由 dispatchTransformedTouchEvent 方法来完成

// ViewGroup.java

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

        ...

        // Perform any necessary transformations and dispatch.
        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);
        }

        ...
        
        return handled
    }
复制代码

能够发现若是 child 传递的不是 null,它会直接调用子元素的 dispatchTouchEvent 方法,这样事件就交由子元素处理了,从而完成了一轮事件的分发。

若是子元素的 dispatchTouchEvent 返回 true,那么上文提到的 mFirstTouchTarget 就会被赋值同时跳出 for 循环,mFirstTouchTarget 真正的赋值过程是由 addTouchTarget 函数完成的。

// ViewGroup.java

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
复制代码

经过代码能够看出, mFirstTouchTarget 是一种单链表数据结构。mFirstTouchTarget 是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略,若是 mFirstTouchTargetnull,那么 ViewGroup 就默认拦截接下来同一序列中全部的点击事件,这点上文已经分析过。

若是遍历全部的子元素后事件都没有被合适的处理,这包含两种状况:

  1. ViewGroup 没有子元素
  2. 子元素处理了点击事件,可是在 dispatchTouchEvent 中返回了 false,这通常是由于子元素在 onTouchEvent 中返回了 false

在以上两种状况下, ViewGroup 会本身处理点击事件

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
        
            // 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 中传入的 childnull,从签名的分析能够知道,它会调用 super.dispatchTouchEvent(event),很显然,这里就转到了 ViewdispatchTouchEvent 方法中,即点击事件开始交由 View 来处理。

View 对点击事件的处理过程

// 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;
    }
复制代码

View 对点击事件的处理过程就比较简单了,由于 View (不包含 ViewGroup)是一个单独的元素,它没有子元素所以没法向下传递事件,因此只能本身处理事件。上面的源码能够看出 View 首先会判断有没有设置 onTouchListener,若是 onTouchListener 中的 onTouchListener 方法返回 true ,那么 onTouchEvent 就不会被调用,可见 onTouchListener 的优先级高于 onTouchEvent,这样作的好处是方便在外界处理点击事件。

// View.java

    public boolean onTouchEvent(MotionEvent event) {
        ...

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) { // 不可用状态下的 View 照样会消耗点击事件
            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;
        }
        
        ...
        
        // 只要 View 的 CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE 和 TOOLTIP 有一个为 true 就会消耗这个事件
        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)) {
                                    performClickInternal();
                                }
                            }
                        }

                        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;

                ...
            }

            return true;
        }

        return false;
    }
复制代码

上面代码中,只要 ViewCLICKABLELONG_CLICKABLECONTEXT_CLICKABLETOOLTIP 有一个为 true 就会消耗这个事件。 即 onTouchEvent 方法返回 true,无论它是否是 DISABLE 状态。而后就是当 ACTION_UP 事件发生时,会触发 performClickInternal 方法,最终调用 performClick 方法。

// View.java

    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;
    }
复制代码

上述代码可知,若是 View 设置了 OnClickListener 那么 performClick 方法内部就会调用它的 onClick 方法

总结

  1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程当中所产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束
  2. 某个 View 一旦决定拦截,那么这一个事件序列都只能又它来处理(若是事件序列能够传递给它的话),而且它的 onIntercepetTouchEvent 不会再被调用。这条也很好理解,就是说当一个 View 决定拦截一个事件后,那么系统会把同一个事件序列内的其余方法都直接交给它来处理,所以就不用再调用这个 ViewonIntercepterTouchEvent 去询问它是否要拦截了
  3. 正常状况下,一个事件序列只能被一个 View 拦截且消耗。这一条的缘由能够参考上一条,由于一旦一个元素拦截了此事件,那么同一个事件序列内的其余事件都会交由它来处理,所以同一个事件序列不可能交由两个 View 同时来处理,可是经过特殊手段能够作到,好比一个 View 将本该本身处理的事件经过 onTouchEvent 强行传递给其余 View 处理。
  4. 某个 View 一旦开始处理事件,若是它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列中的其余事件都不会再交给它来处理,而且事件将从新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。意思就是事件一旦交给一个 View 处理,那么它就必须消耗掉,不然同一事件序列中剩下的事件就再也不交给它来处理了。
  5. 若是 View 不消耗除 ACTION_DOWN 之外的其余事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,而且当前 View 能够持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理
  6. ViewGroup 默认不拦截任何事件,Android 源码中 ViewGrouponInterceptTouchEvent 方法默认返回 false
  7. View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用
  8. ViewonTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击的(clickable 和 longClickable 同时为 false)。ViewlongClickable 属性默认都为 falseclickable 属性要分状况,好比 Buttonclickable 属性默认为 true,而 TextViewclickable 属性默认为 false
  9. Viewenable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 Viewdisable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true
  10. onClick 会发生的前提是当前 View 是可点击的,而且它收到了 downup 的事件
  11. 事件传递过程是由外向内的,即事件老是先传递给父元素,而后再由父元素发给子 View, 经过 requestDisallowInteceptTouchEvent 方法能够在子元素中干预父元素的事件分发过程,可是 ACTION_DOWN 事件除外
  12. View 设置的 OnTouchListener,其优先级比 onTouchEvent 要高,若是 OnTouchListeneronTouch 方法的回调返回 true 那么 onTouchEvent 方法将不会被调用。若是返回 false,则当前 ViewonTouchEvent 方法被回调。

参考

相关文章
相关标签/搜索