第六章 Android 开发中的View和事件分发机制

1. 概述

  作为Android开发中最常见的一个控件,个人觉得有必要谈谈了。我们刚开始接触Android的时候最常见的一些基本控件就有TextView,EditText,Button......,但是细心的你会发现它们的父类都是View。但是View也不能单纯是一个控件,它应该算是一个体系。我们在进行Android开发的时候,有时候一些系统提供的控件不能满足我们的需求,这时候我们需要对进行自定义控件的编写,定制符合我们要求的控件,这样才能满足我们的功能需求。
  在介绍View的时候,首先想提供这样一个知识路线图,View的基础概念,让我们知道什么是View;View的位置参数,了解View的移动;View的触碰事件,了解事件分发机制;View的滑动冲突处理 以及View的一些用法。

2. 认识View

  View是Android中所有控件的基类。同时,它也是界面层所有控件的一种抽象,它代表了一个控件。或者这样说,View既可以作为一个控件来使用,也可以是一个基类,许多基础控件都是继承View的。
  ViewGroup和View的关系。Android中的ViewGroup也是继承View的。ViewGroup,翻译成控件组,意思是很多View控件的集合。ViewGroup的内部包含了很多控件,也可以说ViewGroup包含了许多的View。这意味着View本身可以是单个的控件,也可以是很多个控件组成的一组控件。通过这种关系形成了一个View树结构。
  举个栗子,我们知道TextView是一个View, 而RelativeLayout不但是一个View,还是一个ViewGroup;再举个栗子,我们知道数学中的集合,一个集合中有许多子集合,但同时,这些子集合也包含一些更小集合。子集合就相当于自己是一个View,并且还是一个ViewGroup。所以说ViewGroup内部是可以有子View的,子View同样还可以是ViewGroup,以此类推。这种层级关系有助于我们了解View的结构机制。

3. View的位置参数

  我们知道数学系中的坐标轴,一个原点,两条带方向箭头的坐标线;Android中的位置参数也是如此,但是顶点的坐标不一样。方向也不一样,Android中的View坐标以左上角为顶点,向右和向下为递增方向。

坐标方向

  View的位置主要是由它的四个顶点的来决定的,分别对应了View的四个属性: top , left , right , bottom 。它们都是以顶点为参照点,top是左上角纵坐标,left是左上角横坐标,right是右下角的横坐标,bottom是右下角的纵坐标。它们的坐标位置都是相对于父容器来说的。这是一种相对坐标。它们的关系如下图所示:

View的位置坐标和父容器的关系

  在Android中,X轴和Y轴的正方向是右和下,不仅如此,大部分显示系统都是按照这个标准来定义坐标系的。所以在计算View的宽高的时候,我们这样计算:

width = right - left
height = bottom - top

  Android 还为我们提供了获取View的left,top,right,bottom四个参数的方法。

  • Left = getLeft();
  • Right = getRight();
  • Top = getTop();
  • Bottom = getBottom();

  从Android 3.0开始对View增加了额外的参数: x, y,translationX,translationY。其中x,y是View的左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,其中translationX和translationY的默认值是0,和View的四个基本的位置参数一样,它们的换算关系:

x= left + translationX
y = top + translationY

注意:当View发生平移的时候,top和left表示的是原始左上角的位置信息,它的值不会发生改变,此时发生改变的是x,y,translationX,translationY这四个参数。这样我们就可以通过了解View的位置以及偏移量来了解它的运动轨迹。

4. View的事件分发

4.1 典型的分发事件

  因为我们的是移动设备,屏幕触碰是基本的要求。了解触碰事件(MotionEvent),认识一下事件的分发机制是很有必要的。手指触碰屏幕以后会产生一系列的事件,典型的事件有下面几个:

  • ACTION_DOWN ——手指刚接触屏幕

  • ACTION_MOVE ——手指在屏幕上移动

  • ACTION_UP ——手指从屏幕上松开

