阅读 656

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

基于 Android 28 源码分析

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

三个重要方法

首先我们需要介绍在点击事件分发过程中很重要的三个方法:

dispatchTouchEvent

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

onInterceptTouchEvent

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

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 来处理点击事件,如此反复直到事件被最终处理。

事件分发的源码分析

当一个点击事件发生后,它的传递过程遵循如下顺序: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 就会被调用。

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 方法被回调。

参考

关注下面的标签,发现更多相似文章
评论