Android中的事件分发机制

关于事件分发机制,已经看了无数的帖子,一直觉得一知半解、似懂非懂,今天从源码的角度学习一下,关键是帮助自己理解和记录自己学习的历程。

第一步,要先理解几个概念性的问题

  • 事件分发机制是什么?
    事件分发机制就是点击事件的分发

  • 那么点击事件又是什么?
    在手指接触屏幕后产生的同一个事件序列都是点击事件。

  • 点击事件分为哪几种类型?

    • 手指刚接触屏幕
    • 手指在屏幕上滑动
    • 手指从屏幕上松开的一瞬间
  • 同一个事件序列是什么?
    是从手指接触屏幕的一瞬间起,直到手指从屏幕上松开的一瞬间所产生的一切事件。

  • 点击事件用代码如何表示?
    在源码中MotionEvent就是点击事件,对点击事件的分发就是对MotionEvent对象的分发传递过程

  • MotionEvent的点击事件类型?

    • ACTION_DOWN:手指刚接触屏幕
    • ACTION_MOVE:手指在屏幕上滑动
    • ACTION_UP:手指从屏幕上松开的一瞬间

第二步,从整体上了解事件分发机制

1. 我们来简单描述一次点击事件(不涉及方法调用,先有个大概的体系)

  • 用户接触屏幕产生MotionEvent(点击事件)
  • MotionEvent(点击事件)总是由Activity先接收
  • Activity接收后将MotionEvent(点击事件)进行传递:Activity->Window->DecorView(DecorView是当前界面的底层容器,就是setContentView所设置View的父容器)
  • DecorView是一个ViewGroup,将MotionEvent(点击事件)分发向各个子View

2. 三个方法
相信大家对点击事件已经有所了解,那接下来我们介绍事件分发机制很重要的三个方法,点击事件的分发机制都是根据这三个方法共同完成的:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。

  • dispatchTouchEvent()用来进行事件的分发,如果MotionEvent(点击事件)能够传递给该View,那么该方法一定会被调用。返回值由 本身的onTouchEvent() 和 子View的dispatchTouchEvent()的返回值 共同决定。

    • 返回值为true,则表示该点击事件被本身或者子View消耗。
    • 返回值为false,则表示该ViewGroup没有子元素,或者子元素没有消耗该事件。
  • onInterceptTouchEvent():在dispatchTouchEvent()中调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中不会再访问该方法。

  • onTouchEvent():在dispatchTouchEvent()中调用,返回结果表示是否消耗当前事件,如果不消耗(返回false),则在同一个事件序列中View不会再次接收到事件。

3. 事件传递顺序

  • 用户点击屏幕产生MotionEvent(点击事件)
  • Activity接收MotionEvent(点击事件)—>传递给Window—>传递给DecorView(ViewGroup)—>执行ViewGroup的dispatchTouchEvent()
  • ViewGroup接收到MotionEvent(点击事件)之后,按照事件分发机制去分发事件。
  • 总的来说点击事件的传递顺序是由父到子,再由子到父的。

第三部,源码分析

1.从Activity的源码分析

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

其中onUserInteraction()方法在Activity中为空方法

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.
    public void onUserInteraction() {
    }

我们重点看getWindow().superDispatchTouchEvent(ev)方法。方法将点击事件传递给了Window,如果getWindow().superDispatchTouchEvent(ev)这个方法返回true,那么dispatchTouchEvent()方法也会返回true来表示事件已经被消耗掉了,如果所有的View都没有消耗掉点击事件,则Activity就会调用自己的onTouchEvent(ev)方法。

 

但是Window的方法,却是一个抽象方法

public abstract boolean superDispatchTouchEvent(MotionEvent event);

我们知道系统给出的唯一实现这个抽象类的是PhoneWindow,那我们就去找一找

 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

这里调用了DecorView的superDispaTouchEvent(event)方法,我们都知道DecorView是我们页面的顶层View。看来就是通过这次甩锅实现了事件从Activity到View的传递。接下来我们看一下DecorView里的superDispaTouchEvent(event)。

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

这里又调用了super的dispatchTouchEvent(event)。我们知道DecorView继承自FrameLayout,FrameLayout又继承自ViewGroup。FrameLayout中并没有重写dispatchTouchEvent(event)方法,所以我们直接去ViewGroup中去找。这里的代码比较多,一段一段来分析。

2.ViewGroup的dispatchTouchEvent()方法

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

这里mFirstTouchTarget是什么?它是

// First touch target in the linked list of touch targets.
    private TouchTarget mFirstTouchTarget;

TouchTarget是用来描述被触摸试图及其捕获的指针id,也就是,当目标View不为空并且事件是ACTION_DOWN事件,就进入if体中,继续向下执行,否则直接将intercepted设为true。

那么在if体中具体做了那些判断呢?

