美文网首页
Android事件分发机制

Android事件分发机制

作者: baifanger | 来源:发表于2020-08-17 18:40 被阅读0次

1.事件基础知识

所谓事件,就是用户手触摸到屏幕时,产生的一系列 Touch 事件,无论是用户的拖拽,点击,还是多点多点放大,缩小等操作,都与事件相关。我们此次只讨论单指点击,移动的情况。

MotionEvent事件类型
事件类型 动作 代码值
MotionEvent.ACTION_DOWN 手指按下屏幕(view,所有事件的开始) 0
MotionEvent.ACTION_MOVE 在屏幕上滑动,会多次触发 2
MotionEvent.ACTION_UP 手指抬起(与Down相对应) 1
MotionEvent.ACTION_CANCEL 结束事件,事件被上层拦截时触发 3
事件传递涉及方法
方法名 作用 拥有者 返回值 执行顺序
dispatchTouchEvent 对事件进行分发 Activity, ViewGroup, View都有此方法 true:分发;false:不进行分发,其值由子viewa或孙子view的onTouchEvent等决定 1
onInterceptTouchEvent 是否拦截此事件,拦截后交由自己的onTouchEvent处理 (只有ViewGroup有) true:进行拦截,拦截后,事件交由自己处理;false:不进行拦截 2
onTouchEvent 对事件的处理 Activity, ViewGroup, View都有此方法 true:事件已消费;false:事件未消费 3

2.事件是如何传递的

1)事件的开始

所有的事件,都会按照由Activity-->ViewGroup-->View的顺序进行分发,所以,事件开始的入口,便是Activity.dispatchTouchEvent()方法。通过 Activity 源码

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

通过源码可以发现,最终交给了 getWindow() 的实现类进行了事件的处理,而 getWindow() 实际上就是 PhoneWindow ,所以,我们进入 PhoneWindow的代码看一下:

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

而了解过源码的人知道,mDecor 其实是FrameLayout的子类,这也是我们平时说的界面的根布局是一个 FrameLayout的原因,而FrameLayout又是ViewGroup的子类,ViewGroup又是View的子类,所以,默认情况下,我们研究事件的分发,最终是在看 ViewGroup以及View中对上述提到的三个方法的处理

2)事件传递的过程
UI2.png
如图,当Down事件过来后,activity.dispatchTouchEvent,不会进行任何的拦截,就会交由他的child ViewGroup1来处理,即调用ViewGroup1.dispatchTouchEvent方法,该方法内部,会调用ViewGroup1.onInterceptTouchEvent来判断ViewGroup1是否对此事件进行拦截,如果拦截,将交由自己的onTouchEvent来处理,如果不拦截,会将事件交由自己的child ViewGroup2来处理。如此递归循环,一直到最内层的一个view,并将结果逐步返回。onTouchEvent,用来处理点击事件,若消耗了此事件,则之后的事件会接着收到,若不消耗,则在同一事件序列中,此View无法接收到事件。同时,此事件会交由它的父类的onTouchEvent来处理,如果父类不处理,则继续向上抛,直到Activity.onTouchEvent().
伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}
3)事件传递图解
事件流程 2.png
注意:

在dispatch,onIntercept,onTouchEvent三个方法并不是单独存在的,尤其是后两个方法,它他是在dispatch中被调用的,默认情况下,最内层View.onTouchEvent的返回值会影响到View.dispatchTouchEvent的返回值,而该值又回影响到其父类View的返回值

3.源码分析

界面构造图.png
1)点击View1,Down事件如何传递

首先,Down会经过Activity,DectorView的dispatchTouchEvent,传递到ViewGroup1.dispatchTouchEvent中.因此,我们直接进入源码ViewGroup的该方法中查看

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

disallowIntercept==false ---> onInterceptTouchEvent ---> intercepted==false
接下来会走到

 if (!canceled && !intercepted) {
   if (newTouchTarget == null && childrenCount != 0) {
     if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        mLastTouchDownTime = ev.getDownTime();
    **代码省略**
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
      }
    }
 }

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

当ViewGroup1不进行拦截时,会遍历子View,通过dispatchTransformedTouchEvent方法将事件传递给子View,下面看一下该方法

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    ***省略***
    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不为空,所以在此方法中,会形成递归,继续传递事件,一直到最内存的view为止,因此,事件最终会交由View的dispatchTouchEvent来处理。下面看一下View的disptchTouchEvent源码

if (onFilterTouchEventForSecurity(event)) {
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    //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的方法中,会根据onTouchEventListener以及onTouchEvent的处理,返回结果,最终此结果会回到ViewGroup if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 这里,如果有子view消费了此事件,就会进入if判断,从代码上可以看到,newTouchTarget,mFirstTouchTarget会赋值,而alreadyDispatchedToNewTouchTarget = true;接下来看ViewGroup之后的代码

if (mFirstTouchTarget == null) {
       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;
    }
}

