View事件的分发机制

519 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第24天,点击查看活动详情

1.点击事件的传递规则

  • dispatchTouchEvent
    • 用来进行事件的分发,如果事件能够传递给当前View那么这个方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的onInterceptTouchEvent影响,表示是否消耗当前事件。
  • onInterceptTouchEvent
    • 用来判断是否拦截事件,如果当前View拦截了某个事件那么在同一个事件序列中不会再调用这个方法,返回结果表示是否拦截当前事件。
  • onTouchEvent
    • 用来处理事件,如果不消耗那么在同一个事件序列中将不会再接收到事件,返回结果表示是否消耗了当前事件
  • View的OnTouchListeneronTouchEventonClickListener三个方法中优先级关系
    • onTouchListener > onTouchEvent > onClickListener
  • View被点击后事件的传递过程
    • View被点击后传递的顺序为ActivityWindowView,也就是说事件总是先传递给Activity,然后Activity再传递给Window,然后Window再传递给顶级View,顶级View接收到事件后就会按照事件分发机制去分发事件。
  • 事件分发机制的一些结论
    • View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent一定会被调用
    • ViewonTouchEvent默认都会消耗事件的,除非它是不可点击的例如clickablelongClickable同时为false,View中的longClickable默认为false,clickable要分情况,例如Buttonclickable为true,TextViewclickable为false
    • Viewebable属性不影响onTouchEvent的默认返回值.
    • 事件的传递是由外向内的,即事件的传递总是先传递给父元素,再由父元素传递给子view,可以用requestDisallowInterceptTouchEvent方法干预父元素的事件分发过程。

2.事件分发的源码解析

  1. Activity对点击事件的分发过程

当一个点击操作发生时,事件最先传递给当前的Activity,由Activity的dispatchTouchEvent进行事件的分发

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

根据dispatchTouchEvent源码可知事件开始先交给Activity所附属的Window进行事件分发,如果返回true事件循环结束,返回false意味着无人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用

  1. Window对事件的分发过程

Window类可以控制顶级View的外观和行为策略,它的唯一实现是android.policy.PhoneWindow。那么从PhoneWindow源码中可以得知它把事件传递给了DecorView

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

顶级View一般是ViewGroup,到达ViewGroup之后首先会调用dispatchTouchEvent,如果返回true那么事件将不再进行分发而是ViewGroup自己处理,这是如果ViewGroup中设置了onTouchListener那么onTouch就会被调用否则就会调用onTouchEvent,当设置了onTouchListeneronTouchEvent就会被屏蔽,也就是说调用了onTouch就不会调用onTouchEvent,在onTouchEvent中如果设置了onClickListener那么onClick会被调用。ViewGroup如果返回false那么事件将向下分发,传递给它的子View,然后子View调用dispatchTouchEvent对事件进行分发,到这里顶级View到子View的事件分发就完成了,子View再向下分发的过程跟之前一样,如此循环就完成了整个事件的分发。

  • ViewGroup对点击事件的分发过程
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;
            }

代码actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null 的意思是判断是否要拦截事件,其中actionMasked == MotionEvent.ACTION_DOWN很好理解,而mFirstTouchTarget != null的意思从后面的代码就可以知道,当ViewGroup不拦截事件而是交给子元素处理时mFirstTouchTarget就会被赋值,这个条件就会成立,如果ViewGroup拦截了事件mFirstTouchTarget将不会被赋值,这样上面的条件就不会成立,那么如果eventACTION_UPACTION_MOVEif (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件就是false,这就表明事件将由ViewGroup自己处理,后续的onInterceptTouchEvent将不会再被调用并且同一序列中的其他事件都会默认交给它来处理。

存在一个特殊情况,在子View中如果通过requestDisallowInterceptTouchEvent配置了FLAG_DISALLOW_INTERCEPT的标记位那么ViewGroup将无法拦截除ACTION_DOWN以外的事件,因为在ViewGroup中遇到ACTION_DOWN这个事件时会重置这个标记位,这就导致了子View中设置的这个标记位无效。

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

上面的代码中可以得知当事件是ACTION_DOWN时在cancelAndClearTouchTarget方法中ViewGroup会做重置状态的操作,在resetTouctState中会重置FLAG_DISALLOW_INTERCEPT,所以就验证了上面的结论requestDisallowInterceptTouchEvent的配置不会影响ViewGroupACTION_DOWN的处理。

//这里主要是针对ViewGroup不拦截事件的代码,或者说是ViewGroup向子View分发事件的源码
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
        childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
        preorderedList, children, childIndex);
    if (!child.canReceivePointerEvents()
        || !isTransformedTouchPointInView(x, y, child, null)) {
        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);
    //这里调用的是子元素的dispatchTouchEvent
    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();
        //子元素的dispatchTouchEvent返回true那么暂时就不用考虑子元素的内部是怎么分发的
        //此时mFirstTouchTarget就会被重新赋值并跳出循环
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }

