浅析安卓事件分发机制源码

更多关于安卓源码分析文章,请看:安卓源码分析

最近工作需要需要做一些比较复杂的自定义View,其中事件分发的处理自然少不了,结合之前阅读过的大量资料,工作是完成了,但是对事件分发的处理总觉得很不清晰,知其然不知其所以然的感觉让人很不舒服。如果不知道事件分发原理,要是处理的情况很复杂的话,那就很难解决了。之前也看过任玉刚的《安卓开发艺术探索》对于事件分发源码的分析,但只能说大致了解了事件分发的流程,而不知其中的道理。

索性践行某位大师的名言——

“read the fucking source”

源码的阅读总是让人痛并快乐着,一是因为源码很长逻辑很复杂,二是因为源码要考虑的东西太多,所以干扰的东西实在太多,经常跟进去就迷路。

介于本文是对源码层次的分析,所以如果大家还没了解过事件分发的基本流程的话,最好先看一下这方面的资料。

首先,关于事件分发,相信接触过的人都看过类似这样一张U型图片:

http://www.jianshu.com/p/e99b5e8bd67b

并且也知道dispatchTouchEvent、InterceptTouchEvent、onTouchEvent这三个方法的作用分别分发事件、拦截事件、消费事件。

也看过类似的伪代码:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean result = false;             // 默认状态为没有消费过

    if (!onInterceptTouchEvent(ev)) {   // 如果没有拦截交给子View
        result = child.dispatchTouchEvent(ev);
    }

    if (!result) {                      
    // 如果事件没有被消费,询问自身onTouchEvent
        result = onTouchEvent(ev);
    }

    return result;
}

其实如果熟悉这些,简单的事件分发已经可以处理了,但是这些只是单纯记忆流程,并不知道具体的事件在源码中何去何从。

扫描二维码关注公众号,回复: 10749055 查看本文章

总的来说,ViewGroup和View属于树的结构,事件分发就是从父节点到子节点一步步遍历的过程,直到找到可以消费事件的View为止。而在这一过程中,就是一个不断递归调用以上伪代码的过程。子View总是在经过dispatchTouchEvent的执行后将返回值交给父View,父View根据子View是否消费了事件再确定自己是否需要消费事件,然后再向自己的父View返回一个表示自己或者自己的子View是否消费了事件的布尔值。如果当前View(ViewGroup)自己或者子View不消费就会将事件转给父View的onTouchEvent方法,所以就会呈现了上面的U型图。分析了源码之后,更能体会这其中设计的精妙

关于事件分发源码我认为最好还是模拟一个事件流,跟着代码走,努力避开各种其他代码的干扰(例如安全检查等),并且从最简单的事件入手,即单指触模、控件不进行滑动、先不考虑ACTION_CANCEL事件。

本文基于Android23的源码进行分析。请对照View和ViewGroup的源码来看本文

首先要明确一个事件序列指的是从手指按下、滑动、抬起的这一个过程中的多个事件。分别是DOWN、MOVE、UP事件。

在这里,假设一个情景,有一个Activity,里面的布局是最外层一个ViewGroup A(充满屏幕),A里面有个ViewGroup B(充满A),B里面有个Button C(比如在屏幕中间)。

情形1:
A完全(即A上的所有点都要拦截)拦截事件(即InterceptTouchEvent直接返回true),现在单指点击了一下A范围任意点,然后滑动并抬起。
(Activity、PhoneWindow 、DecorView的分发就不进行分析了)

现在的事件为ACTION_DOWN。
首先DecorView调用了ViewGroup A的dispatchTouchEvent方法(当然这里只挑关键代码),看到ViewGroup的2103行:

// Check for interception.
            final boolean intercepted;
          
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    //先判断是否允许该ViewGroup拦截事件
                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;
            }

这一段很简单,但是很重要。关键在 intercepted = onInterceptTouchEvent(ev);,大家也很熟悉,此时A要拦截事件的,所以intercepted 为true。

于是,2134行的:

if (!canceled && !intercepted)

里面的语句就不会被执行(里面的语句主要是遍历子View找到消费这一系列事件的子View)

然后来到2239行:

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

mFirstTouchTarget就是找到的那个要消费事件的子View,此时因为根本没有遍历过子View去寻找,所以为null,所以调用:

handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);

这里dispatchTransformedTouchEvent很重要,还要注意第三个参数传了null。

进入dispatchTransformedTouchEvent方法,先看下注释:

Transforms a motion event into the coordinate space of a particular child view,filters out irrelevant pointer ids, and overrides its action if necessary.
If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.

最后一句话说的很明白了,如果child(即第三个参数)为null,则该事件会传给当前的ViewGroup,即A。

对应的代码在dispatchTransformedTouchEvent方法中,处于ViewGroup的2567行:

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

是的,现在调用了

handled = super.dispatchTouchEvent(transformedEvent);

其实就是View的dispatchTouchEvent方法。

在View的dispatchTouchEvent中,关键看View的9285行:

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

