1、前言

事件分发可以说是 Android 众多知识点中最为基础且最重要的之一了,吃透这块知识点不仅能让你在解决滑动冲突时由仍有余,也能为你在面试过程中轻松过关斩将。

2、MontionEvent + 3个重要方法

MontionEvent

事件分发的起源点来自哪?
答:用户对手机屏幕进行指尖的触摸、亦或是轻轻的抚摸、又或是一顿狂风暴雨般的猛击。

Android 将我们对手机这一系列操作封装到了 MontionEvent 对象中

常见的几种:

事件类型 具体操作
MontionEvent .ACTION_DOWN 手指触摸屏幕(内部对应点击某个View)
MontionEvent .ACTION_MOVE 手指滑动屏幕(内部对应滑动某个View)
MontionEvent .ACTION_UP 手指离开屏幕的一瞬间(与DOWN对应)
3个重要方法

接着我们先来看看事件分发中主要的三个方法,先看看结果再来分析过程,这段简单了解即可,

方法 作用 调用 view中是否存在 ViewGroup中是否存在
dispatchTouchEvent 分发点击事件 当点击事件传递到当前View时 存在 存在
onInterceptTouchEvent 拦截点击事件 ViewGroup 内部的 dispatchTouchEvent() 内部调用 不存在 存在
onTouchEvent 处理点击事件 dispatchTouchEvent()内部调用 存在 存在

三个方法都是 boolean 类型返回值的方法,true 即表示自己消费,false 则继续分发,一段伪代码简单描述上述的三个方法的关系:

//摘自艺术探索public boolean dispatchTouchEvent(MotionEvent ev) {        boolean consume = false;        if (onInterceptTouchEvent(ev)) {            consume = onTouchEvent(ev);        } else {            consume = child.dispatchTouchEvent(ev);        }        return consume;    }

3、Activity 分发

现在我们从源码的角度简单的走一遍分发的流程,当我们的 Activity 接收到用户的触摸屏操作时,便会调用 Activity 的 dispatchTouchEvent(), 如下

 public boolean dispatchTouchEvent(MotionEvent ev) {        //可以忽略这一部分代码        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            //当此activity在栈顶时,点击按home、back、menu等键都会触发此方法            onUserInteraction();        }        //1、点进去会走到Window 类中的 superDispatchTouchEvent        //2、然后 Window 的实现类只有PhoneWindow,类注释中有明确说明        //3、如下有贴出PhoneWindow 的superDispatchTouchEvent        //和 DecorView 的superDispatchTouchEvent,先去看这俩方法的注释再回来        if (getWindow().superDispatchTouchEvent(ev)) {            //4、欢迎回来.若superDispatchTouchEvent返回true,            //即ViewGroup或者View的dispatchTouchEvent返回true,则事件结束            return true;        }        //5、如果ViewGroup或者View都没有消费事件则会返回false,<--面试重点        //那么事件将会交给Activity的onTouchEvent处理。        return onTouchEvent(ev);    } //走进#PhoneWindow@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {        //mDecor 指的是 DecorView 的实例        //DecorView 其实就是 activity 窗口的根视图        return mDecor.superDispatchTouchEvent(event);    }//走进#DecorViewpublic boolean superDispatchTouchEvent(MotionEvent event) {        //这个 dispatchTouchEvent 其实就是调用的 ViewGroup 中的 dispatchTouchEvent        //这个 ViewGroup 就相当于每个界面的顶层View(根View)        //好了,可以回去了...        return super.dispatchTouchEvent(event);    }

该说的都在码里了,我干了,你随意...

这部分只是 Activity 中的分发,接着我们走进 ViewGroup 瞧瞧

4、ViewGroup 分发

接着我们来分析 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;        }

上面的代码主要是描述当前 View 是否拦截点击事件这个逻辑。

第一个 if 判断中:
第一个判断很好理解,根据用户操作手指进行判断;
第二个mFirstTouchTarget: 这个对象结合后面的代码逻辑可知,在 ViewGroup 的子 view 处理成功时,mFirstTouchTarget 会被赋值并指向子 View ,所以反推可知此时的 mFirstTouchTarget == null;那么当事件来到 ACTION_MOVE 和 ACTION_UP 的时候该 if 语句将会不成立,这就导致 ViewGroup 的 onInterceptTouchEvent() 不会被调用,并且 ViewGroup 同一事件序列中的其他事件都会交给它处理;

