Android的事件分发机制(下)|SquirrelNote

系列文章:
Android的事件分发机制(上)|SquirrelNote
Android的事件分发机制(下)|SquirrelNote

前言

本篇将会从源码的角度进一步分析。

事件分发的源码解析

分为:
1.Activity对点击事件的分发过程
2.ViewGroup对点击事件的分发过程
3.View对点击事件的处理过程

一、Activity对点击事件的分发过程

点击事件使用MotionEvent来表示(这里对MotionEvent不了解的,可以先看上篇),当一个点击操作发生的时候,事件是最先传递给当前的Activity,由Activity的dispatchTouchEvent方法进行事件分发,具体的工作是由Activity内部的Window来完成。Window会将事件传递给当前界面的底层容器DectorView(即setContentView所设置的父容器),通过Activity.getWindow.getDecorView()可以获得。
这里先从Activity源码的dispatchTouchEvent开始分析:

/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     * 
     * 调用处理触摸屏事件。您可以在它们被发送到窗口之前,重写它来拦截所有的触摸屏事件。
     * 请务必调用此实现,以实现应正常处理的触摸屏事件。
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        
        /**
         * 调用Activity内部的mWindow的superDispatchTouchEvent方法,mWindow其实是PhoneWindow的实例
         * 如果getWindow().superDispatchTouchEvent(ev)返回false,即没有任何子View处理事件,
         * 最终会执行Activity的onTouchEvent
         */
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

从源码可以看出:首先会判断当前触摸事件的类型,如果是ACTION_DOWN事件,会触发onUserInteration方法。根据文档注释,当有任意一个按键、触屏或者轨迹球事件发生时,栈顶的Activity的onUserInteraction会被触发。如果我们需要知道用户是不是在和设置交互,可以在子类(MainActivity)重写这个方法,去获取通知(比如取消屏保这个场景)。

然后调用Activity内部的mWindow的superDispatchTouchEvent方法,mWindow其实是PhoneWindow的实例。如果getWindow().superDispatchTouchEvent(ev)返回false,即没有任何子View处理事件,最终会执行Activity的onTouchEvent:

/**
     * Called when a touch screen event was not handled by any of the views
     * under it.  This is most useful to process touch events that happen
     * outside of your window bounds, where there is no view to receive it.
     * 
     * 当触摸屏幕事件没有被它的任何视图处理时调用。这对于处理发生在窗口边界之外的触摸事件是最有用的,
     * 因为没有视图可以接收它。
     *
     * @param event The touch screen event being processed.
     *
     * @return Return true if you have consumed the event, false if you haven't.
     * The default implementation always returns false.
     */
    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

小结:
事件是从Activity的dispatchTouchEvent开始,通过DectorView向下传递,交由子View处理,如果事件没有被任何Activity的子View处理,将由Activity自己处理。

二、ViewGroup对点击事件的分发过程

ViewGroup对点击事件的分发过程,主要实现在ViewGroup的dispatchTouchEvent这个方法中,这个方法比较
长,这里进行分段说明。

  1. 判断事件是否需要被ViewGroup拦截
  2. 遍历所有子View,逐个分发事件
  3. 将事件交给ViewGroup自己或者目标子View处理

1.判断事件是否需要被ViewGroup拦截

下面描述的是当前View是否拦截点击事件这个逻辑,有注释:

// Check for interception.
final boolean intercepted;
/**
* 这里判断是否要拦截当前事件,判断类型actionMasked == MotionEvent.ACTION_DOWN
*|| mFirstTouchTarget != null。从后面的代码逻辑可以看出来,当事件由ViewGroup的子元素成功处
*理时,mFirstTouchTarget会被赋值并指向子元素,换种方式说,当ViewGroup不拦截事件并将事件交由
* 子元素处理时 mFirstTouchTarget != null。反过来,如果当前ViewGroup拦截事件时,
* mFirstTouchTarget != null不成立。那么当ACTION_MOVE和ACTION_UP事件到来时,由于
* (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)这个条件为false,
* 将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给
* 它处理。
*/
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;
}