我们注意到FLAG_DISALLOW_INTERCEPT这个标志位,这个标志位是requestDisallowInterceptTouchEvent()方法来设置的,一般是在子View中调用。当标志位设置之后VeiwGroup将无法拦截除了ACTION_DOWN以外的事件(因为ACTION_DOWN是事件序列的开始,所以不能被拦截)。也就是说如果子View请求父View(ViewGroup)不要拦截,那么父View(ViewGroup)会直接将intercepted设置为false,否则intercepted的值由onInterceptTouchEvent()方法的返回值决定。也就是父View(ViewGroup)可以通过重写onInterceptTouchEvent()方法来控制是否拦截事件。

如果当前的ViewGroup不拦截事件,那么就会继续向下分发。

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

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

这个分发的过程实际上就是一个遍历子View的过程,在遍历的过程中,有这样 一个判断条件

if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null))

这是在判断当前点击事件是否在子View的坐标范围,且子View是否在坐标系中移动(在执行动画)

我们看一下源码

    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

如果当前子View是可见的并且没有执行动画,说明当前View可以处理事件,返回true。

protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

当event的事件的触发的位置在子View的范围中时返回true。

如果满足以上两个条件,就向下分发下去,我们看到这样一段代码:

dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)

看一下这个方法的一部分源码:

final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

如果子View为null,则调用super的dispatchTouchEvent()方法,即View的dispatchTouchEvent()。(这个具体的放到后面去分析) 。如果子View不为null,则调用当前子View的dispatchTouchEvent()方法。

如果dispatchTransformedTouchEvent()返回true,表明点击事件被子View消耗,执行addTouchTarget()方法给最开始的mFirstTouchTarget赋值。

如果遍历完了所有的子View,点击事件都没有被消耗掉,可能有两种情况:一、ViewGroup下面没有子View。二、子View没有消耗点击事件。这两种情况下,ViewGroup会自己处理点击事件。当子View不消耗点击事件,那点击事件将交由给他的父View去处理。

if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
}

代码中child参数赋值为null,就会调用super.dispatchTouchEvent()方法,即View的该方法。

3.View的dispatchTouchEvent()方法

我们来看一下View的dispatchTouchEvent() 方法的一段核心的代码

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

onFilterTouchEventForSecurity()方法是用来判断点击事件来到的时候,窗口是否被遮挡住,如果遮挡住返回false,同时dispatchTouchEvent()方法也会返回false,表示事件不被消耗。如果窗口没有被遮挡住,则进入if体内,继续向下处理。

handleScrollBarDragging()方法是用来判断是否为滚动条事件,如果这个事件被当做滚动条事件,则返回true,那dispatchTouchEvent()也会返回true,这个事件被消耗。否则返回false。

接下来我们看到了ListererInfo类,这是View的静态内部类。这个类中定义了一系列的listener。View会先判断自己是否有设置OnTouchListener,如果所设置的OnTouchListener得onTouch返回true,则直接消耗点击事件,不再执行onTouchEvent()方法。相反,就会执行onTouchEvent()方法。

接下来,我们继续看一下onTouchEvent()方法。

4.onTouchEvent()方法

这个方法是在View中的方法,ViewGroup并没有重写这个方法。

先看下面这段代码:

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是否可用,如果当前View不可用,根据注释我们知道,它依旧会消耗事件,只是不作出任何反应。

接下来

if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
}

这里判断是否设置了mTouchDelegate,这个表示View的代理,即如果设置了代理,那么当前View的点击事件会交给代理的View来处理,调用代理View的onTouchEvent方法,如果代理View消耗了事件,那么相当于当前View消耗了事件。

我们着重看一下对点击事件的处理

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

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;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

首先判断当前View是否可以点击或者长按,符合任何一个条件都会进入if体。我们可以看到if中最后都是返回true的,也就是说当View的CLICKABLE、LONG_CLICKABLE和CONTEXT_CLICKABLE有其中一个为true那么View就会消耗掉这个事件。

我们看看对ACTION_UP的事件进行响应的部分,首先会判断当前View是否是pressed状态,即按下状态,如果是按下状态就会触发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;
    }

可以看出,这里检测了当前View是否设置了onClickListener,如果设置了那么回调它的onClick方法,所以我们平时对一个Button设置点击事件之后,都会在其onTouchEvent方法的ACTION_UP逻辑里面得到回调。
这里可以得出结论:onTouchListener、onTouchEvent、onClickListener三者的优先级是:onTouchListener>onTouchEvent>onClickListener。

最后我们还要看一眼Activity中的dispatchTouchEvent方法

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

当这样将事件一路分发出去都没有View或ViewGroup来处理这个事件时,最后就会调用Activity的onTouchEvent方法。这就是为什么说点击事件的传递顺序是由父到子,再由子到父的。事件按Activity->ViewGroup->View传递下去,如果不被处理又会按照原路回传回来了。

参考

https://www.jianshu.com/p/e6ceb7f767d8

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值