一、事件分发机制过程

Android事件分发机制是Android开发必须掌握的东西,分发的事件是点击Touch事件,在Android中对应的是MotionEvent对象。

该对象类型主要有三种:

含义
MotionEvent.ACTION_DOWN 按下View。
MotionEvent.ACTION_UP 松开View。
MotionEvent.ACTION_MOVE 移动View。

整个过程会形成一个事件列:从用户点击View开始传递MotionEvent.ACTION_DOWN事件,然后随着用户移动,会传递N多个MotionEvent.ACTION_MOVE事件,最后用户松开手指,传递MotionEvent.ACTION_UP事件。

整个事件传递的过程就是一个事件分发的过程。这个过程参与的对象主要有Activity->Window->ViewGroup->View。

二、事件分发机制三个重要方法

事件分发机制的三个重要方法如下:

方法 作用
dispatchTouchEvent() 分发事件。
onInterceptTouchEvent() 判断是否进行事件拦截。
onTouchEvent() 点击事件处理。

任玉刚大神的《Android开发艺术探索》里面有一段伪代码表达的非常清楚~

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

对于传入的事件,首先调用diapatchTouchEvent方法开始进行分发,然后调用onInterceptTouchEvent来判断是否进行拦截,如果要拦截,那么事件就在这个ViewGroup进行处理了,onTouchEvent会被调用。如果不拦截事件,就会继续传递给子View的dispatchTouchEvent进行处理。层层传递,直到事件被拦截。

三、源码

源码基于Android7.1。

1、从Activity出发。里面的dispatchTouchEvent方法如下。

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

可以看见调用了getWindow().superDispatchTouchEvent(ev)方法,如果这个方法返回true,就直接返回,即消费了,否则Activity会调用onTouchEvent方法。

2、先看Window的处理,getWindow获取对应的Window,进入Window.java找对应的方法。可以看见是一个抽象的方法。

public abstract boolean superDispatchTouchEvent(MotionEvent event);

具体实现在哪呢?可以看见前面的说类说明有这么一段话。

The only existing implementation of this abstract class is android.view.PhoneWindow

所以具体实现类是PhoneWindow,进入PhoneWindow.java,看superDispatchTouchEvent方法。

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

可以看见直接调用了mDecor的superDispatchTouchEvent方法,即Windows其实对事件没有进行任何处理。

mDecor是什么呢?

// This is the top-level view of the window, containing the window decor.private DecorView mDecor;

下面有一张图其实比较清楚的能展示DecorView。

【Android】事件分发机制_第1张图片 DecorView.png

可以看见DecorView其实就是我们的顶层View,它是一个FrameLayout布局,从源码也可以看见它是继承自FrameLayout。里面是一个线性布局,包含一个TitleBar一个content,content就是我们每次在setContent的内容,即我们自定义的布局。

public class DecorView extends FrameLayout

说到这里呢,再返回去看superDispatchTouchEvent函数,这个调用了super的dispatchTouchEvent方法,一直往父类看,可以看见super的dispatchTouchEvent方法在FrameLayout没有实现,再往上是ViewGroup类,这里面就有实现了,所以DecorView其实直接调用了ViewGroup的dispatchTouchEvent方法。

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

3、看ViewGroup的dispatchTouchEvent方法。

可以说代码巨长无比,但是总体思路和前面的伪代码一样,我们挑重点。

    // Check for interception.    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;    }

intercepted是来标记是否在这个地方进行拦截,先不管最外面的else,先从最里面看起,最里面调用了onInterceptTouchEvent来判断是否拦截,即伪代码里面的内容。

往外有个判断disallowIntercept,如果mGroupFlags有FLAG_DISALLOW_INTERCEPT这个标记的话,直接就不拦截,FLAG_DISALLOW_INTERCEPT这个是什么呢?这个其实是在子View里面设置的,即不允许父容器拦截,设置这个标记之后,父容器就不进行拦截。

但是往前还有这么一段话。

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

即当MotionEvent.ACTION_DOWN事件的时候,会执行resetTouchState函数,这个函数里面有个处理就是去掉这个标记。所以开发中有时候遇到requestDisallowInterceptTouchEvent设置失效,就是在这里失效的。

mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;