当然,有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。如下:

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        /**
         * 层层向上传递,告知所有父类不拦截事件
         */
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

源码分析:

  • 首先会根据mGroupFlags判断是否可以执行onInterceptTouchEvent方法,它的值可以通过requestDisallowInterceptTouchEvent方法来设置。所以,我们在处理某些滑动冲突场景时,可以从子View中调用父View的requestDisallowInterceptTouchEvent方法,阻止父View拦截事件。

  • 如果View没有设置FLAG_DISALLOW_INTERCEPT,就可以进入onInterceptTouchEvent方法,判断是否应该被自己拦截。
    ViewGroup的onInterceptTouchEvent直接返回了false,即默认不拦截事件,ViewGroup的子类可以重写这个方法,内部判断拦截逻辑。

  • FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件,这一点从源码也可以看出来。

注意:
只有当事件类型是ACTION_DOWN或者mFirstTouchTarget不为空时,才会走是否需要拦截事件这一判断,如果事件是ACTION_DOWN的后续事件(如ACTION_MOVE、ACTION_UP等),且在传递ACTION_DOWN事件过程中没有找到目标子View时,事件将会直接被拦截,交给ViewGroup自己处理。

// Handle an initial down.
/**
* ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对
* FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用requestDisallowInterceptTouchEvent方法并
* 不能影响ViewGroup对ACTION_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();
}

从上面源码分析,可以得出结论:当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法。这证实了如下结论:
当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。

FLAG_DISALLOW_INTERCEPT这个标志的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不ACTION_DOWN事件,这证实了如下结论:
事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

那么,这段分析对我们有什么价值呢?总结起来有如下两点:

  1. onInterceptTouchEvent不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法才能确保每次都会被调用,但是前提是事件能够传递到当前的ViewGroup;
  2. FLAG_DISALLOW_INTERCEPT标记位的作用给我们提供了一个思路,当面对滑动冲突的时候,我们是不是就可以考虑用这种方法去解决滑动冲突的问题?

2.遍历所有子View,逐个分发事件

当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理,如下:

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 preorderedList = buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = customOrder
                ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(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;
        }

执行遍历分发的条件:当前事件是ACTION_DOWN。如果是事件ACTION_DOWN的后续事件,如ACTION_UP事件,将不会进入遍历流程。
上面源码逻辑:首先是遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。
dispatchTransformedTouchEvent方法里:

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

可以看到,dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法,这样事件就直接交由子元素去处理了,从而完成了一轮事件分发。

如果子元素的dispatchTouchEvent返回true,这时我们暂时不考虑事件在子元素内部是怎么分发的,就会把mFirstTouchTarget设置为child,即不为null,并将alreadyDispatchedToNewTouchTarget设置为true,然后跳出循环,事件不再继续传递给其他子View。如下:

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;

这几行代码完成了对mFirstTouchTarget的赋值并终止对子元素的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素(如果还有下一个子元素)。

可以理解为,这一步的主要作用是,在事件的开始,即传递ACTION_DOWN事件过程中,找到一个需要消费事件的子View,我们可以称之为目标子View,执行第一次事件传递,并把mFirstTouchTarget设置为这个目标子View。

3.将事件交给ViewGroup自己或者目标子View处理

/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 * 
 * 在列表的开头添加指定子的触摸目标。假设目标子尚未出现。
 */
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

可以看出,mFirstTouchTarget真正的赋值过程是在addTouchTarget内部完成的,从addTouchTarget方法的内部结构可以看出,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略。

如果mFirstTouchTarget仍然为空,说明没有任何一个子View消费事件,将同样会调用dispatchTransformedTouchEvent,但此时这个方法的View child参数为null,所以调用的其实是super.dispatchTouchEvent(event),即事件交给ViewGroup自己处理。ViewGroup是View的子View,所以事件将会使用View的dispatchTouchEvent(event)方法判断是否消费事件。