disallowIntercept默认为false,在代码中我们可以通过requestDisallowInterceptTouchEvent() 来设置,一旦设置之后,ViewGroup将无法拦截除了 ACTION_DOWN 以外的点击事件,为什么说除 ACTION_DOWN 以外呢?因为在 ViewGroup 分发的时候,如果是 ACTION_DOWN 将会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,

以下是事件为 ACTION_DOWN 时重置 FLAG_DISALLOW_INTERCEPT 标记位的源码:

   // 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();  }     private void resetTouchState() {        clearTouchTargets();        resetCancelNextUpFlag(this);        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;        mNestedScrollAxes = SCROLL_AXIS_NONE;    }

根据源码分析可知:
当 ViewGroup 进行拦截事件时,只有事件为 ACTION_DOWN 时才会调用 onInterceptTouchEvent(),并且 FLAG_DISALLOW_INTERCEPT 这个标记位也可以来控制 ViewGroup 是否进行拦截,在处理滑动冲突的时候可以利用这一特性来解决。

以上分析的是 ViewGroup 拦截事件的源码,接着来看看当 ViewGroup 不对事件进行拦截时的情况会是如何:

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);                 // 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);                          continue;                  }                  newTouchTarget = getTouchTarget(child);                 if (newTouchTarget != null) {                 // Child is already receiving touch within its bounds.                  // Give it the new pointer in addition to the ones it is handling.                   newTouchTarget.pointerIdBits |= idBitsToAssign;                    break;             }               resetCancelNextUpFlag(child);                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(event);  } else {      handled = child.dispatchTouchEvent(event);  }

总结一下:
首先会遍历 ViewGroup 所有的子 View ,然后判断子 View 是否有接收到点击事件,如果有则事件便会传递给它,dispatchTransformedTouchEvent 实际上就是调用了子 View 的 child.dispatchTouchEvent 方法,这样事件就从 ViewGroup 传递到了 View上,交由 View 去进行一轮新的分发,这里暂不讨论子 View 具体如何分发,如果 child.dispatchTouchEvent(event) 返回了 true ,那么 if 语句成立 ,mFirstTouchTarget便会被赋值,并且跳出 for 循环,如下所示:

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

还有一种情况便是 child.dispatchTouchEvent(event) 返回了 false ,那么便会接着遍历分发(在还有下一个 View 的情况下),如果遍历完所有的子 View 都没有被处理,要么就是 ViewGroup 没有子 View ,要么就是子 View 内部自己做了处理,在 dispatchTouchEvent 或者 onTouchEvent 方法中返回了 false ;如果是这样的话那么 ViewGroup 便会自己去处理点击事件,如下所示:

          // 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为null时,同样会调用 dispatchTransformedTouchEvent 方法,但是注意第三个参数(View child),传的是个 null,再看一遍:

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

这个判断主要是用来处理当 ACTION_MOVE 和 ACTION_UP 事件到来时,改 if 语句就会不成立,这样就是导致 ViewGroup 的 onInterceptTouchEvent() 不会再被调用,并且同一事件序列中的其他事件都会默认交给它处理。

5、View 分发

View 的事件分发相比 ViewGroup 就相对简单一些了,先来看看它的 dispatchTouchEvent 方法:

 boolean result = false;       ......        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;            }         ......        return result;

从源码中可以看出,会先去判断当前 View 是否有设置 onTouchListener 监听事件,如果有的话,事件会交给 onTouchListener 中的 onTouch 处理,则不会传递到 onTouchEvent 方法,从这里可以说明, onTouchListener 的优先级要高于 onTouchEvent 方法,这样处理的目的就是为了方便用户自己去处理点击事件 。