所以当我们将手指触摸屏幕的话,考虑如下几种情况:

  • 屏幕点击一次就离开: DOWN--------->UP

  • 屏幕点击且按住滑动再离开:DOWN---->MOVE...----->MOVE------->UP

  上述三种情况是典型的事件序列,同时通过MotionEvent对象,我们可以得到点击事件发生的x和y坐标。系统提供了两组方法:getX/getY 和getRawX/getRawY。这两组方法的不同之处在于参照对象的不同,getX/getY返回的是相对于当前View的左上角的x和y坐标,而getRawX/getRawY是相对于手机屏幕左上角的x和y坐标。

4.2 View的事件分发机制

  点击事件的事件分发,其实是对MotionEvent事件的分发的过程。即当一个MotionEvent产生以后,系统需要把这个事件传递给一个具体的View,这个传递的过程其实就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成。

public boolean dispatchTouchEvent(MotionEvent ev)

   用来进行事件的分发,如果事件能够传递给当前的View。那么这个方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消耗当前事件。
  Touch 事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会以隧道方式(从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递)将事件传递给最外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由该 View 的 dispatchTouchEvent(MotionEvent ev) 方法对事件进行分发。

public boolean onInterceptTouchEvent(MotionEvent ev)

  在上述的方法内部调用,用来判断是否拦截当前事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,这个方法不会被调用。返回结果表示是否拦截当前事件。
  在外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系统默认的 super.dispatchTouchEvent(ev) 情况下,事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件拦截逻辑如下:

  1. 如果 onInterceptTouchEvent 返回 true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;
  2. 如果 onInterceptTouchEvent 返回 false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;
  3. 如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),事件默认不会被拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理。

public boolean onTouchEvent(MotionEvent event)

  在dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消费当前事件,如果不消费,则在同一事件序列中,当前View无法再次接受事件。
  在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。onTouchEvent 的事件响应逻辑如下:

  1. 如果事件传递到当前 View 的 onTouchEvent 方法,而该方法返回了 false,那么这个事件会从当前 View 向上传递,并且都是由上层 View 的 onTouchEvent 来接收,如果传递到上面的 onTouchEvent 也返回 false,这个事件就会“消失”,而且接收不到下一次事件。
  2. 如果返回了 true 则会接收并消费该事件。
  3. 如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。

  Android 中提供了View ,ViewGroup,Activity三个层次的Touch事件处理。处理过程是按照Touch事件从上到下传递,再按照是否消费的返回值从下往上传递。如果View的onTouchEvent返回false,将会向上传给它的parent的ViewGroup,如果ViewGroup不消费,会往上传给Activity。

即隧道式向下分发,然后冒泡式处理

  onInterceptTouchEvent用于改变事件的传递方向。决定传递方向的是返回值,返回为false时事件会传递给子控件,返回值为true时事件会传递给当前控件的onTouchEvent(),这就是所谓的Intercept(拦截)。
  正确的使用方法是,在此方法内仅判断事件是否需要拦截,然后返回。即便需要拦截也应该直接返回true,然后由onTouchEvent方法进行处理。
  onTouchEvent用于处理事件,返回值决定当前控件是否消费(consume)了这个事件。尤其对于ACTION_DOWN事件,返回true,表示我想要处理后续事件;返回false,表示不关心此事件,并返回由父类进行处理。

  Android 中与 Touch 事件相关的方法包括:dispatchTouchEvent(MotionEvent ev)、onInterceptTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent ev);能够响应这些方法的控件包括:ViewGroup、View、Activity。方法与控件的对应关系如下表所示:

Touch事件相关方法 方法功能 ViewGroup View Activity
dispatchTouchEvent(MotionEvent ev) 事件分发 Yes Yes Yes
onInterceptTouchEvent(MotionEvent ev) 事件拦截 Yes No No
onTouchEvent(MotionEvent event) 事件响应 Yes Yes Yes

  从这张表中我们可以看到 ViewGroup 和 View 对与 Touch 事件相关的三个方法均能响应,而 Activity 对 onInterceptTouchEvent(MotionEvent ev) 也就是事件拦截不进行响应。另外需要注意的是 View 对 onInterceptTouchEvent(MotionEvent ev) 的响应的前提是可以向该 View 中添加子 View,如果当前的 View 已经是一个最小的单元 View(比如 TextView),那么就无法向这个最小 View 中添加子 View,也就无法向子 View 进行事件的拦截,所以它没有 onInterceptTouchEvent(MotionEvent ev)。