再往外有一个mFirstTouchTarget这个标记,这个标记是啥呢?mFirstTouchTarget的意思是,如果ViewGroup的有子元素成功处理,mFirstTouchTarget就会指向该元素。
如果onInterceptTouchEvent()返回true,说明ViewGroup拦截事件,mFirstTouchTarget为null,同一序列的事件都由它处理,onInterceptTouchEvent也不会再调用了,因为actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null条件都不满足。

最后看看onInterceptTouchEvent方法,可以看见,默认是不拦截的。

public boolean onInterceptTouchEvent(MotionEvent ev) {    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)            && ev.getAction() == MotionEvent.ACTION_DOWN            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)            && isOnScrollbarThumb(ev.getX(), ev.getY())) {        return true;    }    return false;}

分析到这里,先得出一点小结论:

  • 当ViewGroup开始拦截事件之后,后面的事件列都会交给它,且不调用onInterceptTouchEvent方法。
  • 子元素可以设置FLAG_DISALLOW_INTERCEPT标记,这样父元素ViewGroup就不会进行拦截,但是有个前提就是父元素一开始不拦截MotionEvent.ACTION_DOWN。
  • 只有diapatchTouchEvent一定每次调用,onInterceptTouchEvent不一定每次调用。
  • ViewGroup的onInterceptTouchEvent方法默认是不拦截的。

继续,如果不拦截的话会干嘛呢?即在if (!canceled && !intercepted)这个if条件里面,直接看里面关键部分。

final View[] children = mChildren;for (int i = childrenCount - 1; i >= 0; i--) {    final int childIndex = getAndVerifyPreorderedIndex(                childrenCount, i, customOrder);    final View child = getAndVerifyPreorderedView(                preorderedList, children, childIndex);    ...}

这里是遍历所有的子View。之后怎么处理呢,往下看。接下来有几个判断。

if (!canViewReceivePointerEvents(child)        || !isTransformedTouchPointInView(x, y, child, null)) {    ev.setTargetAccessibilityFocus(false);    continue;}

canViewReceivePointerEvents函数。从代码可以看出这个是在判断View可见并且没有播放动画才可以接收。

/** * Returns true if a child view can receive pointer events. * @hide */private static boolean canViewReceivePointerEvents(@NonNull View child) {    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE            || child.getAnimation() != null;}

isTransformedTouchPointInView函数。从代码可以看出必须要点在View内才可以,这里是用View的Top和Left参数判断,即View的真身。

/** * Returns true if a child view contains the specified point when transformed * into its coordinate space. * Child must not be null. * @hide */protected boolean isTransformedTouchPointInView(float x, float y, View child,        PointF outLocalPoint) {    final float[] point = getTempPoint();    point[0] = x;    point[1] = y;    transformPointToViewLocal(point, child);    final boolean isInView = child.pointInView(point[0], point[1]);    if (isInView && outLocalPoint != null) {        outLocalPoint.set(point[0], point[1]);    }    return isInView;}/** * @hide */public void transformPointToViewLocal(float[] point, View child) {    point[0] += mScrollX - child.mLeft;    point[1] += mScrollY - child.mTop;    if (!child.hasIdentityMatrix()) {        child.getInverseMatrix().mapPoints(point);    }}

综上,遍历之后判断子元素View是否可以接收事件的条件有三个:

  • View可见
  • View没有在播放动画
  • 点击的点的坐标在View里面

然后,如果可以接收,继续执行。

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

接着一句句看上面的部分,首先是dispatchTransformedTouchEvent函数。直接看关键代码,一般关键代码都在后面。

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

可以看见,如果子View不为null,则调用child.dispatchTouchEvent(transformedEvent)将事件分发下去。如果子元素处理了,那么dispatchTransformedTouchEvent会返回true。返回true之后会调用执行newTouchTarget = addTouchTarget(child, idBitsToAssign),这里就是对mFirstTouchTarget 进行了赋值,前面讲的mFirstTouchTarget 就是在这里赋值的。

如果在遍历完子View以后ViewGroup仍然没有找到事件处理者即ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup会去处理这个事件。即dispatchTransformedTouchEvent返回了false,那么mFirstTouchTarget 就为null。

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