由于mFirstTouchTarget已有值,所以会走到else,在这里面,会命中代码if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
,然后dispatchTouchEvent会返回true
以上便是Down事件,在父View不拦截的情况下的处理流程,如果是拦截的话,我们看一下代码执行
如果是拦截的话 if (!canceled && !intercepted)这个不成立,就不会有遍历子view的情况,那么 mFirstTouchTarget便是null, 因而会走到

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

此方法,最终会交给super.dispatchTouchEvent,即view对该方法的实现。这也是我们平时说的,当子view不处理事件时,会交给父view,如果父view也不处理,就会再交给爷爷view处理

Down事件总结

dispatchTouchEvent确定事件给谁
1.先看自己是否拦截,如果不拦截:
2.分发下去:排序、遍历、领取事件的View处理事件结果
3.如果子view没人处理事件,则交由自己的onTouchEvent,看自己是否消费此事件
4.如果拦截,其后续流程和没有子view消耗该事件是一样的

2)点击View1后,Move事件如何传递

首先明确一点,Move事件,依然后经Dector-->ViewGroup-->View的顺序,通过dispatchTouchEvent进行传递

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) {
}

注意,当move事件过来时,alreadyDispatchedToNewTouchTarget会置为false,同时,上面代码中的if,由于是MOVE事件,if中的代码将不再执行。

if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
} else {
    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;
               }
     }
}

由于mFristTouchTarget!=nullalreadyDispatchedToNewTouchTarget==false,故move事件最终给了else中的dispatchTransformedTouchEvent处理,因此,move事件,一层层传入了之前保存的 target.child中,并返回处理结果 true

Move事件拦截问题
Move事件的传递相对来说就上是面的流程了,还有一点需要注意的是Move事件的拦截问题,看代码

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

在ViewGroup.dispatchTouchEvent方法中,有这样的代码,MotionEvent.ACTION_DOWN || mFirstTouchTarget,意味着,除了Down事件,当move事件时,由于 mFirstTouchTarget!=null,也会走是否进行拦截的判断,而此时需要依赖mGroupFlags & FLAG_DISALLOW_INTERCEPT的结果,这两个参数在ViewGroup.requestDisallowInterceptTouchEvent中会被更改,也就是说,如果子View拿到父ViewGroup,便可调用此方法,在Move事件时,更改父View是否可以拦截事件。如果是这样处理,那以下代码的逻辑便又会有变化

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

此时的move事件,由于intercepted=true,故cancelChild = true,那么再看dispatchTransformedTouchEvent中的处理

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

此时,由于cancel==true,所以该事件的会被改为ACTION_CANCEL,进而调用View.dispatchTouchEvent,查看View的该方法,可看到结果返回false,同时,在ViewGroup中,会走以下的逻辑

if (cancelChild) {
    if (predecessor == null) {
        mFirstTouchTarget = next;
    } else {
        predecessor.next = next;
    }
    target.recycle();
    target = next;
    continue;
}

从之前的代码中,可以看到 mFirstrTouchTarget.next==null,因此,此逻辑走完后, mFirstTouchTarget==null, target==null,当下一次Move事件到来后,在是否进行拦截的判断actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null
这里,无法进行,就会走 else逻辑:intercepted = true;, 对事件进行拦截,然后执行以下代码逻辑

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

即随后的move事件将都由该ViewGroup进行处理。

Move事件总结

dispatchTouchEvent确定事件给谁
1.先看自己是否拦截,如果不拦截:
2.分发下去:直接分发给down事件确定的view处理
3.网上说的Down事件如果没接收到,则再也不能接到任何事件,这样的说法是有问题的,这样的说法只适合叶子结点,如果是非页子结点,子View可以通过requestDisallowInterceptTouchEvent来设置父ViewGroup是否对事件进行拦截

3)dispatchTouchEvent,onclick, onTouchListener的关系
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}

if (!result && onTouchEvent(event)) {
    result = true;
}

以上为View.dispatchTouchEvent方法中的代码,从上面可以看到,当给View添加了onTouchListener后,事件到来后,会先响应 mOnTouchListener.onTouch方法,如果该方法返回true,则下面的 if中的onTouchEvent(event)就不会执行,否则,会运行onTouchEvent。那再看一下onTouchEvent方法

if (mPerformClick == null) {
    mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
    performClick();
}

该方法中,重点为当事件为UP时,会有performClick的处理,再看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;
    }
    return result;
}

可以看到,在该方法中,会判断view是否有clickListener,然后进行调用,并且有返回值,这也是为什么我们重写clickListener时,方法中不用写返回值,而onTouchListener则需要

Click, TouchEvent,TouchListener总结

View.dispatchTouchEvent中,会先对onTouchListener的设置进行其处理,再根据其结果,判断是否调用view的onTouchEvent方法
在View.onTouchEvent中,当事件为UP时,进行clickListener的调用,且有返回值
给view同时添加onTouchListener和onClickListener时,onTouchListener方法的返回值需为true,否则onClickListener也会响应。

4.发散功能,Spannable

TextView可以添加Spannable,当为textView添加了clickListener及spannalbe后,点击spannable时,click也会响应。

相关文章

网友评论

      本文标题:Android事件分发机制

      本文链接:https://www.haomeiwen.com/subject/cwkbrktx.html