View的事件分发源码分析

来源:互联网 发布:抢票软件哪个好 编辑:程序博客网 时间:2024/06/02 11:22

前言

在Android中,View主要负责界面的绘制和事件的分发、处理,它是所有控件Widgets的基类。通过源码分析View的事件分发,我们可以更加深刻地理解Android系统中View的工作原理。不仅如此,在日常的开发中,当我们遇到View事件冲突、滑动冲突时,处理起来将会游刃有余。

基础知识

当我们的手指触摸手机屏幕时,手机中的应用会对我们的触摸动作做出响应,确切地说是应用里的控件Widgets响应了触摸事件。在Android中,使用MotionEvent来描述触摸事件,我们可以通过getAction()方法来获取当前的事件类型。通常,一次手势动作会产生一系列的事件,下面列举了4个主要事件:

  • ACTION_DOWN事件 当手指第一次触摸到屏幕时将产生此事件。ACTION_DOWN事件表示一系列事件的开始。
  • ACTION_UP事件 当手指离开屏幕时将产生此事件。与ACTION_DOWN事件对应,ACTION_UP事件表示一系列事件的结束。
  • ACTION_MOVE事件 当手指有在屏幕上滑动时将产生此事件。
  • ACTION_CANCEL事件 表示当前的手势被中止了。如果一个View收到了ACTION_CANCEL事件,那么它不会再收到其它任何事件,包括ACTION_UP事件。

通过getX(), getY()方法可以获取到当前事件在屏幕上的坐标。注意,这个坐标是相对于父容器左上角的坐标。通过getRawX(), getRawY()方法可以获取到当前事件在屏幕上的原始坐标。通过前后两个ACTION_MOVE事件的坐标我们就可以知道当前手势动作的方向了。

在具体分析之前,先提一下View的事件分发的3个核心方法:

  • dispatchTouchEvent()方法 主要负责事件的分发。
  • onInterceptTouchEvent()方法 主要负责事件的拦截,ViewGroup专有。
  • onTouchEvent()方法 主要负责事件的处理。

dispatchTouchEvent()和onTouchEvent()方法都有返回值,如果返回值为true,表示当前事件被处理了或者被消费了。另外再提一个ViewGroup的requestDisallowInterceptTouchEvent()方法,子控件通过调用这个方法可以控制是否允许父容器拦截事件,它具体影响了父容器的FLAG_DISALLOW_INTERCEPT标志位。

下面我们开始具体的源码分析。

Activity的事件分发

在Android中,底层的触摸事件最开始是传递到Activity中的,从Activity的dispatchTouchEvent()方法开始分发事件。

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

从上面的代码可以知道,Activity将事件交给Window来负责分发到具体的页面布局中。如果Window的superDispatchTouchEvent()方法返回了true,即事件被消费了,那么直接退出。反之,如果没有任何一个View消费事件,那么最终Activity的onTouchEvent()方法将被调用,即Activity自己来处理事件。

Activity的Window是个抽象类,它的具体实现类是PhoneWindow。下面来看PhoneWindow的superDispatchTouchEvent()方法。

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

PhoneWindow的superDispatchTouchEvent()方法比较简单,它直接将事件传递给了DecorView。DecorView是Android系统中所有Activity页面布局的顶级父容器。平常我们在Activity的onCreate()方法中调用setContentView()方法来设置页面布局,其实页面布局是被添加到DecorView这个父容器中。下面来看DecorView的superDispatchTouchEvent()方法。

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

DecorView的superDispatchTouchEvent()方法比较简单,它将事件传递给了父类的dispatchTouchEvent()方法。在Android中,所有的父容器都是继承自ViewGroup,而ViewGroup继承自View。ViewGroup重写了View的dispatchTouchEvent()方法,所以事件开始从ViewGroup中进行分发。

ViewGroup的事件分发

