Android内部分享[13]——事件和事件传递分发机制

在 Android 中拦截用户交互的事件不止一种,我们可能已经注意到前面使用过的一些公共回调方法例如 setOnClickListener, onTouchEvent 等,有时候我们在自定义视图的时候需要去扩展或者覆盖某些事件,问题远比你想象的复杂。

监听事件

监听事件是视图 View 类中包含的单个回调方法接口,当你注册了某个事件的监听器后,当用户触发 UI 操作就会回调此方法,主要包括如下几个监听器:

  • onClick() 点击事件,来自 View.OnClickListener 接口。
  • onLongClick() 长按事件,来自 View.OnLongClickListener 接口。
  • onFocusChange() 焦点变化事件,来自 View.OnFocusChangeListener 接口。
  • onKey() 硬件键盘事件,来自 View.OnKeyListener 接口。
  • onTouch() 触摸事件,来自 View.OnTouchListener 接口。
  • onCreateContextMenu() 构建上下文菜单事件,来自 View.OnCreateContextMenuListener 接口。

我们先来看看比较常见的两个事件,点击和长按事件:

Button clickListener = findViewById(R.id.button_clicklistener);
clickListener.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 点击了 Button
    }
});

Button longclickListener = findViewById(R.id.button_long_clicklistener);
longclickListener.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
        // 长按了 Button
        return false;
    }
});

你会发现上面的普通点击事件没有返回值,而长按事件有一个 boolean 的返回值,这里如果我们返回 false 则表示没有消费此事件(也就是说还可以继续传递),如果返回为 true 则表示消费了此事件,此事件终止。这个可能不好理解,你也没有理解,不过没关系,我们用一个小案例来说明一下。

Button longAndClickListener = findViewById(R.id.button_long_and_click);
longAndClickListener.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d(TAG, "onClicked");
    }
});
longAndClickListener.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
        Log.d(TAG, "onLongClicked");
        return true;
    }
});

我们给同一个按钮添加普通点击事件和长按事件,如果我们长按返回 true 则只会触发长按事件,那是因为此事件已经被消费,如果长按事件中返回 false 则长按后又会触发点击事件,因为事件未被消费。

如果我们再给上面的按钮添加一个 onTouch 事件,然后让 onTouch 和 onLongClick 的返回都为 false.

longAndClickListener.setOnTouchListener(new View.OnTouchListener(){

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.d(TAG, "onTouched");
        return false;
    }
});

长按一下按钮日志如下:

10-08 14:06:02.344 22305-22305/com.test.innersharecode12 D/TEST: onTouched
10-08 14:06:02.845 22305-22305/com.test.innersharecode12 D/TEST: onLongClicked
10-08 14:06:04.247 22305-22305/com.test.innersharecode12 D/TEST: onTouched
10-08 14:06:04.248 22305-22305/com.test.innersharecode12 D/TEST: onClicked

你会发现 onTouch 在按下和抬起的时候各调用了一次,这里显而易见的是这些事件的执行有先后顺序的,onTouch 先执行,然后是 onLongClick 最后才是 onClick. 那么基于上面的理论,你可以猜一下,如果我们将 onTouch 的返回设置为 true 则其他事件不会回调了,因为已经被 onTouch 消费掉了。

事件传递过程

在 Android 中事件从根视图开始向下(向内)分配,直到有事件消费掉(返回 true),上面的几个方法是监听函数,均是 View 中的事件接口,接下来我们再来看看几个可以被重写的事件回调方法。

  • onKeyDown(int, KeyEvent) - 按下事件回调
  • onKeyUp(int, KeyEvent) - 抬起事件回调
  • onTrackballEvent(MotionEvent) - 轨迹球滑动事件回调
  • onTouchEvent(MotionEvent) - 触屏滑动事件回调
  • onFocusChanged(boolean, int, Rect) - 获得或失去焦点事件回调