接着看看 onTouchEvent 的实现,首先来看看 View 处于不可用状态 (指 TextView 、ImageView等)下点击事件的处理过程,如下:

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;        if ((viewFlags & ENABLED_MASK) == DISABLED) {            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {                setPressed(false);            }            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;            // A disabled view that is clickable still consumes the touch            // events, it just doesn't respond to them.            return clickable;        }

很显然,不可用状态下的 View 同样会消耗掉点击事件,即便它看起来是不可用的。

接着往下走,如果 View 有设置代理,那么还会执行 TouchDelegate 的 onTouchEvent 方法,这个机制跟 onTouchListener类似

if (mTouchDelegate != null) {            if (mTouchDelegate.onTouchEvent(event)) {                return true;            }        }

接着往下走,来看看 onTouchEvent 对事件的具体处理

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {            switch (action) {                case MotionEvent.ACTION_UP:                    ...                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {                                              ...                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {                            // This is a tap, so remove the longpress check                            removeLongPressCallback();                            // Only perform take click actions if we were in the pressed state                            if (!focusTaken) {                                // Use a Runnable and post this rather than calling                                // performClick directly. This lets other visual state                                // of the view update before click actions start.                                if (mPerformClick == null) {                                    mPerformClick = new PerformClick();                                }                                if (!post(mPerformClick)) {                                    performClick();                                }                            }                        }                      ...                    break;                   ...            return true;        }

从上面 if 语句就可以看出只要 View 的 CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE(代理),有一个为 true ,便会消费掉事件,不管它的状态是不是可用状态,当 ACTION_UP 事件发生时,会触发 performClick 方法,如果 View 设置了 OnClickListener 监听事件,则会调用它的 onClick 方法

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);        notifyEnterOrExitForAutoFillIfNeeded(true);        return result;

6、总结

关于这个知识点可以从三个步骤进行理解,也是事件分发的流程
Activity --> ViewGroup -->View

Activity

事件分发首先从 Activity 开始进行分发,在 Activity 的 dispatchTouchEvent() 中会将事件分发到 window 上,而 window 是个抽象类,所以会把具体工作交给它的唯一实现类 PhoneWindow 上,而 PhoneWindow 又会将事件传递到 DecorView 上,而 DecorView 其实就相当于当前界面的底层容器,接着这个事件会来到容器的顶层 View ,一般来说这个顶层 View 就是 ViewGroup。

ViewGroup

事件首先会来到 dispatchTouchEvent 方法,如果 onInterceptTouchEvent 方法返回 true 表示对事件进行了拦截,那么该事件便会交给 ViewGroup 的 onTouchEvent 方法进行消费;

如果 onInterceptTouchEvent 方法返回 false 即表示没有对事件进行拦截,那么 ViewGroup 则会去遍历子View ,如果子 View 没有做特殊处理的情况下,事件便会顺利的传递到子 View ;如果子 View 在 dispatchTouchEvent 或者 onTouchEvent 方法中返回了 false ,那么该事件还是会交给 ViewGroup 处理,最终会传递到 Activity 的 onTouchEvent 方法进行消费掉;

View

View 的分发同样是从 dispatchTouchEvent 方法开始,但是 View 没有 onInterceptTouchEvent 方法,当 View 事件传递到 onTouchEvent 之前会先判断是否有设置 onTouchListener 监听,如果有的话事件便交给 onTouchListener 的 onTouch 处理,如果没有设置,则会交给 onTouchEvent 进行处理,当 View 有设置 onClickListener 监听时,事件最终便会传递到 onClickListener 的 onClick 方法。

所以在 View 中的优先级如下:

onTouchListener > onTouchEvent > onClickListener

参考:艺术探索

更多相关文章

  1. 1.4掌握日志工具的使用——Android第一行代码(第二版)笔记
  2. Android(安卓)4.4 meminfo 实现分析
  3. Android中的线程与线程池
  4. Android怎样播放多张图片形成一个动画
  5. Android:Activity与Fragment通信(99%)完美解决方案
  6. 一行代码搞定Android屏幕适配
  7. 第一行代码 Android读书笔记(四)
  8. [置顶] Android触摸事件分发
  9. DataBinding使用教程详解

随机推荐

  1. MySQL事务处理及字符集和校对顺序
  2. java爬虫-初识
  3. 回到基础:优化 JavaScript 的循环[每日前
  4. 网易云音乐评论爬取。
  5. Javascript的对象拷贝[每日前端夜话0x53]
  6. 使用ESLint + Prettier简化代码 Review
  7. Edge 拥抱 Chromium 对前端工程师意味着
  8. 数据整合与数据清洗。
  9. 跟繁琐的命令行说拜拜!Gerapy分布式爬虫管
  10. TensorFlow layers模块用法