开启成长之旅!这是我参加「日新计划 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;
}