我们前面学习了如何自定义 View,重写这些事件回调函数也属于自定义 View 的一部分内容,我们来简单看看如何使用。你会发现 onKeyDown, onKeyUp 和 onTouchEvent 以及上面的监听回调 onTouch 都会有一个 MotionEvent 对象作为参数,这个对象中有一个变量记录事件的状态,例如常见的四种状态:

  • ACTION_DOWN:表示按下了屏幕的状态
  • ACTION_MOVE:表示为移动手势
  • ACTION_UP:表示为离开屏幕
  • ACTION_CANCEL:表示取消手势,不会由用户产生,而是由程序产生的

实际上我们上面的 onTouch 在点击和长按的时候调用了两次分别是 ACTION_DOWN 和 ACTION_UP. 我们来给前面的自定义 View 重写 onTouchEvent 事件,并添加 onTouch 监听看看会发生什么。

public class CustomTouchView extends android.support.v7.widget.AppCompatButton {

    public static final String TAG = "Touch";

    public CustomTouchView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "onTouchEvent__DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent_UP");
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.d(TAG, "onTouchEvent_CANCEL");
                break;
        }
        return super.onTouchEvent(event);
    }
}

给添加上 onTouch 监听事件:

View view = getActivity().getLayoutInflater().inflate(R.layout.custom_touch_view_dialog, null);
CustomTouchView customeTouchView = view.findViewById(R.id.custome_touch_view);
customeTouchView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(CustomTouchView.TAG, "onTouch__DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(CustomTouchView.TAG, "onTouch_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(CustomTouchView.TAG, "onTouch_UP");
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.d(CustomTouchView.TAG, "onTouch_CANCEL");
                break;
        }
        return false;
    }
});

如果 onTouch 返回 false 则 onTouch 和 onTouchEvent 的行为一致,如果 onTouch 返回为 true 也就是拦截和消费事件,那么 onTouchEvent 就不会被执行,我们接下来尝试将 onTouchEvent 的返回改为 true 你会发现没有任何变化,但是如果我们将 onTouchEvent 的返回改为 false 你会发现只会调用它们两个的 ACTION_DOWN 事件。要解开这个谜我们就得继续看看关于事件分发的内容了。

事件分发过程

我们知道,所有的事件操作都发生在触摸屏上,而在屏幕上与我们交互的就是各种各样的视图组件(View),在 Android 中,所有的视图都继承于 View,另外通过各种布局组件(ViewGroup)来对 View 进行布局,ViewGroup 也继承于 View。所有的 UI 控件例如 Button、TextView 都是继承于 View,而所有的布局控件例如 RelativeLayout、容器控件例如 ListView 都是继承于 ViewGroup。所以,我们的事件操作主要就是发生在 View 和 ViewGroup 之间,那么 View 和 ViewGroup 中主要有哪些方法来对这些事件进行响应呢?记住如下 3 个方法,我们通过查看 View 和 ViewGroup 的源码可以看到:

View.java

public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event) 

ViewGroup.java

public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event) 
public boolean onInterceptTouchEvent(MotionEvent event)

在 View 和 ViewGroup 中都存在 dispatchTouchEvent 和 onTouchEvent 方法,但是在 ViewGroup 中还有一个 onInterceptTouchEvent 方法,那这些方法都是干嘛的呢?别急,我们先看看他们的返回值。这些方法的返回值全部都是boolean型,为什么是 boolean 型呢,看看本文的标题,“事件传递”,传递的过程就是一个接一个,那到了某一个点后是否要继续往下传递呢?你发现了吗,“是否”二字就决定了这些方法应该用 boolean 来作为返回值。没错,这些方法都返回 true 或者是 false。在 Android 中,所有的事件都是从开始经过传递到完成事件的消费,这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。

  • dispatchTouchEvent 方法用于事件的分发,Android 中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回 true 表示不继续分发,事件没有被消费。返回 false 则继续往下分发,如果是 ViewGroup 则分发给 onInterceptTouchEvent 进行判断是否拦截该事件。
  • onTouchEvent 方法用于事件的处理,返回 true 表示消费处理当前事件,返回 false 则不处理,交给子控件进行继续分发。
  • onInterceptTouchEvent 是 ViewGroup 中才有的方法,View 中没有,它的作用是负责事件的拦截,返回 true 的时候表示拦截当前事件,不继续往下分发,交给自身的 onTouchEvent 进行处理。返回 false 则不拦截,继续往下传。这是 ViewGroup 特有的方法,因为 ViewGroup 中可能还有子 View,而在 Android 中 View 中是不能再包含子 View 的(iOS 可以)。