这时候还是调用dispatchTransformedTouchEvent,只是有一个不同是此时第三个参数也就是child传入了null。再贴一次代码,可以看见此时调用super.dispatchTouchEvent(transformedEvent),即调用父类View的dispatchTouchEvent方法。

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

4、此时看View.java的dispatchTouchEvent方法。

看最关键的几句话。

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且这个监听里面的onTouch返回true,那么onTouchEvent就不被调用。反之,调用自身的onTouchEvent方法。

接下来看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.    return (((viewFlags & CLICKABLE) == CLICKABLE            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);}

首先,如果View被设置为disable,那么只要CLICKABLE和LONG_CLICKABLE有一个为true,就一定会消费这个事件。View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false。

if (((viewFlags & CLICKABLE) == CLICKABLE ||            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {    ...}

接下来,即View为enable的时候,只要CLICKABLE和LONG_CLICKABLE有一个为true,也会消费这个事件。里面是一些列的action判断,这里主要看ACTION_UP,执行了一个performClick()方法。这个方法即如果设置了mOnClickListener监听,那么就会执行对应的监听里面的onClick方法。

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

从这里可以总结出优先级OnTouchListener>OnTouchEvent>OnClickListener,当设置了OnTouchListener且OnTouch方法返回true的时候,就不再执行后面两个。

同时可以看见,如果View全部不进行消费,那么事件又会一层层回传,直到Activity那儿执行onTouchEvent方法。回到Activity的dispatchTouchEvent方法。

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

看下对应的onTouchEvent方法,这里判断了一个shouldCloseOnTouch方法,这个其实只是判断是否点在了空白区域,所以点击空白区域会关闭就是因为这里执行了finish()。

public boolean onTouchEvent(MotionEvent event) {    if (mWindow.shouldCloseOnTouch(this, event)) {        finish();        return true;    }    return false;}

四、太乱了,总结一下

所有入口都是dispatchTouchEvent,从Activity->Window->ViewGroup->View依次传入,如果onInterceptTouchEvent为true(不一定都执行)拦截,否则继续一级级往下传。事件处理onTouchEvent为true则消费掉,否则原路返回一级级往上传。

任玉刚大神的《Android开发艺术探索》给出了11点结论帮助理解。

1、同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。

2、正常情况下,一个事件序列只能被一个View拦截且消耗。因为前面源码分析过了,当一个事件交给一个View执行之后,就不再执行onInterceptTouchEvent进行判断了。但是通过特殊手段可以使得事件列里面不同事件被不同View处理,比如一个View本该处理然后通过onTouchEvent强行转给其他View。

3、某一个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递到它的话),并且它的onInterceptTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个时间序列内的其他方法都交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截啦。参考2。

4、当某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了,两者类似。

5、如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失(当然了:点击事件需要消耗,DOWN 和 UP事件的),此时父元素的onTouchEvent并不会被调用,并且当前的View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

6、ViewGroup默认不拦截任何事件。Android源码中的ViewGroup的onInterceptTouchEvent方法默认不拦截false。

7、View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

8、View的onTouchEvnet默认都会消耗(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分请情况,比如Button的clickable属性默认为true,而TextView 的 clickable属性默认为false。

9、View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或则longClickable有一个为true,那么它的onTouchEvent就返回true。

10、onClick会发生的前提是当前View是可点击的,并且他收到了down和up的事件。

11、事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

更多相关文章

  1. android获取各种系统路径的方法
  2. android 应用如何获取系统权限 以及root系统方法
  3. Android 和 JS 交互时调用不成功解决方法
  4. android应用程序最小化的处理方法
  5. Android新线程中更新主线程UI中的View方法汇总
  6. Android 点击事件分发
  7. 彻底解决Android 应用方法数不能超过65536的问题
  8. android install faild insufficient storage错误的解决方法

随机推荐

  1. Android根据输入银行卡号判断属于哪个银
  2. Android(安卓)Studio 初体验
  3. This Android(安卓)SDK requires Android
  4. RelativeLayout用到的一些重要的属性(自
  5. 如何发布你的Android应用程序
  6. Android(安卓)Studio vs. Eclipse ADT Co
  7. Android(安卓)TextView 换行
  8. Professional Android(安卓)2 Developmen
  9. Android项目中图标的更改
  10. android xml布局文件属性说明