反之,如果mFirstTouchTarget不为null,说明上一次事件传递时,找到了需要处理事件的目标子View,此时,ACTION_DOWN的后续事件,如ACTION_UP等事件,都会传递至mFirstTouchTarget中保存的目标子View中。这里面还有一个小细节,如果在上一节遍历过程中已经把本次事件传递给子View,alreadyDispatchedToNewTouchTarget的值会被设置为true,代码会判断alreadyDispatchedToNewTouchTarget的值,避免做重复分发。

如果遍历所有的子元素后事件都没有被合适地处理,有两种情况:

  1. ViewGroup没有子元素
  2. 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。

在这两种情况下,ViewGroup会自己处理点击事件。证实了如下结论:
某个View一旦开始处理事件,如果它不消耗Action_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。

小结:

  • dispatchTouchEvent方法首先判断事件是否需要被拦截,如果需要拦截会调用onInterceptTouchEvent,若该方法返回true,事件由ViewGroup自己处理,不在继续传递。
    若事件未被拦截,将先遍历找出一个目标子View,后续事件也将交由目标子View处理。
    若没有目标子View,事件由ViewGroup自己处理。

  • 此外,如果一个子View没有消费ACTION_DOWN类型的事件,那么事件将会被另一个子View或者ViewGroup自己消费,之后的事件都只会传递给目标子View(mFirstTouchTarget)或者ViewGroup自身。简单来说,就是如果一个View没有消费ACTION_DOWN事件,后续事件也不会传递进来。

三、View对点击事件的处理过程

注意:这里的View不包含ViewGroup。它的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(这里不包含ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。从源码可以看出:View对点击事件的处理过程,首先它会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可以发现onTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。

下面分析onTouchEvent的实现。当View处于不可用状态下点击事件的处理过程,如下所示(onTouchEvent方法里面):

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.
    /**
    * 如果一个View处于DISABLED状态,但是CLICKABLE或者LONG_CLICKABLE的话,这个View仍然能消费事    
    *件
    */
    return (((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}

可以看出,不可用状态下的View同样会消耗点击事件。

下面看一下onTouchEvent中对点击事件的具体处理,如下:

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();
                        }
                    }
                }
                ...
            }
            break;
  }
  ...
  return true;
}

从上面代码看,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态,这证实了如下结论:

  • View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(即clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,如:Button的clickable默认为true,而TextView的默认为false。
  • View的enable属性不影响onTouchEvent的默认返回值。即使一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
  • onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。

然后就是当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法,如下:

/**
 * Call this view's OnClickListener, if it is defined.  Performs all normal
 * actions associated with clicking: reporting accessibility event, playing
 * a sound, etc.
 * 如果定义了这个视图的OnClickListener。执行与单击有关的所有正常操作:报告可访问性事件、播放声音等。
 * 
 * @return True there was an assigned OnClickListener that was called, false
 * otherwise is returned.
 */
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;
}

通过setClickable和setLongClickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性。另外,setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener则会自动将View的LONG_CLICKABLE设为true,这一点可以从源码中看出来,如下:

/**
 * Register a callback to be invoked when this view is clicked. If this view is not
 * clickable, it becomes clickable.
 * 在单击此视图时注册一个回调。如果这个视图不是可点击的,它就会变成可点击的。
 *
 * @param l The callback that will run
 * @see #setClickable(boolean)
 */
public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
    if (!isLongClickable()) {
        setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}

OK,点击事件的分发机制的源码实现已经分析完了。

以上是根据我的一些理解,做的总结分享,旨在抛砖引玉,希望有更多的志同道合的朋友一起讨论学习,共同进步!

你可能感兴趣的:(Android的事件分发机制(下)|SquirrelNote)