事件分发源码详解

835 阅读6分钟

书接上文:事件分发浅析 - 仿京东首页二级联动

结构

事件

事件就是MotionEvent对象。

ACTION_DOWN:事件开始,主要处理事件的分发,分发完了会给mFirstTouchTarget赋值(处理事件的子View链表)

ACTION_MOVE:move事件会多次触发,对于View来说,没有down,一定不会有move。但是ViewGroup,没有down也会有第二次move以后的事件。因为是多次触发的,所以看源码得多次看

ACTION_UP:事件结束

ACTION_CANCEL:在Move事件的时候,会赋值成cancel,第二次move以后可以被上层拦截

image.png

View与ViewGroup结构

1.ViewGroup extends View。
所以在ViewGroup没有实现的方法会调用View方法。ViewGroup是没有实现事件处理的,所以都会调用View的事件处理dispatchTouchEvent

2.从布局角度,View的父亲一定是ViewGroup。
所以ViewGroup一层一层的下发事件,给View。ViewGroup中可能会将事件发给自己的子View,或者调用自己的(extends View)的dispatchTouchEvent来处理。

事件处理: View.java --> dispatchTouchEvent
事件分发: ViewGroup.java --> dispatchTouchEvent

事件冲突

事件只有一个,但是有多个人想处理,如果处理的对象不是我们想给的对象,那么就是说发生了事件冲突。

解决:
解决事件冲突有两种:内部拦截和外部拦截。
子View去处理,叫内部拦截。他爹ViewGroup去处理,叫外部拦截。

整体流程

image.png

事件处理 - View

先结论,事件处理就View的onClick和onTouch。
onTouch先执行,onClick后执行。
当onTouch返回false的时候,onClick会执行。
当onTouch返回true的时候,onClick不执行。表示事件onTouch消费了。onClick没有返回值是因为,执行onClick的时候事件就已经消费了。

image.png

源码

View.java 的dispatchTouchEvent()

往下看 image.png
核心代码 image.png

事件处理的核心代码就这么一点
image.png

1.Java中的 && 处理的逻辑是这样的,if(a && b && c) 当a=false,直接不执行b。b=false,不执行c
image.png

ListenerInfo li = mListenerInfo;这个是啥呢?

image.png image.png
点进getListenerInfo()
image.png

所以前面几个条件中的mListenerInfo就是setOnTouchListener中new的OnTouchListener,也就是只要实现了,setOnTouchListener那么前几个条件都会成立,所以必会执行li.mOnTouchListener.onTouch(this, event))

点进li.mOnTouchListener.onTouch(this, event))发现是个接口,
也就是会执行到自定义的onTouch image.png image.png

自定义的onTouch里的返回值true,false就会影响到View.dispatchTouchEvent()的返回值。所以onTouch里返回true就代表事件被消费了,false=没有消费
image.png

所以自定义的onTouch里的返回值true,
result = true;
if (!result && onTouchEvent(event))第一个条件不成立,就不会执行onTouchEvent(event)
也就是不会执行onClick()在onTouchEvent(event) ACTION_UP方法里

image.png

onTouchEvent(event)
onTouchEvent()里有down/up等各种事件的处理。其中onClick在Up事件里。

onTouchEvent的up事件里有一个performClick image.png
点进去
image.png

performClick()方法
1.处理按键声音
2.处理onClick
3.result = true;
这样事件的处理就完事了

image.png

事件分发 - ViewGroup

事件分发是在ViewGroup中的dispatchTouchEvent(),这里的源码是重点。

精简源码

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        //...省略
        if (onFilterTouchEventForSecurity(ev)) {
            //...省略
            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 {
                intercepted = true;
            }

            //...省略

            /**事件分发*/
            if (!canceled && !intercepted) {

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

                    //...省略

                    //子View个数
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        /**遍历子View*/
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {

                            //...省略

                            /**判断子View是否需要分发*/
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

                                //...省略

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

                        }
                    }
                }
            }

            /**具体执行分发*/
            //是否拦截
            if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {

                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) {
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
        }
        return handled;
    }

事件分发结构 -- Down事件!

image.png

1.清空缓存

跳过一些辅助功能等代码

image.png

2.拦截判断