事件分发机制图解

  • 从上图所示中,事件的分发机制分为3层,分别是Activity,ViewGroup,View。
  • 事件的返回值分别为 return false ,true,super.xxxx。super是调用父类实现的意思。
  • 事件的分发机制是从左上角的ACTION_DOWN开始的,由Activity的dispatchTouchEvent()开始分发
  • 在dispatchTouchEvent() 和onTouchEvent()中,return true ,代表事件传递到这里就消费掉了,事件不是再进行传递了
  • 在Activity中的dispatchTouchEvent()中,只有传递过来super ,才能继续向下分发事件。除此外return true/false都表示事件被消费掉了。

再来一张U形图方便记忆,从Action_Down开始,每个事件分别返回true,false,super。

左边是向下事件分发的理解:

第一层是Activity层,Activity层 return true/ false消费事件,return super 将事件分发到了ViewGroup层;

第二层是ViewGroup层,return true消费事件,return false 将事件回传到父类Activity,进行事件的响应。return super 进行事件拦截。事件拦截以后,返回false/或者super才能将事件传递到下一层View.

第三层是View层,return true 消费事件,return false将事件会传导父类ViewGroup,return true 进行事件的响应。

右边是冒泡向上消费事件的理解:

第三层是View层,在这一层,进行事件响应的时候,如果return true ,则直接消费事件,return super /false 的时候不消费事件,需要将事件响应回传到父类ViewGroup

第二层是ViewGroup层,return true ,则直接消费事件,return super /false 的时候不消费事件,需要将事件响应回传到父类Activity。

第一层 是Activity层,无论返回什么都结束掉。

U形图事件分发

4.3 关于事件传递的一些小结论

  1. 同一个事件序列是指从手指接触屏幕开始,到手指离开屏幕结束,在这个过程中产生的一系列事件,就是以down事件开始,中间是许多个move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能被一个View拦截消耗,一旦一个元素拦截了某个事件,那么这个事件序列中的所有事件都会直接交给它处理。
  3. 事件一旦交给一个View处理,那么它就必须消耗掉。在它还没有消耗掉该事件之前,那么同一事件中的剩余事件就不再交给它处理。
  4. 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent 并不会被调用,并且当前Vie可以持续受到后续的事件,最终这些消失的点击事件会传递给Activity处理。
  5. ViewGroup 默认不拦截事件,Android 源码中的ViewGroup 的onInterceptTouchEvent()方法默认返回false。
  6. View 没有onInterceptTouchEvent 方法,一旦事件传递给他了,那么它的onTouchEvent方法就会被调用。
  7. View 的onTouchEvent 默认会消费事件,默认返回true,除非是不可点击(clickable和longClickable同时为false)。
  8. View的enable属性不影响onTouchEvent的默认返回值。
  9. 事件传递过程是由外向内的,事件总是先传递给父元素,然后再由父元素分发给子View。

5. 从源码的角度来看事件分发机制

很多我们需要探讨的机制都离不开源码的设计,从源码的角度来看待问题,有助于加深理解。

5.1 Activity 对点击事件的分发过程

  点击事件用MotionEvent来表示,当一个点击事件发生的时候,最先传递给了Activity,由Activity的dispatchTouchEvent来进行事件的分发。具体的工作是有Activity内部的Window来完成。Window会将事件传递给decor view,decor view一般是当前界面的底层容器(即是setContentView 所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。
——————————源码 Activity # dispatchTouchEvent——————————

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

一般事件都是从ACTION_DOWN开始,所以这个if 返回的结果是true,接下来查看onUserInteraction()的源码.

——————源码 Activity # dispatchTouchEvent#onUserInteraction———————

 /**
     * Called whenever a key, touch, or trackball event is dispatched to the
     * activity.  Implement this method if you wish to know that the user has
     * interacted with the device in some way while your activity is running.
     * This callback and {@link #onUserLeaveHint} are intended to help
     * activities manage status bar notifications intelligently; specifically,
     * for helping activities determine the proper time to cancel a notfication.
     *
     * <p>All calls to your activity's {@link #onUserLeaveHint} callback will
     * be accompanied by calls to {@link #onUserInteraction}.  This
     * ensures that your activity will be told of relevant user activity such
     * as pulling down the notification pane and touching an item there.
     *
     * <p>Note that this callback will be invoked for the touch down action
     * that begins a touch gesture, but may not be invoked for the touch-moved
     * and touch-up actions that follow.
     *
     * @see #onUserLeaveHint()
     */
    public void onUserInteraction() {
    }

  呃,你没有看错,这是一个空方法。查看一下注释,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。所以onUserInteraction()主要用于屏保。
接下来再看看下一个方法superDispatchTouchEvent
——————源码 Activity # dispatchTouchEvent#superDispatchTouchEvent——————

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
  return mDecor.superDispatchTouchEvent(event);
//mDecor是DecorView的实例
//DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
}