可以看到如果View已经设置了onTouchListener并且返回true,则onTouchEvent并不会被执行,并且整个dispatchTouchEvent方法最后也是返回这个result。

View默认onTouchListener为null,所以onTouchEvent会被执行。

onTouchEvent中默认主要是对OnClickListener和OnLongClickListener等事件的处理。看源码View的10288行:

if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) 

这里的判断语句如果条件不成立,即假如View不是Clickable(默认状态)的话,onTouchEvent则返回false,当然也不执行各种点击事件。

此时如果A是默认状态,则A的dispatchTouchEvent返回false给DecorView,它的意义是:A本身和子View都不消耗该事件。因为DecorView只有一个子View A,如果A不消费事件,那么在这一系列事件的后续事件,及MOVE、UP,就不会分发给A了。

如果A是Clickable的(比如被set了OnClickListenrer),则onTouchEvent返回true,那么返回true给DecorView,DecorView就会将后续的事件都传给它处理。而在ACTION_UP事件传递过来的时候,onTouchEvent就会触发OnClickListenrer等的点击事件。

至于为什么,且听后面分解。
在这里,可以知道,在ViewGroup拦截事件的情况下,会通过dispatchTransformedTouchEvent去调用自己的super.dispatchTouchEvent方法最后调用onTouchEvent方法,也就是把自己当做一个View处理事件。

dispatchTransformedTouchEvent很关键,具体下面会说明~~

情形2:
A不拦截事件,B拦截事件,单指点击屏幕任意点,然后滑动并抬起。

同样,首先DecorView调用了ViewGroup A的dispatchTouchEvent方法,这时候intercepted已经是false了,所以到了ViewGroup的2134行的判断

 if (!canceled && !intercepted) 

就需要进入了(此时的cancel为false,一般情况下都为false)。

此时来到ViewGroup的2144行:

if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

目前只看第一个判断actionMasked == MotionEvent.ACTION_DOWN,其实这个判断语句内部是用于找出能够消费Down事件的子View,这个对于此次系列的事件意义重大,我们进入判断语句看。

首先看从ViewGroup的2155行开始的这一段:

 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.
                        
                        //从顶到底的子View集合
                        final ArrayList<View> 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);
                                //如果View不包含触摸点则继续遍历
                                continue;
                            }

很明显,ViewGroup在遍历子View,buildOrderedChildList方法这里是创建了一个从顶到底的子View集合去遍历,所以越顶部的View越优先可以消费事件。最后主要是使用isTransformedTouchPointInView方法判断触摸点是否在子View上。

B是A的子View,并且触摸点在B上,所以不会执行continue,即继续执行后面的代码。
来到ViewGroup的2199行:

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

又见到了isTransformedTouchPointInView方法,
进入该方法,跳过关于ACTION_CANCEL以及多指触控的代码,主要就是一下代码:

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

这里主要是分child是否为null两种情况,在这里先不讨论null的情况,这里的child为通过遍历且确定触摸点在其中的View。

当child不为null的时候,先做一些滑动偏移量处理,然后调用child的dispatchTouchEvent方法。

是的,这里就是真正实现事件分发的地方,开始将事件传递到子View的dispatchTouchEvent方法。在已经确定触摸点在View上的情况下,调用dispatchTransformedTouchEvent方法的作用是通过自View的dispatchTouchEvent的返回值来判断该View是否要消费这个事件,true则为消费,false则不消费。

现在子View为ViewGroup B,B是拦截事件的,所以interceptTouchEvent返回true,所有B也会像情形1中的A一样执行View的dispatchTouchEvent方法,再调用onTouchEvent方法。默认情况onTouchEvent返回false,所以不会走入前面A的

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

里面的代码,而是会走到ViewGroup 2239行的代码:

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

是的,这里的mFirstTouchTarget 是在

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

判断语句内才会被赋值,使得它持有消费了事件的View。
如果B的onTouchEvent返回true,即B消费事件的情况下才会被赋值,它本身为一个ViewGroup内部类TouchTarget的链表,之所以为链表主要是为了多指触摸情况,这里我们暂且认为它代表的是能够消费该事件的View就可以。

走到这里是已经遍历完所有A的子View ,如果遍历完发现没有子View消费事件(mFirstTouchTarget == null),则调用:

handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);

注意到此时child参数传null,回看dispatchTransformedTouchEvent的代码就知道会调用super.dispatchTouchEvent,源码中有关于此的注释:

前面已经说过了,如果没有子View消费,当前ViewGroup就自己调用dispatchTouchEvent尝试去消费。

如果B可以消费事件呢(B的onTouchEvent返回true)?那么在A遍历到B的时候,A的判断语句中的dispatchTransformedTouchEvent就会返回true(当此时B已经执行完onTouchEvent方法),那么就会执行前面列出来的,ViewGroup的2201行开始的代码。

重点是ViewGroup的2215行:

 newTouchTarget = addTouchTarget(child, idBitsToAssign);