到目前为止,Android 中事件的构成以及事件处理方法的作用你应该比较清楚了,接下来我们就通过一个 Demo 来实际体验实验一下。新建一个类 RTButton 继承 Button,用来实现我们对按钮事件的跟踪。

public class RTButton extends Button {
	public RTButton(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			System.out.println("RTButton---dispatchTouchEvent---DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			System.out.println("RTButton---dispatchTouchEvent---MOVE");
			break;
		case MotionEvent.ACTION_UP:
			System.out.println("RTButton---dispatchTouchEvent---UP");
			break;
		default:
			break;
		}
		return super.dispatchTouchEvent(event);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			System.out.println("RTButton---onTouchEvent---DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			System.out.println("RTButton---onTouchEvent---MOVE");
			break;
		case MotionEvent.ACTION_UP:
			System.out.println("RTButton---onTouchEvent---UP");
			break;
		default:
			break;
		}
		return super.onTouchEvent(event);
	}
}

在 RTButton 中我重写了 dispatchTouchEvent 和 onTouchEvent 方法,并获取了 MotionEvent 各个事件状态,打印输出了每一个状态下的信息。然后在 activity_main.xml 中直接在根布局下放入自定义的按钮 RTButton。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    <com.ryantang.eventdispatchdemo.RTButton 
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button"/>

</LinearLayout>

接下来在 Activity 中为 RTButton 设置 onTouch 和 onClick 的监听器来跟踪事件传递的过程,另外,Activity 中也有一个 dispatchTouchEvent 方法和一个 onTouchEvent 方法,我们也重写他们并输出打印信息。

public class MainActivity extends Activity {
	private RTButton button;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		button = (RTButton)this.findViewById(R.id.btn);
		button.setOnTouchListener(new OnTouchListener() {
			
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				switch (event.getAction()) {
				case MotionEvent.ACTION_DOWN:
					System.out.println("RTButton---onTouch---DOWN");
					break;
				case MotionEvent.ACTION_MOVE:
					System.out.println("RTButton---onTouch---MOVE");
					break;
				case MotionEvent.ACTION_UP:
					System.out.println("RTButton---onTouch---UP");
					break;
				default:
					break;
				}
				return false;
			}
		});
		
		button.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				System.out.println("RTButton clicked!");
			}
		});
		
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
		  System.out.println("Activity---dispatchTouchEvent---DOWN");
		  break;
		case MotionEvent.ACTION_MOVE:
	          System.out.println("Activity---dispatchTouchEvent---MOVE");
		  break;
		case MotionEvent.ACTION_UP:
		  System.out.println("Activity---dispatchTouchEvent---UP");
		  break;
		default:
		  break;
		}
		return super.dispatchTouchEvent(event);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
		  System.out.println("Activity---onTouchEvent---DOWN");
		  break;
		case MotionEvent.ACTION_MOVE:
		  System.out.println("Activity---onTouchEvent---MOVE");
		  break;
		case MotionEvent.ACTION_UP:
		  System.out.println("Activity---onTouchEvent---UP");
		  break;
		default:
		  break;
		}
		return super.onTouchEvent(event);
	}
}

代码部分已经完成了,接下来运行工程,并点击按钮,查看日志输出信息,我们可以看到如下结果:

示例打印结果

通过日志输出可以看到,首先执行了 Activity 的 dispatchTouchEvent 方法进行事件分发,在 MainActivity.java 代码第 55 行,dispatchTouchEvent 方法的返回值是 super.dispatchTouchEvent(event),因此调用了父类方法,我们进入 Activity.java 的源码中看看具体实现。

