天天看点

从源码角度解析Android事件分发机制

事件分发是Android中的一个重点也是一个难点,在自定义控件中很是常用。前后看了好多书和博客,感觉写的东西顺序都稍微有些不对,让刚接触的人看起来不是很好懂。在这里也是将我从不清楚到熟悉的过程写下来,希望对大家有所帮助,对自己也起到总结的作用。

下面介绍几个方法,只要先有个印象就好,以后会慢慢解释:

首先是

dispatchTouchEvent(MotionEvent ev)

分发事件,返回true表示事件被当前的View或其子View消耗,返回false表示事件没被消耗。

onInterceptTouchEvent(MotionEvent ev)

是否拦截某个事件,返回true表示拦截,返回false表示不拦截

onTouchEvent(MotionEvent ev)

处理点击事件,返回true表示事件被当前的ViewGroup/View消耗掉,返回false表示没消耗

任玉刚大神书中的一段代码对这个逻辑阐述的非常清楚,下面是这段伪代码:

public boolean dispatchTouchEvent(MotionEvent ev){
		boolean consume = false;
		if(onInterceptTouchEvent(ev)){
			consume = onTouchEvent(ev);
		}else{
			consume = child.dispatchTouchEvent(ev);
		}
		return consume;
}
           

先简单的理解上面的代码,在源码的分析中将会对代码进行进一步分析,而后给出结论。

源代码分析:

当一个点击事件发生时,最先接收到事件的是Activity的dispatchTouchEvent方法,下面是相应源码:

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

如果动作是ACTION_DOWN的话,会回调onUserIteraction方法,这个和我们关系不大。

我们先暂且认为getWindow().superDispatchTouchEvent(ev)是调用子View分发事件的方法,当子View没有消耗掉这个事件(也就是子View的dispatchTouchEvent返回false)时,Activity就会调用自身的onTouchEvent方法来处理这个事件。

进入getWindow().superDispatchTouchEvent(ev)发现这是一个抽象的方法,我们知道Window的实现类是PhoneWindow,查看PhoneWindow的相应方法可以看到其调用了DecorView的superDispatchTouchEvent,在以前的学习中我们知道在setContentView中我们设置的View就是DecorView的子View,所以点击事件一定能传递到子View,而一般的子View就是ViewGroup。根据本文开始的分析,进入时应该调用的是dispatchTouchEvent方法,我们现在来看一下ViewGroup中dispatchTouchEvent方法的源码:

if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
           

在方法开始的时候有上述的代码片段,注释上写的很明确,要清除之前的所有状态,具体要清除的是什么状态呢?先接着看代码:

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变量,它代表的是什么意思呢?在后文中会有对mFirstTouchTarget的说明,但是这里为了解释清楚,不妨先透露一下,如果当前View将事件分发到子View,那么mFirstTouchTarget将被赋值。

还有一个符号位FLAG_DISALLOW_INTERCEPT,看名字也应该可以猜出来,它的作用是不要拦截当前的事件,那和这个符号位进行与操作的mGroupFlags又是谁设置的?不难猜出,应该是子View设置的为了让当前View不去拦截事件。

现在重新审视我们刚才写过的内容,在进入dispatchTouchEvent方法时清除状态所清除的内容也就很好理解了:

private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
           
private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
           

很清楚,将mGroupFlags的状态清除掉了,同时将mFirstTouchTarget置为null。

现在我们再来看给intercepted赋值的代码块,不难得出下面这几个结论:

1.每次ACTION_DOWN发生的时候,都会回调onInterceptEvent方法(因为每次ACTION_DOWN发生的时候状态都会清除)。

2.一旦当前View将ACTION_DOWN事件拦截后,mFirstTouchTarget仍然为空,那么在一个事件序列(一个ACTION_DOWN,中间任意多个ACTION_MOVE,一个ACTION_UP)内不会再调用onInterceptTouch方法进行判断,intercepted变量始终为true,也就是所事件一直被当前View所拦截。

3.若ACTION_DOWN不拦截,那么在一个事件序列内不管拦截了什么动作,在下一个事件到来时都要回调onInterceptTouchEvent方法。