1.如果是ACTION_DOWN 或 mFirstTouchTarget != null(刚开始一定是null的,没有走遍历子View之前)
2.子View没有设置不可拦截(尚方宝剑 requestDisallowInterceptTouchEvent()
3.一般都走这onInterceptTouchEvent(),重点方法

image.png

所以自己处理是否拦截,需要重写onInterceptTouchEvent()方法,返回true表示拦截

image.png

3.拦截判断 - 拦截时(onInterceptTouchEvent()返回true

中间大段代码直接跳过。 直接走最后的分发执行流程

dispatchTransformedTouchEvent()这个方法是事件分发的处理方法,重点!!!,拦截时child为null image.png

点进去:
当child为空时,调用的 handled = super.dispatchTouchEvent(transformedEvent);
也就是调用的自己的(Viewgroup)extends(继承的) View中的dispatchTouchEvent

返回值:handled就是是否处理完事

image.png

4.拦截判断 - 不拦截时 (onInterceptTouchEvent()返回false

  1. 不拦截时,会走 if (!canceled && !intercepted)

image.png

  1. down事件的正常流程

1.先判断是否是down事件
2.清空指针
3.判断子View是否有,有的话,根据xml顺序倒叙插入到一个ArrayListArrayList(xml里是textView/ImagView... 那么存的时候List(0) =ImagView,list(1) = textView )
4.for循环从后开始取,这样取出来又是根据xml的顺序排的了

image.png

  1. for遍历子View,判断是否分发

重点!!! if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))这个方法来判断儿子是否消费,不消费就for下一个,消费那就addTouchTarget添加到mFirstTouchTarget然后break跳出循环。

image.png

3.1 点进dispatchTransformedTouchEvent方法

点进dispatchTransformedTouchEvent方法,当child不为空时,会调用
handled = child.dispatchTouchEvent(transformedEvent);
也就是当child为view时,会调用View的dispatchTouchEvent去处理事件,当child为ViewGroup时,会调用ViewGroup的dispatchTouchEvent去(递归)分发事件,再去遍历儿子的儿子...一层层下去。

image.png

  1. 确定分发

image.png

这里重点三(第7步用):
target.next = null
mFirstTouchTarget = target; !=null
alreadyDispatchedToNewTouchTarget = true

image.png

  1. 如果一顿for遍历下来,没有子类处理这个事件,也就是所有子View的
    dispatchTransformedTouchEvent()都为false,那么走之前拦截的代码

MNKV.png

  1. 如果有要处理的子View,addTouchTarget以后break;跳出for循环,走mFirstTouchTarget!= null 去分发事件

image.png

  1. 分发事件里是while (target != null) 这个while循环一般只会走一次(单点触摸的时候,多点触摸走多次)。

第五步:for循环找子View处理的时候,其实已经事件分发给View完事了,所以这里直接标记handled =true返回

image.png

事件分发 -- Move事件!

down事件已经确定了具体哪个View来消费事件,但是Move事件还是会总顶层ViewGroup开始走

1.清缓存这步不走 image.png

  1. 因为mFirstTouchTarget != null 所以还是会走是否拦截的流程

image.png

  1. (如果本来就没拦截) 那么if (!canceled && !intercepted) 这里依然会走。down的时候分发事件完事了,所以不会走子View分发事件了

image.png

  1. 跳过分发事件代码以后,就到最后一步,因为mFirstTouchTarget!=null ,所以会走else

image.png

  1. dispatchTransformedTouchEvent()分发事件,分发给之前存下来的子View

image.png

事件冲突解决

事件冲突只能在Move时候处理!!!

事件冲突的解决方法分为内部拦截(子View处理)和外部拦截(父容器处理)。

内部拦截

内部拦截,子View中的dispatchTouchEvent通过 getParent().requestDisallowInterceptTouchEvent来设置父亲是否有权拦截

getParent().requestDisallowInterceptTouchEvent();这个方法就是上面代码的尚方宝剑。子View可以设置父容器不让他拦截。

image.png

但是这样写完还不够,因为ViewGroup DOWN的时候会清空缓存,所以子View怎么设置都不行,所以还得处理父亲的Down

image.png

外部拦截

外部拦截是通过设置父容器的onInterceptTouchEvent 来控制,父容器是否拦截

image.png