一、事件分发机制过程

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。

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(安卓)菜单(OptionMenu)大全 建立你自己的菜单
  3. Android的线程
  4. android获取各种系统路径的方法
  5. Android(安卓)编程下 Touch 事件的分发和消费机制
  6. Android修行之路——Android程序设计基础(一)
  7. MediaRecorder流程分析
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. Android适配全攻略
  2. Android 移植到 C#
  3. webview
  4. Android高仿网易新闻客户端之侧滑菜单
  5. Android MuPDF 部署
  6. Android Binder入门指南之addService详解
  7. Android,谁动了我的内存
  8. 使用U3D 实现 Android(安卓)Launcher(提
  9. android 按行读取txt文本内容
  10. Android(安卓)Studio 工程.GitIgnore应该