/**
* Called to process touch screen events.  You can override this to
* intercept all touch screen events before they are dispatched to the
* window.  Be sure to call this implementation for touch screen events
* that should be handled normally.
* 
* @param ev The touch screen event.
* 
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

从源码中可以看到,dispatchTouchEvent 方法只处理了 ACTIONDOWN 事件,前面提到过,所有的事件都是以按下为起点的,所以,Android 认为当 ACTIONDOWN 事件没有执行时,后面的事件都是没有意义的,所以这里首先判断 ACTION_DOWN 事件。如果事件成立,则调用了 onUserInteraction 方法,该方法可以在 Activity 中被重写,在事件被分发前会调用该方法。该方法的返回值是 void 型,不会对事件传递结果造成影响,接着会判断 getWindow().superDispatchTouchEvent(ev) 的执行结果,看看它的源码:

/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);

通过源码注释我们可以了解到这是个抽象方法,用于自定义的 Window,例如自定义 Dialog 传递触屏事件,并且提到开发者不需要去实现或调用该方法,系统会完成,如果我们在 MainActivity 中将 dispatchTouchEvent 方法的返回值设为 true,那么这里的执行结果就为 true,从而不会返回执行 onTouchEvent(ev),如果这里返回 false,那么最终会返回执行 onTouchEvent 方法,由此可知,接下来要调用的就是 onTouchEvent 方法了。别急,通过日志输出信息可以看到,ACTION_DOWN 事件从 Activity 被分发到了 RTButton,接着执行了 onTouch 和 onTouchEvent 方法,为什么先执行 onTouch 方法呢?我们到 RTButton 中的 dispatchTouchEvent 看看 View 中的源码是如何处理的。

View.java

/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags &
                ENABLED_MASK) == ENABLED  && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}

挑选关键代码进行分析,可以看代码第 16 行,这里有几个条件,当几个条件都满足时该方法就返回 true,当条件 li.mOnTouchListener 不为空时,通过在源码中查找,发现 mOnTouchListener 是在以下方法中进行设置的。

View.java

/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
    getListenerInfo().mOnTouchListener = l;
}

这个方法就已经很熟悉了,就是我们在MainActivity.java中为 RTButton 设置的 onTouchListener,条件 (mViewFlags & ENABLED_MASK) == ENABLED 判断的是当前 View 是否是 ENABLE 的,默认都是 ENABLE 状态的。接着就是 li.mOnTouchListener.onTouch(this, event) 条件,这里调用了 onTouch 方法,该方法的调用就是我们在MainActivity.java中为 RTButton 设置的监听回调,如果该方法返回 true,则整个条件都满足,dispatchTouchEvent 就返回 true,表示该事件就不继续向下分发了,因为已经被 onTouch 消费了。

如果 onTouch 返回的是 false,则这个判断条件不成立,接着执行 onTouchEvent(event) 方法进行判断,如果该方法返回 true,表示事件被 onTouchEvent 处理了,则整个 dispatchTouchEvent 就返回 true。到这里,我们就可以回答之前提出的“为什么先执行 onTouch 方法”的问题了。到目前为止,ACTIONDOWN 的事件经过了从 Activity 到 RTButton 的分发,然后经过 onTouch 和 onTouchEvent 的处理,最终,ACTIONDOWN 事件交给了 RTButton 得 onTouchEvent 进行处理。

当我们的手(我这里用的 Genymotion 然后用鼠标进行的操作,用手的话可能会执行一些 ACTIONMOVE 操作)从屏幕抬起时,会发生 ACTIONUP 事件。从之前输出的日志信心中可以看到,ACTIONUP 事件同样从 Activity 开始到 RTButton 进行分发和处理,最后,由于我们注册了 onClick 事件,当 onTouchEvent 执行完毕后,就调用了 onClick 事件,那么 onClick 是在哪里被调用的呢?继续回到View.java的源代码中寻找。由于 onTouchEvent 在View.java中的源码比较长,这里就不贴出来了,感兴趣的可以自己去研究一下,通过源码阅读,我们在 ACTIONUP 的处理分支中可以看到一个performClick()方法,从这个方法的源码中可以看到执行了哪些操作。

View.java

/**
* Call this view's OnClickListener, if it is defined.  Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
*         otherwise is returned.
*/
public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        return true;
    }

    return false;
}

在 if 分支里可以看到执行了 li.mOnClickListener.onClick(this); 这句代码,这里就执行了我们为 RTButton 实现的 onClick 方法,所以,到目前为止,可以回答前一个“onClick 是在哪里被调用的呢?”的问题了,onClick 是在 onTouchEvent 中被执行的,并且,onClick 要后于 onTouch 的执行。