4.子View可以通过设置mGroupFLags值的方式来控制当前View拦截事件,但是ACTION_DOWN事件不被控制。

if (!canceled && !intercepted) {
                    final int childrenCount = mChildrenCount;
                    if (childrenCount != 0) {
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final View[] children = mChildren;
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);

                        final boolean customOrder = isChildrenDrawingOrderEnabled();
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder ?
                                    getChildDrawingOrder(childrenCount, i) : i;
                            final View child = children[childIndex];
                            if (!canViewReceivePointerEvents(child)
                                    || !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);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownIndex = childIndex;
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }
           

这里删除了一些不必要的代码。我们仔细观察一下最后一个if语句块,可以发现,当 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法返回true的时候for循环即可退出,下面是这个方法代码中有用的片段:

if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
           

可以看出当前的child不为空,那么其调用了child的dispatchTouchEvent方法。如果当前所有的子View的dispatchTouchEvent都返回的是false,或者当前没有子View,那么for循环就会跳出,执行如下的代码:

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

这次传入dispatchTransformedTouchEvent方法的参数是null,由上面的代码可以看出将调用父View的dispatchTouchEvent方法。这里的返回值是handled,也就是子View的dispatchTouchEvent返回值或者当前View的处理,也说明了开篇时伪代码的正确性。

前面提到了mFirstTouchTarget,那么这个参数是在哪赋值的呢?仔细看dispatchTransformedTouchEvent返回true时进入的if代码块,调用了一个函数是addTouchTarget(child, idBitsToAssign),其中的代码如下:

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
           

在其中完成了对mFirstTouchTarget的赋值。

至此我们完成了整个ViewGroup的分析过程,如果仔细观察不难得出结论,当前View如果将事件分发给子View来进行处理,但是子View的dispatchTouchEvent方法返回false的话,那么当前View将会处理这个事件。同时不难用递归的思想来想这个问题,如果当前的View的dispatchTouchEvent也返回false呢,那么当前View的上一级View就会进行处理,而最后不难发现事件将会有Activity中的dispatchTouchEvent方法进行消耗,也就是调用了Activity中的onTouchEvent,如果忘记了可以去看最开始贴出的Activity源码。

分析过了整个ViewGroup的分发过程,现在到了整个事件分发的最底端,也就是View的分发过程,同样的,我们先来看一下dispatchTouchEvent中的代码:

<span style="font-size:18px;">public boolean dispatchTouchEvent(MotionEvent event) {

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }

            if (onTouchEvent(event)) {
                return true;
            }
        }
        return false;
    }</span>
           

整个方法很简单,if的判断条件过滤了所有View不响应的状态,我们看if代码块内的内容。值得注意的是,当为View设置了OnTouchListener后,并且重写的onTouch方法返回true的时候,整个方法直接返回了。换句话说就是onTouchEvent方法并不会执行,这点要注意。另外,注意到整个dispatchTouchEvent方法的返回值也就是onTouch或者onTouchEvent的返回值,代表了当前的事件传递到了View是否被消耗了,如果并没有被消耗,根据上文中我们得出的结论,那么上一级View的dispatchTouchEvent就会被调用,这个关系可以依次向上级传递。

上面的方法中调用了onTouchEvent,我们现在来看一看这个方法的默认实现:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == 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));
        }
           

刚进入方法时来了一段这样的片段,不难理解出它的意思。在当前的View状态为DISABLE时,只要它是可点击的,或者是可以长点击的话,onTouchEvent就会返回true并且将当前的事件消费掉,但是并不响应这个事件。

继续向后来阅读:

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

看到了这样一个代码块,如果当前的View设置有TouchDelegate(代理,一般用于增加点击的范围,有兴趣的可以自行了解一下)的话,那么事件是否被消耗掉取决于代理onTouchEvent的返回值。

而后面的代码比较多也和事件分发无关,如果感兴趣可以去读一读。值得注意的是,在当前的View的Clickable或LongClickable至少有一个为真时,onTouchEvent就会返回true,也就是说View会将这个事件消费掉,同时要注意上文中有一行代码是ViewGroup调用super的dispatchTouchEvent方法,那么最终也是会执行到这里的。

继续阅读