接下来看 mDecor.superDispatchTouchEvent(event)

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
//DecorView继承自FrameLayout
//那么它的父类就是ViewGroup
而super.dispatchTouchEvent(event)方法,其实就应该是ViewGroup的dispatchTouchEvent()

}

所以执行了getWindow().superDispatchTouchEvent(ev) ,就是执行了ViewGroup的dispatchTouchEvent(event)。然后再回头看源码,
——————————源码 Activity # dispatchTouchEvent——————————

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

  从事件MotionEvent.ACTION_DOWN开始,返回true,所以注定会返回到下一个if判断中,也就是getWindow().superDispatchTouchEvent(ev)的判断中。所以执行了Activity的dispatchTouchEvent()实际上就是执行了ViewGroup的dispatchTouchEvent()方法。

再来捋捋顺序:

  1. 首先我们在手指按下屏幕,事件最先传递到Activity的dispatchTouchEvent() 进行事件分发。
  2. 具体的工作由Window类的实现类PhoneView的superDispatchTouchEvent来完成。
  3. 调用DecorView的superDispatchTouchEvent。
  4. 最终调用DecorView的父类ViewGroup的dispatchTouchEvent(),将事件分发到了ViewGroup。

5.2 ViewGroup 的事件分发机制

  上面我们分析了Activity将事件分到到ViewGroup了,接下来是对ViewGroup的分析。在Android 5.0 以后的源码发生了改动,但是原理是相同的,这里用5.0之前的源码来分析。源码太长了,我们分开了讨论。
——————————源码 ViewGroup# dispatchTouchEvent——————————

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }

        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;

            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);

                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    final View target = mMotionTarget;



    if (target == null) {
        ev.setLocation(xf, yf);
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        }

        return super.dispatchTouchEvent(ev);
    }
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
        }
        mMotionTarget = null;
        return true;
    }
    if (isUpOrCancel) {
        mMotionTarget = null;
    }
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        mMotionTarget = null;
    }
    return target.dispatchTouchEvent(ev);
} 
5.2.1 关于onInterceptTouchEvent的分析

  ViewGroup在dispatchTouchEvent进行分发的时候,需要调用onInterceptTouchEvent()来判断是否拦截。
——————————源码 ViewGroup# onInterceptTouchEvent——————————

public boolean onInterceptTouchEvent(MotionEvent ev) {  
    return false;  
}

截取其中关于onInterceptTouchEvent的的判断分析
——————————源码 ViewGroup#dispatchTouchEvent#if ——————————

if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }

  这个if判断语句,第一个判断值disallowIntercept:是否禁用事件拦截的功能(默认是false),可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改;所以onInterceptTouchEvent()的值决定了这个if循环能否继续,当 值为flase时,!onInterceptTouchEvent(ev) 为true,从而进入条件的内部了。当值为true的时候,!onInterceptTouchEvent(ev) 为false,跳出了这个条件判断。

再截取这个关于onInterceptTouchEvent的源码判断,当条件符合进入if内部的时候,遍历ViewGroup中的子View
——————————源码 ViewGroup# dispatchTouchEvent# if# for——————————

 for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }

  判断当前遍历的View是不是正在点击的View,如果是,再进入条件内部,这时候我们已经进入子View的 if (child.dispatchTouchEvent(ev))中
——————————源码 ViewGroup# dispatchTouchEvent# if# for#if——————————

        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                     //判断当前遍历的View是不是正在点击的View
                    //如果是,则进入条件判断内部
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                         //进入到了子View层中了
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                           return true;
                        }
                    }

  所以onInterceptTouchEvent()当值为flase 的时候,默认返回不拦截,继续分发事件到子View中。