到此,点击按钮的事件传递就结束了,我们结合源代码窥探了其中的执行细节,如果我们修改各个事件控制方法的返回值又会发生什么情况呢,带着这个问题,进入下一节的讨论。

事件拦截过程

从上一节分析中,我们知道了在 Android 中存在哪些事件类型,事件的传递过程以及在源码中对应哪些处理方法。我们可以知道在 Android 中,事件是通过层级传递的,一次事件传递对应一个完整的层级关系,例如上节中分析的 ACTIONDOWN 事件从 Activity 传递到 RTButton,ACTIONUP 事件也同样。结合源码分析各个事件处理的方法,也可以明确看到事件的处理流程。

之前提过,所有事件处理方法的返回值都是 boolean 类型的,现在我们来修改这个返回值,首先从 Activity 开始,根据之前的日志输出结果,首先执行的是 Activity 的 dispatchTouchEvent 方法,现在将之前的返回值 super.dispatchTouchEvent(event) 修改为 true,然后重新编译运行并点击按钮,看到如下的日志输出结果。

输入出日志结果

可以看到,事件执行到 dispatchTouchEvent 方法就没有再继续往下分发了,这也验证了之前的说法,返回 true 时,不再继续往下分发,从之前分析过的 Activity 的 dispatchTouchEvent 源码中也可知,当返回 true 时,就没有去执行 onTouchEvent 方法了。

接着,将上述修改还原,让事件在 Activity 这继续往下分发,接着就分发到了 RTButton,将 RTButton 的 dispatchTouchEvent 方法的返回值修改为 true,重新编译运行并查看输出日志结果。

输入出日志结果

从结果可以看到,事件在 RTButton 的 dispatchTouchEvent 方法中就没有再继续往下分发了。接着将上述修改还原,将 RTButton 的 onTouchEvent 方法返回值修改为 true,让其消费事件,根据之前的分析,onClick 方法是在 onTouchEvent 方法中被调用的,事件在这被消费后将不会调用 onClick 方法了,编译运行,得到如下日志输出结果。

输入出日志结果

跟分析结果一样,onClick 方法并没有被执行,因为事件在 RTButton 的 onTouchEvent 方法中被消费了。下图是整个事件传递的流程图。

Android 事件传递流程图

到目前为止,Android 中的事件拦截机制就分析完了。但这里我们只讨论了单布局结构下单控件的情况,如果是嵌套布局,那情况又是怎样的呢?接下来我们就在嵌套布局的情况下对 Android 的事件传递机制进行进一步的探究和分析。

嵌套布局事件传递

首先,新建一个类 RTLayout 继承于 LinearLayout,同样重写 dispatchTouchEvent 和 onTouchEvent 方法,另外,还需要重写 onInterceptTouchEvent 方法,在文章开头介绍过,这个方法只有在 ViewGroup 和其子类中才存在,作用是控制是否需要拦截事件。这里不要和 dispatchTouchEvent 弄混淆了,后者是控制对事件的分发,并且后者要先执行。

那么,事件是先传递到 View 呢,还是先传递到 ViewGroup 的?通过下面的分析我们可以得出结论。首先,我们需要对工程代码进行一些修改。

RTLayout.java

public class RTLayout extends LinearLayout {
	public RTLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
		  System.out.println("RTLayout---dispatchTouchEvent---DOWN");
		  break;
		case MotionEvent.ACTION_MOVE:
		  System.out.println("RTLayout---dispatchTouchEvent---MOVE");
		  break;
		case MotionEvent.ACTION_UP:
		  System.out.println("RTLayout---dispatchTouchEvent---UP");
		  break;
		default:
		  break;
		}
		return super.dispatchTouchEvent(event);
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			System.out.println("RTLayout---onInterceptTouchEvent---DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			System.out.println("RTLayout---onInterceptTouchEvent---MOVE");
			break;
		case MotionEvent.ACTION_UP:
			System.out.println("RTLayout---onInterceptTouchEvent---UP");
			break;
		default:
			break;
		}
		return super.onInterceptTouchEvent(event);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			System.out.println("RTLayout---onTouchEvent---DOWN");
			break;
		case MotionEvent.ACTION_MOVE:
			System.out.println("RTLayout---onTouchEvent---MOVE");
			break;
		case MotionEvent.ACTION_UP:
			System.out.println("RTLayout---onTouchEvent---UP");
			break;
		default:
			break;
		}
		return super.onTouchEvent(event);
	}
}