看下addTouchTarget方法:

  /**
     * 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为头的链表插入一个新的头节点。不考虑多指触摸情况,就是类似赋值child给mFirstTouchTarget持有的意思。

这里很关键,mFirstTouchTarget 被赋值了,保存的是A中需要消费事件的子View,然后在dispatchTouchEvent剩下的代码中,会在mFirstTouchTarget 不为null的情况下,返回true,向DecorView报告A的子View或自己有View消费事件。

mFirstTouchTarget 有什么意义呢?记得事件分发中有一条规则:
一旦一个View消费了DOWN事件,那么该系列的后续事件都由该View处理。

现在DOWN事件结束了,来了MOVE事件。
同样的A的dispatchTouchEvent方法,又来到ViewGroup 2144行的:

if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE)

判断语句,这次是ACTION_MOVE事件,当然无法进入语句内部,于是乎A遍历子View找出能够消费事件的View都没有执行,直接跳到前面提到的ViewGroup的2240行的代码,然后进入2244行的else语句,其中关键是2251行代码:

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }

alreadyDispatchedToNewTouchTarget 表示是否是新添加TouchTarget的,这个在上一个事件DOWN的时候是被置为true,但是在这次事件中由于没有添加新的TouchTarget,所以为false。

所以会走到2256行:

 if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }

这里的target.child就是mFirstTouchTarget中持有的View,在这里就是ViewGroup B。所以通过dispatchTransformedTouchEvent我们知道这里将当前事件ACTION_MOVE传给了B的dispatchTouchEvent方法。

总的来说就是通过mFirstTouchTarget保存DOWN事件的消费View B,然后在后续的事件直接传给了B的dispatchTouchEvent处理。

此时如果B要消费这个MOVE事件,则handle赋值为true,则A的dispatchTouchEvent返回handle为true。如果B不要消费这个MOVE事件,那么A的dispatchTouchEvent返回false给DecorView。

情形3:
A不拦截事件,B也不拦截事件,单指点击Button,然后滑动并抬起。

其实和情形2很相似了。

首先是Down事件,DecorView调用A的dispatchTouchEvent方法,A因为onInterceptTouchEvent返回false,遍历点击到的子View找到B,调用了B的dispatchTouchEvent方法,B因为onInterceptTouchEvent返回false,遍历子View找到Button C,Button因为本身为Clickable,所以dispatchTouchEvent方法返回true给B,所以B将C记录在B的mFirstTouchTarget中,然后B的dispatchTouchEvent返回true给A,告诉它“我这边可以处理这个事件”,然后A将B记录在A的mFirstTouchTarget中,A的dispatchTouchEvent方法返回true给DecorView。

于是等到MOVE事件下来,A直接找A的mFirstTouchTarget持有的View,即B,B直接找B的mFirstTouchTarget持有的View C,如果C要消费这个事件那还是一路往上返回true。

那如果C这时候不要消费事件呢?那么C的dispatchTouchEvent返回false,B的dispatchTouchEvent返回false,且还不能调用自己的super.dispatchTouchEvent处理,所以又将false返回给A,A也和B一样,所以因为递归最终这个事件交给了Activity处理。

这就是事件分发规则中:
“如果View不消耗DOWN以外的其他事件,则父View不会调用onTouchEvent处理这个事件,同时该View仍然可以继续接收到后续的事件,这些View不处理的事件都交给Activtiy处理”。

那如果View不消耗DOWN事件呢?其实前面情形1已经简单说过了,这里结合A,B,C以及后面两个情形一起来说会更加直观。就是C如果不接受DOWN事件,那么B的onTouchEvent方法会处理事件(不考虑onTouchListener情况),如果B不可以消费DOWN事件,则调用A的onTouchEvent方法处理。所以出现了U型图那样将事件往上抛的情形。
如果B可以消费DOWN事件,那么C的
mFirstTouchTarget就记录为B,此时B的mFirstTouchTarget为null,所以后续事件来到C后,C直接交给了B,B因为事件不是ACTION_DOWN了,所以不会遍历子View,直接判断mFirstTouchTarget是否为null。由于没有遍历子View,所以mFirstTouchTarget仍然为null,所以B会调用自己的super.dispatchTouchEvent处理,以之前分析的类推,后续事件不会被C接收(这部分可以重看下ViewGroup的2144行的判断语句)。

这就对应了另一个事件分发的规律:
一旦一个View在onTouchEvent中不消耗事件,则后续的事件都不会交给他来处理。(看源码发现如果DOWN触摸点在多个View中,应该说是这几个View都不消耗事件为前提?)

最后,当一个系列事件结束之后,新的系列事件来到的时候,会将上一次的 保存的状态清空(比如mFirstTouchTarget),看ViewGroup的dispatchTouchEvent在ViewGroup中的2094行:

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

关于事件分发机制就先简单说到这里,还有很多东西没有提及,比如多指触摸、CANCEL事件等。事件分发由于涉及递归,有时候一层层进入又一层层出来很容易让人迷路。我也是对源码研究不深入,难免有疏漏或者错误的地方,望各位指正~~

发布了69 篇原创文章 · 获赞 76 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/sinat_23092639/article/details/74858558