——————————源码 ViewGroup# dispatchTouchEvent# if# for#if#if——————————

                  if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }

  到了这一步了,条件判断子View的dispatchTouchEvent,实现了点击事件从ViewGroup到View的分发传递,调用子View的dispatchTouchEvent是有返回值的,如果子View控件是可点击的,子View可以消费事件,那么点击该子View的控件是,事件分发到子View的dispatchTouchEvent的值必定为true,所以该if条件判断成立,所以进入条件内部 mMotionTarget = child。ViewGroup的dispatchTouchEvent,方法直接返回true,后面的代码无法执行,直接跳出去了,即把ViewGroup的touch事件拦截掉了。

最后我们捋捋顺序:

  1. 首先Activity将事件分发到ViewGroup的dispatchTouchEvent进行事件分发。
  2. 在ViewGroup的dispatchTouchEvent中,我们通过获取onInterceptTouchEvent()的值来判断if循环是否继续,当if值为true的时候,我们对ViewGroup中的子View进行遍历。
  3. 当遍历中的子View是我们点击的View的时候,这时候ViewGroup就将事件分发到子View中。
5.3 View的事件分发

  当ViewGroup将事件传给了View之后,我们接下来对View的dispatchTouchEvent()事件进行处理。

——————————源码 View# dispatchTouchEvent——————————

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}

  子View的条件判断有三个:

* mOnTouchListener != null
* (mViewFlags & ENABLED_MASK) == ENABLED
* mOnTouchListener.onTouch(this, event)

只有三个条件都为真的时候,dispatchTouchEvent()才返回true,接下来是对这个条件的判断。

  1. 条件一 : mOnTouchListener != null
public void setOnTouchListener(OnTouchListener l) { 
    mOnTouchListener = l;  
}

  mOnTouchListener是View类下的setOnTouchListener()方法赋值,只要给控件注册了Touch事件,mOnTouchListener 的值就一定不为空。

  1. (mViewFlags & ENABLED_MASK) == ENABLED
    这个条件是判断当前点击的事件是否可点击,很多View的默认条件是enable,所以这个条件默认为true

  2. mOnTouchListener.onTouch(this, event)
    回调控件注册Touch事件时的onTouch方法

button.setOnTouchListener(new OnTouchListener() {  

  @Override  
  public boolean onTouch(View v, MotionEvent event) {  
      return false;  
  }  
});

如果在onTouch方法返回true,就会让上述三个条件全部成立,从而整个方法直接返回true。
如果返回false,就会去执行onTouchEvent(event)方法。

最后在捋捋顺序:

  1. 在ViewGroup将事件分发到了View层之后,View层的dispatchTouchEvent对事件判断是否需要消费掉,当三个条件都满足的时候,事件直接消费掉了,不需要进行分发了。不完全满足时就会将事件传递到onTouchEvent中。

5.4 事件响应