同时,在布局文件中为 RTButton 添加一个父布局,指明为自定义的 RTLayout,修改后的布局文件如下。

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.ryantang.eventdispatchdemo.RTLayout
        android:id="@+id/myLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <com.ryantang.eventdispatchdemo.RTButton
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Button" />
    </com.ryantang.eventdispatchdemo.RTLayout>

</LinearLayout>

最后,我们在 Activity 中也为 RTLayout 设置 onTouch 和 onClick 事件,在 MainActivity 中添加如下代码。

MainActivity.java

rtLayout.setOnTouchListener(new OnTouchListener() {
			
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            System.out.println("RTLayout---onTouch---DOWN");
            break;
        case MotionEvent.ACTION_MOVE:
            System.out.println("RTLayout---onTouch---MOVE");
            break;
        case MotionEvent.ACTION_UP:
            System.out.println("RTLayout---onTouch---UP");
            break;
        default:
            break;
        }
        return false;
    }
});
    
rtLayout.setOnClickListener(new OnClickListener() {
        
    @Override
    public void onClick(View v) {
        System.out.println("RTLayout clicked!");
    }
});

代码修改完毕后,编译运行工程,同样,点击按钮,查看日志输出结果如下:

从日志输出结果我们可以看到,嵌套了 RTLayout 以后,事件传递的顺序变成了 Activity -> RTLayout -> RTButton,这也就回答了前面提出的问题,Android 中事件传递是从 ViewGroup 传递到 View 的,而不是反过来传递的。

从输出结果第三行可以看到,执行了 RTLayout 的 onInterceptTouchEvent 方法,该方法的作用就是判断是否需要拦截事件,我们到 ViewGroup 的源码中看看该方法的实现。

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

该方法的实现很简单,只返回了一个 false。那么这个方法是在哪被调用的呢,通过日志输出分析可知它是在 RTLayout 的 dispatchTouchEvent 执行后执行的,那我们就进到 dispatchTouchEvent 源码里面去看看。由于源码比较长,我将其中的关键部分截取出来做解释说明。

ViewGroup.java

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

从这部分代码中可以看到 onInterceptTouchEvent 调用后返回值被赋值给 intercepted,该变量控制了事件是否要向其子控件分发,所以它起到拦截的作用,如果 onInterceptTouchEvent 返回 false 则不拦截,如果返回 true 则拦截当前事件。我们现在将 RTLayout 中的该方法返回值修改为 true,并重新编译运行,然后点击按钮,查看输出结果如下。

可以看到,我们明明点击的按钮,但输出结果显示 RTLayout 点击事件被执行了,再通过输出结果分析,对比上次的输出结果,发现本次的输出结果完全没有 RTButton 的信息,没错,由于 onInterceptTouchEvent 方法我们返回了 true,在这里就将事件拦截了,所以他不会继续分发给 RTButton 了,反而交给自身的 onTouchEvent 方法执行了,理所当然,最后执行的就是 RTLayout 的点击事件了。

总结

以上我们对 Android 事件传递机制进行了分析,期间结合系统源码对事件传递过程中的处理情况进行了探究。通过单布局情况和嵌套布局情况下的事件传递和处理进行了分析,现总结如下:

  • Android 中事件传递按照从上到下进行层级传递,事件处理从 Activity 开始到 ViewGroup 再到 View。
  • 事件传递方法包括dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent,其中前两个是 View 和 ViewGroup 都有的,最后一个是只有 ViewGroup 才有的方法。这三个方法的作用分别是负责事件分发、事件处理、事件拦截。
  • onTouch 事件要先于 onClick 事件执行,onTouch 在事件分发方法 dispatchTouchEvent 中调用,而 onClick 在事件处理方法 onTouchEvent 中被调用,onTouchEvent 要后于 dispatchTouchEvent 方法的调用。