ViewGroup的dispatchTouchEvent()方法比较复杂,我们分段来分析。

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {    ...    boolean handled = false;    if (onFilterTouchEventForSecurity(ev)) {        final int action = ev.getAction();        final int actionMasked = action & MotionEvent.ACTION_MASK;        // 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();        }        ...    }    ...    return handled;}

前面说过,ACTION_DOWN事件表示一次手势动作产生的一系列事件的起始事件。在dispatchTouchEvent()方法的开始,如果是ACTION_DOWN事件,那么ViewGroup会做一些复位、重置操作。

private void cancelAndClearTouchTargets(MotionEvent event) {    if (mFirstTouchTarget != null) {        ...        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {            resetCancelNextUpFlag(target.child);            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);        }        clearTouchTargets();        ...    }}

ViewGroup使用mFirstTouchTarget变量来存储消费了事件的子控件。mFirstTouchTarget变量将所有消费了事件的子控件以链表的形式存储在一起。但是,通常要么没有子控件消费事件,要么只有一个子控件消费了事件。在cancelAndClearTouchTargets()方法中,如果之前有子控件消费了事件,那么ViewGroup将通过dispatchTransformedTouchEvent()方法向它们分发ACTION_CANCEL中止事件以便开始一轮新的事件传递。接着在clearTouchTargets()方法中将mFirstTouchTarget变量重置为null。

private void resetTouchState() {    clearTouchTargets();    resetCancelNextUpFlag(this);    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;    mNestedScrollAxes = SCROLL_AXIS_NONE;}

resetTouchState()方法中复位了一些标志位,包括了不允许父容器拦截事件的标志位FLAG_DISALLOW_INTERCEPT。接着往下看dispatchTouchEvent()方法。

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {        ...        // 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;        }        ...        // Check for cancelation.        final boolean canceled = resetCancelNextUpFlag(this)                || actionMasked == MotionEvent.ACTION_CANCEL;        ...}

这段代码的主要作用是检查ViewGroup是否拦截了事件、是否中止了事件传递。当ACTION_DOWN事件发生或者mFirstTouchTarget变量不为null,即之前有子控件消费了事件时,检查ViewGroup是否拦截事件。如果子控件没有调用ViewGroup的requestDisallowInterceptTouchEvent()方法来设置FLAG_DISALLOW_INTERCEPT标志位,那么ViewGroup将调用onInterceptTouchEvent()方法来决定是否拦截事件。

ViewGroup的onInterceptTouchEvent()方法默认返回false,即父容器默认是不拦截事件的。在平常的开发中,我们可以根据需要重写ViewGroup的onInterceptTouchEvent()方法来决定是否拦截事件。

如果ViewGroup拦截了ACTION_DOWN事件,那么mFirstTouchTarget变量将为null。根据上面的代码可以知道,当后续的ACTION_MOVE、ACTION_UP等其它事件到来时,intercepted直接为true,ViewGroup拦截事件。这种情况下所有的事件都将由ViewGroup自己处理,子控件一个事件也接收不到。所以,在平常的开发中一般不会让ViewGroup拦截ACTION_DOWN事件。

如果ViewGroup不拦截ACTION_DOWN事件,但是没有子控件消费ACTION_DOWN事件,那么mFirstTouchTarget变量将为null。同上,当后续的ACTION_MOVE、ACTION_UP等其它事件到来时,intercepted直接为true,ViewGroup拦截事件。这种情况下子控件只接收到一个ACTION_DOWN事件,不会接收到后续的其它事件。

接着往下看dispatchTouchEvent()方法。

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {        ...        // Update list of touch targets for pointer down, if needed.        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;        TouchTarget newTouchTarget = null;        boolean alreadyDispatchedToNewTouchTarget = false;        if (!canceled && !intercepted) {            ...            if (actionMasked == MotionEvent.ACTION_DOWN                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                ...                final int childrenCount = mChildrenCount;                if (newTouchTarget == null && childrenCount != 0) {                    ...                    final View[] children = mChildren;                    for (int i = childrenCount - 1; i >= 0; i--) {                        ...                        if (!canViewReceivePointerEvents(child)                                || !isTransformedTouchPointInView(x, y, child, null)) {                            ev.setTargetAccessibilityFocus(false);                            continue;                        }                        ...                        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;                        }                        ...                    }                    if (preorderedList != null) preorderedList.clear();                }                ...            }        }        ...}

这段代码的主要作用是将事件分发到可以处理事件的子控件。当事件没有被中止和拦截时,如果是ACTION_DOWN事件,那么ViewGroup开始遍历子控件进行事件分发。ViewGroup主要通过两个方法来判断子控件是否可以接收事件,canViewReceivePointerEvents()方法判断子控件的可见性和是否有动画,isTransformedTouchPointInView()方法判断事件是否落在子控件的布局区域中。

当子控件满足条件时,ViewGroup将调用dispatchTransformedTouchEvent()方法将事件传递给子控件。如果dispatchTransformedTouchEvent()方法返回了true,即子控件消费了事件,那么将调用addTouchTarget()方法将子控件设置给mFirstTouchTarget变量,然后退出循环。如果没有一个子控件消费了事件,那么mFirstTouchTarget变量仍然为null。

接着往下看dispatchTouchEvent()方法。

@Overridepublic 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);        } else {            // Dispatch to touch targets, excluding the new touch target if we already            // dispatched to it.  Cancel touch targets if necessary.            TouchTarget predecessor = null;            TouchTarget target = mFirstTouchTarget;            while (target != null) {                final TouchTarget next = target.next;                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {                    handled = true;                } else {                    final boolean cancelChild = resetCancelNextUpFlag(target.child)                            || intercepted;                    if (dispatchTransformedTouchEvent(ev, cancelChild,                            target.child, target.pointerIdBits)) {                        handled = true;                    }                    if (cancelChild) {                        if (predecessor == null) {                            mFirstTouchTarget = next;                        } else {                            predecessor.next = next;                        }                        target.recycle();                        target = next;                        continue;                    }                }                predecessor = target;                target = next;            }        }        ...}

当ViewGroup一开始就拦截了ACTION_DOWN事件或者没有子控件消费ACTION_DOWN事件时,mFirstTouchTarget变量为null,ViewGroup将通过dispatchTransformedTouchEvent()方法将事件传递给自己处理。反之,如果有子控件消费了ACTION_DOWN事件,并且后续事件没有被ViewGroup拦截,那么ViewGroup将直接通过mFirstTouchTarget变量进行事件分发。

如果有子控件消费了ACTION_DOWN事件,即mFirstTouchTarget变量不为null,但是后续事件被ViewGroup拦截了,此时cancelChild为true,ViewGroup将通过dispatchTransformedTouchEvent()方法向子控件分发ACTION_CANCEL事件,之后mFirstTouchTarget将被置为null。当后续事件到来时,ViewGroup将自己处理拦截的事件了。

接着看下dispatchTransformedTouchEvent()方法。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,        View child, int desiredPointerIdBits) {    final boolean handled;    // Canceling motions is a special case.  We don't need to perform any transformations    // or filtering.  The important part is the action, not the contents.    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;    }    ...    // 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);    }    // Done.    transformedEvent.recycle();    return handled;}

当参数cancel为true或者是ACTION_CANCEL事件时,ViewGroup通过dispatchTransformedTouchEvent()方法传递ACTION_CANCEL事件给子控件或者ViewGroup自身。反之,将传递其它事件。

如果参数child为null,那么将调用ViewGroup父类的dispatchTouchEvent()方法,即ViewGroup自己处理事件。如果child不为null,那么将调用child,即View的dispatchTouchEvent()方法,即子控件处理事件。此时,事件分发由ViewGroup传入了View。

View的事件分发

因为ViewGroup也继承自View,所以要特别说明一下这部分提到的View特指子控件,不包括ViewGroup。View的事件处理相对来说就比较简单了,来看下View的dispatchTouchEvent()方法。

public boolean dispatchTouchEvent(MotionEvent event) {    ...    boolean result = false;    ...    if (onFilterTouchEventForSecurity(event)) {        ...        //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是enabled状态并且设置了OnTouchListener时,View将先调用OnTouchListener的onTouch()方法。如果onTouch()方法返回了true,那么将不再调用View的onTouchEvent()方法。可见,View的OnTouchListener的优先级高于onTouchEvent()方法。

如果View没有设置OnTouchListener,那么onTouchEvent()方法将被调用。最后我们来看下onTouchEvent()方法。

public boolean onTouchEvent(MotionEvent event) {    ...    if ((viewFlags & ENABLED_MASK) == DISABLED) {        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {            setPressed(false);        }        // A disabled view that is clickable still consumes the touch        // events, it just doesn't respond to them.        return (((viewFlags & CLICKABLE) == CLICKABLE                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);    }    ...    if (((viewFlags & CLICKABLE) == CLICKABLE ||            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {        switch (action) {            case MotionEvent.ACTION_UP:                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {                    ...                    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)) {                                performClick();                            }                        }                    }                    ...                }                mIgnoreNextUpEvent = false;                break;            ...        }        return true;    }    return false;}

当View是disabled状态时,只要View是clickable的,onTouchEvent()方法将返回true。如果View是enabled状态并且是clickable的,onTouchEvent()方法默认也返回true。这说明,默认情况下只要有事件传递到了View并且View是clickable的,那么事件就会被消费。

阅读View的源码可以发现,默认情况下View不是clickable的,即默认情况下View没有消费事件。ViewGroup继承自View,但是ViewGroup没有重写View的onTouchEvent()方法,所以默认情况下ViewGroup也没有消费事件。

通过View的setClickable()、setLongClickable()和setContextClickable()方法可以设置相应的clickable状态。特别要提一下的是,平常我们通过View的setOnClickListener()方法设置监听器时其实也设置了View的clickable状态。

最后,在ACTION_UP事件时View将调用performClick()方法。

public boolean performClick() {    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);    return result;}

在performClick()方法中,如果View设置了OnClickListener,那么将调用OnClickListener的onClick()方法。

到这里View的事件分发源码分析就结束了。

总结

通过对View的事件分发的源码进行分析,我们可以总结出以下一些结论:

  • 触摸事件的传递顺序是:Activity -> Window -> DecorView -> 具体的页面布局容器 -> 具体的子控件。如果没有View消费事件,那么事件将逐级返回,最终Activity的onTouchEvent()方法会被调用。
  • ViewGroup的onInterceptTouchEvent()方法默认返回false,即父容器默认是不拦截事件的。
  • 如果父容器拦截了ACTION_DOWN事件,那么它的子控件一个事件也接收不到。
  • 如果一个View没有消费ACTION_DOWN事件,那么后续的ACTION_MOVE、ACTION_UP等其它事件它都接收不到了。
  • 如果一个View消费了ACTION_DOWN事件,并且后续事件没有被父容器拦截,那么父容器会将后续事件直接传递给此View。
  • 如果一个View消费了ACTION_DOWN事件,但是后续事件被父容器拦截了,那么这个View只会再收到一个ACTION_CANCEL事件。
  • 默认情况下,ViewGroup和View都是不消费事件的。
  • OnTouchListener的onTouch()方法优先级高于onTouchEvent()方法。

例子

这里举了两个简单的例子。例子代码地址:https://github.com/chongyucaiyan/ViewDemo

第一个例子主要用来了解正常情况下触摸事件的传递顺序。

demo01页面.png

如上图所示,布局很简单,垂直方向的LinearLayout布局里放置了一个TextView和一个Button。代码里主要是在View的事件分发核心方法里加了日志打印,PhoneWindow和DecorView没办法加日志就没加了。首先,在TextView上触发一次手势,打印的日志如下图所示:

demo01页面TextView日志.png

如上图所示,触摸事件从Demo01Activity传递到MyLinearLayout01父容器,最后传递到MyTextView01子控件。同时我们可以看到,默认情况下,父容器不拦截事件,父容器和子控件不消费事件。下面简要分析一下。

ACTION_DOWN事件发生后,事件先被传递到Demo01Activity的dispatchTouchEvent()方法。接着事件被传递到MyLinearLayout01的dispatchTouchEvent()方法。MyLinearLayout01的onInterceptTouchEvent()方法返回false,即父容器默认不拦截事件。接着事件被传递到MyTextView01的dispatchTouchEvent()方法。此时,MyTextView01调用onTouchEvent()方法来处理事件。MyTextView01的onTouchEvent()方法返回false,即子控件默认不消费事件。这时事件返回,MyLinearLayout01调用onTouchEvent()方法来处理事件。MyLinearLayout01的onTouchEvent()方法返回false,即父容器默认不消费事件。事件接着返回,最终Demo01Activity的onTouchEvent()方法被调用。

因为MyTextView01和MyLinearLayout01都没有消费ACTION_DOWN事件,所以后续的ACTION_MOVE、ACTION_UP事件它们都接收不到了。

然后,在Button上触发一次手势,打印的日志如下图所示:

demo01页面Button日志.png

如上图所示,默认情况下MyButton01消费了事件,代码里并没有给MyButton01设置OnClickListener。这是因为Android应用默认使用的theme之中设置了Button的clickable属性为true,造成Button默认消费事件。

MyButton01消费了ACTION_DOWN事件,所以MyLinearLayout01和Demo01Activity的onTouchEvent()方法都不会被调用了。并且MyButton01可以正常接收到后续的ACTION_MOVE、ACTION_UP事件。

第二个例子主要用来了解拦截情况下触摸事件的传递情况。

布局更简单,FrameLayout里放置了一个Button。代码里在MyFrameLayout02中重写了onInterceptTouchEvent()方法,对ACTION_MOVE事件进行拦截。

@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {    boolean intercepted;    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN:            intercepted = false;            break;        case MotionEvent.ACTION_MOVE:            intercepted = true;            break;        case MotionEvent.ACTION_UP:            intercepted = false;            break;        default:            intercepted = super.onInterceptTouchEvent(event);            break;    }    Log.i(TAG, "onInterceptTouchEvent(), " + Utils.getActionString(event) + ", intercepted = " + intercepted);    return intercepted;}

然后,在Button上触发一次手势,打印的日志如下图所示:

demo02页面Button日志.png

如上图所示,ACTION_DOWN事件被MyButton02正常消费,当ACTION_MOVE事件发生时,MyFrameLayout02对事件进行拦截。此时,MyButton02只再接收到一个ACTION_CANCEL事件,其它事件都接收不到了。

参考

  • Android 7.1.1 (API level 25)
  • https://developer.android.com/reference/android/view/View.html
原创粉丝点击