同样的,对onTouchEvent进行源码分析,onTouchEvent是事件响应,源码主要是对一个switch进行判断,也就是对我们的MotionEvent 分发事件的几个基本动作进行处理。 源码有点长,但是我们只要把关注点分别放在不同的动作要求上的时候,就比较好理解了。

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  
    if ((viewFlags & ENABLED_MASK) == DISABLED) {  
        // 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));  
    }  
    if (mTouchDelegate != null) {  
        if (mTouchDelegate.onTouchEvent(event)) {  
            return true;  
        }  
    }  
     //如果该控件是可以点击的就会进入到下两行的switch判断中去;

    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
    //如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。

        switch (event.getAction()) {  
            case MotionEvent.ACTION_UP:  
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
               // 在经过种种判断之后,会执行到关注点1的performClick()方法。
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                    boolean focusTaken = false;  
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                        focusTaken = requestFocus();  
                    }  
                    if (!mHasPerformedLongPress) {  
                        removeLongPressCallback();  
                        if (!focusTaken) {  
                            if (mPerformClick == null) {  
                                mPerformClick = new PerformClick();  
                            }  
                            if (!post(mPerformClick)) {  
                          //          请往下看performClick()的源码分析
                                performClick();  
                            }  
                        }  
                    }  
                    if (mUnsetPressedState == null) {  
                        mUnsetPressedState = new UnsetPressedState();  
                    }  
                    if (prepressed) {  
                        mPrivateFlags |= PRESSED;  
                        refreshDrawableState();  
                        postDelayed(mUnsetPressedState,  
                                ViewConfiguration.getPressedStateDuration());  
                    } else if (!post(mUnsetPressedState)) {  
                        // If the post failed, unpress right now  
                        mUnsetPressedState.run();  
                    }  
                    removeTapCallback();  
                }  
                break;  
            case MotionEvent.ACTION_DOWN:  
                if (mPendingCheckForTap == null) {  
                    mPendingCheckForTap = new CheckForTap();  
                }  
                mPrivateFlags |= PREPRESSED;  
                mHasPerformedLongPress = false;  
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                break;  
            case MotionEvent.ACTION_CANCEL:  
                mPrivateFlags &= ~PRESSED;  
                refreshDrawableState();  
                removeTapCallback();  
                break;  
            case MotionEvent.ACTION_MOVE:  
                final int x = (int) event.getX();  
                final int y = (int) event.getY();  
                // Be lenient about moving outside of buttons  
                int slop = mTouchSlop;  
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                        (y < 0 - slop) || (y >= getHeight() + slop)) {  
                    // Outside button  
                    removeTapCallback();  
                    if ((mPrivateFlags & PRESSED) != 0) {  
                        // Remove any future long press/tap checks  
                        removeLongPressCallback();  
                        // Need to switch from pressed to not pressed  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                    }  
                }  
                break;  
        }  
//如果该控件是可以点击的,就一定会返回true
        return true;  
    }  
//如果该控件是可以点击的,就一定会返回false
    return false;  
}

注意看一下当MotionEvent_ACTION_UP ,手指抬起时,里面有很多的判断,最后有一个performClick()方法,这个方法的源码再看看。

public boolean performClick() {  
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
    if (mOnClickListener != null) {  
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}

只要mOnClickListener不为null,就会去调用onClick方法。mOnClickListener的源码如下:

public void setOnClickListener(OnClickListener l) {  
    if (!isClickable()) {  
        setClickable(true);  
    }  
    mOnClickListener = l;  
}

当我们通过调用setOnClickListener方法来给控件注册一个点击事件时,就会给mOnClickListener赋值(不为空),即会回调onClick(),最终消费事件。

6. 事件分发机制总结

事件分发图

因为缩小了图,怕看不清,所以用大图来看了。

下面是一个总体概括:

  1. 事件由Activity的dispatchTouchEvent()开始,将事件传递给当前的Activity的根ViewGroup:mDecorView,事件自上而下传递,直到被消费。

  2. 事件分发到ViewGroup时,调用dispatchTouchEvent()进行分发处理。首先会被ViewGroup的onInterceptTouchEvent()拦截。如果onInterceptTouchEvent 返回false,则开始遍历ViewGroup中的子View,将事件依次发给子View,若事件被某个子View消费了,将不再继续分发;如果onInterceptTouchEvent返回true,事件由ViewGroup自己处理。ViewGroup通过调用子View中的mOnTouchLisenter事件得到onTouchEvent的返回值。当这个返回值为true时,自己消费;否则将事件回传到Activity中,最后事件结束。

  3. 当事件分发到View层的时候,事件传递到View的dispatchTouchEvent() ,首先会判断OnTouchListener是否存在,倘若存在,则执行onTouch(),若onTouch()未对事件进行消费,事件将继续交由onTouchEvent处理,根据上面分析可知,View的onClick事件是在onTouchEvent的ACTION_UP中触发的,因此,onTouch事件优先于onClick事件。

  4. 事件在自上而下的传递过程中一直没有被消费,而且最底层的子View也没有对其进行消费,事件会反向向上传递,此时,父ViewGroup可以对事件进行消费,若仍然没有被消费的话,最后会回到Activity的onTouchEvent。

参考文章:
http://allenfeng.com/2017/02/22/android-touch-event-transfer-mechanism/
http://www.jianshu.com/p/38015afcdb58

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容