上面的这块的代码主要是ViewGroup不拦截事件的时候,这里首先遍历了ViewGroup中的子元素,然后判断子元素是都能够接受到点击事件,判断子元素能否接受到点击事件的因素主要有两个:1.子元素是否正在播放动画;2.当点击子元素的时候点击的坐标是否正好在子元素的区域内,这两个条件满足后事件就会向下传递。接下来就是调用dispatchTransfmoredTouchEvent方法来传递事件,并且从这个方法的源码中可以得知它在向子元素分发事件的时候调用的就是子元素的dispatchTouchEvent方法,这样事件就交给了子元素处理,从而完成了一轮事件分发。具体的可以看它的源码,源码中有这么一段

//dispatchTransformedTouchEvent部分源码
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    //这个child在父元素的dispatchTouchEvent方法中不拦截那一段代码会有赋值,因此它不为null
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

如果子元素的dispatchTouchEvent返回true那么就暂时不用考虑子元素是如何进行事件分发的,此时mFirstTouchTarget就会被重新赋值并跳出ViewGroup遍历子元素的循环。如果子元素的dispatchTouchEvent返回false时ViewGroup就会继续向下分发事件,前提是下面还有子元素。

从下面的源码中可以了解到mFirstTouchTarget的赋值是在addTouchTarget内部完成的,mFirstTouchTarget的赋值将直接影响ViewGroup对事件的拦截策略,如果为null那么ViewGroup就默认拦截下同一事件序列中的所有点击事件。

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

如果遍历所有的子元素后事件都没有被处理那么ViewGroup会怎么样呢?

首先分析遍历完所有元素都没有被处理的情况有哪些:

    • ViewGroup没有子元素,这个交由他自己处理
    • 子元素处理了事件但是dispatchTouchEvent返回了false,这一块是因为子元素在onTouchEvent中返回了false,这种情况下ViewGroup也会自己处理事件。
  1. View对点击事件的处理过程
// 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,从这个方法中可以得知当child==null时会调用super.dispatchTouchEvent,这里就转到了ViewdispatchTouchEvent方法,也就是说点击事件开始交由View来处理了。

先来看一下View#distatchTouchEvent源码

public boolean dispatchTouchEvent(MotionEvent event) {
    // If the event should be handled by accessibility focus first.
    if (event.isTargetAccessibilityFocus()) {
        // We don't have focus or no virtual descendant has it, do not handle the event.
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        // We have focus and got the event, then use normal event dispatch.
        event.setTargetAccessibilityFocus(false);
    }
    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        //这里先判断是否配置了OnTouchListener,再判断onTouch是否返回true
        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;
}

这里的 View是不包含ViewGroup的因此它是一个单独的元素,也就不存在向下分发事件的情况,所以事件就只能由自己来处理了,在处理的过程中首先判断是否配置了onTouchListener然后再判断onTouch是否为true,如果为true那么事件就不会由onTouchEvent处理而是交给onTouch,这样做的好处是方便在外界处理点击事件

下面再来分析onTouchEvent的实现,先看一下View处于不可用的状态时点击事件的处理过程

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

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    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依旧会消耗点击事件只不过是没有任何响应的。

再来分析一下onTouchEvent对点击事件的处理过程是怎么样的

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

先来分析下在什么情况下会进入switch语句开始消耗点击事件,当clickable == true或者TOOLTIP == true时点击事件会有响应,clickable == true的可能有三点:CLICKABLE = true或者LONG_CLICKABLE = true或者CONTEXT_CLICKABLE = true,这个CONTEXT_CLICKABLE可以理解为一个可以点击的视图,TOOLTIP又是什么呢,从它的注释可以理解为是一个长按或者悬停的操作,这个可以理解为指针在浏览器的某个按钮上放着然后会弹出一个提示。分析完在什么情况下会开始消耗点击事件后再来分析下具体是怎么实现点击的逻辑的。

ACTION_UP中最终会通过performClickInternal()方法调用performClick(),然后在performClick方法中最终会调用ViewonClick方法,这里还要了解的一点是LONG_CLICKABLE默认为false,CLICKABLE是否为true则要根据具体的View来决定,例如Button默认为true,TextView默认为false,当调用了setOnClickListenerCLICKABLE就会自动置为true,当调用了setOnLongClickListener会自动把LONG_CLICKABLE置为true。

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