事件分发机制一直是Android中的一个重难点,最近也正好有点时间,于是决定好好研究一下这个东西,顺便也写下来。
首先我们来看一下当点击一个处在一个layout中的button时,事件究竟是怎么最后被button处理的。这里我分别用一个MyLayout和MyButton分别继承LinearLayout和Button,代码如下:
MyLayout.java
public class MyLayout extends LinearLayout {
private final String TAG = getClass().getName();
public MyLayout(Context context) {
super(context);
}
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "on touch event layout down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "on touch event layout move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "on touch event layout up");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "intercept touch event layout down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "intercept touch event layout move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "intercept touch event layout up");
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "dispatch touch event layout down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "dispatch touch event layout move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "dispatch touch event layout up");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
}
MyButton.java
public class MyButton extends Button {
private final String TAG = getClass().getName();
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "dispatch touch event button down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "dispatch touch event button move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "dispatch touch event button up");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "on touch event button down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "on touch event button move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "on touch event button up");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
这里我们重写了LinearLayout和Button的dispatchTouchEvent和onTouchEvent还有LinearLayout的onInterceptTouchEvent。
现在我们运行程序并点击button,看看打印的日志都有那些内容:
MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyButton: dispatch touch event button down
MyButton: on touch event button down
MyLayout: dispatch touch event layout move
MyLayout: intercept touch event layout move
MyButton: dispatch touch event button move
MyButton: on touch event button move
MyLayout: dispatch touch event layout up
MyLayout: intercept touch event layout up
MyButton: dispatch touch event button up
MyButton: on touch event button up
从日志中我们可以看出当我们点击的动作被捕获后,点击事件的分发顺序为:
父View的dispatchTouchEvent->父View的onInterceptTouchEvent->子View的dispatchTouchEvent->子View的onTouchEvent最后处理事件。
那么现在问题来了,当我们的点击事件被捕获之后,究竟经过了什么过程使得事件最终到达我们的目标View并被处理的,另外我们是否可以根据我们想要的效果对这个过程进行自定义呢?接下来就来小小地研究一下这个过程。
过程分析
这一篇我们先不深入源码进行分析,因为深入源码是个比较复杂的过程,容易被绕进其中,因此这个内容在下一篇进行分析。我们先来看看能不能通过其他的方式把这个过程大概的弄清楚。
先来看一下官方API对这三个方法的描述:
dispatchTouchEvent:Pass the touch screen motion event down to the target view, or this view if it is the target.
onInterceptTouchEvent:Implement this method to intercept all touch screen motion events.This allows you to watch events as they are dispatched to your children, and take ownership of the current gesture at any point.
onTouchEvent:Implement this method to handle touch screen motion events.
官方的对这三个方法的解释十分清楚:dispatchTouchEvent负责分配屏幕事件究竟是由自己处理还是向下传递给它的子View进行处理;onInterceptTouchEvent负责当屏幕事件从父View向子View传递的时候进行拦截,我们可以通过这个方法监视并研究事件向下传递的过程;onTouchEvent负责最后屏幕事件的处理。
刚刚我们在打印日志的过程中发现点击事件并没有经过MyLayout的onTouchEvent,从刚刚的官方的解释中我们知道onTouchEvent是用于处理屏幕事件的,而日志中可以看出屏幕事件最后被我们安排在MyLayout中的子View——MyButton处理了。
那么现在我们想要对这个过程进行简单的分析和处理有需要知道点什么呢?我们再来看看官方对这三个方法的布尔类型的返回值的解释是什么样的:
dispatchTouchEvent:True if the event was handled by the view, false otherwise.
onInterceptTouchEvent:Return true to steal motion events from the children and have them dispatched to this ViewGroup through onTouchEvent().
onTouchEvent:True if the event was handled, false otherwise.
这三个返回值的解释是这样的:dispatchTouchEvent:若返回true则由自己分发屏幕事件,否则传递给子View处理;onInterceptTouchEvent:若返回true则将传递给子View的屏幕事件拦截下,并交给自己的onTouchEvent处理;onTouchEvent:若返回true则代表屏幕事件被自己处理了,反之则不是。
看到这里好像有点意思了,原来事件处理的过程中这三个方法的返回值起到了相当关键的作用,现在我们稍微修改一下MyLayout看看有什么效果。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "dispatch touch event layout down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "dispatch touch event layout move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "dispatch touch event layout up");
break;
default:
break;
}
return false;
}
这里将dispatchTouchEvent的返回值设置为false,然后打印日志:
MyLayout: dispatch touch event layout down
只有一条,说明当MyLayout捕获了ACTION_DOWN后dispatchTouchEvent方法返回了false,然后紧接其后的其他事件就不再有MyLayout进行分配处理了,那么后面的事件都上哪里进行处理了呢?我们将MyLayout的dispatchTouchEvent的返回值改回去,再改改MyButton中的dispatchTouchEvent。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "dispatch touch event button down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "dispatch touch event button move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "dispatch touch event button up");
break;
default:
break;
}
return false;
}
接着打印日志:
MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyButton: dispatch touch event button down
MyLayout: on touch event layout down
我们发现当dispatchTouchEvent返回了false之后,则由它的父View处理ACTION_DOWN事件,但全过程只是处理了ACTION_DOWN,剩下的去哪里了呢?这时候我们想起官方对onTouchEvent的返回值的解释,我们来修改一下MyLayout的onTouchEvent的返回值。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "on touch event layout down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "on touch event layout move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "on touch event layout up");
break;
default:
break;
}
return false;
}
我们将返回值设为false,但是打印日志后发现并没有任何改变,我们再将它设为true试试。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "on touch event layout down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "on touch event layout move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "on touch event layout up");
break;
default:
break;
}
return true;
}
这时候我们通过打印日志有了一个大发现:
MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyButton: dispatch touch event button down
MyLayout: on touch event layout down
MyLayout: dispatch touch event layout move
MyLayout: on touch event layout move
MyLayout: dispatch touch event layout up
MyLayout: on touch event layout up
原来当onTouchEvent返回false的时候,后续屏幕事件便不会再被处理。相当于做了一个标记,只有当这个标记为true时,ACTION_DOWN之后的一系列屏幕事件之后被处理。
现在我们大致弄懂了dispatchTouchEvent和onTouchEvent的作用,接下来我们再来看看onInterceptTouchEvent,看看事件是怎样由ViewGroup传递给子View的。
我们将MyLayout的onInterceptTouchEvent的返回值改为true:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "intercept touch event layout down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "intercept touch event layout move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "intercept touch event layout up");
break;
default:
break;
}
return true;
}
这时候的日志内容是这样的:
MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyLayout: on touch event layout down
通过日志我们发现,本应该传递给子View的事件被onInterceptTouchEvent拦截并返回true之后交由MyLayout本身的onTouchEvent去处理了。接下来我们将返回值设为false看看:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "intercept touch event layout down");
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "intercept touch event layout move");
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "intercept touch event layout up");
break;
default:
break;
}
return false;
}
现在的日志就和没有做任何修改的时候一样了:
MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyButton: dispatch touch event button down
MyButton: on touch event button down
MyLayout: dispatch touch event layout move
MyLayout: intercept touch event layout move
MyButton: dispatch touch event button move
MyButton: on touch event button move
MyLayout: dispatch touch event layout up
MyLayout: intercept touch event layout up
MyButton: dispatch touch event button up
MyButton: on touch event button up
现在我们懂了onInterceptTouchEvent在父View向子View传递事件的过程中起到的作用就是决定这个事件是否是由本View接下处理还是传递给子View进行处理,当方法返回true的时候就交由本View处理,而返回false的时候就交由子View进行处理。
总结
现在我们基本弄懂了这三个方法在事件分发过程中各自起到的作用,下面我们再重新梳理一下Android事件分发的过程。
首先事件在传递过程中交由dispatchTouchEvent进行事件的分配,若dispatchTouchEvent返回false,则将事件传递回其父View,让其父View的onTouchEvent进行处理;若返回true,则将事件传递给onInterceptTouchEvent。当onInterceptTouchEvent接收到事件之后,它也会有一个布尔类型的返回值,若返回true,则事件不会继续向下传递而是被该View拦截下,交由该View的onTouchEvent进行处理;若返回false,则会将事件传递给其子View进行处理。最后,当目标View接收到这一系列屏幕事件的时候,首先会去处理ACTION_DOWN事件,然后会由一个返回值,若返回值为true,则表明以ACTION_DOWN为开始的一些列事件目标View都会接下并处理;但若是返回false,则后续的一系列事件,整个程序都不会将其接下并处理。
我们通过不停地尝试,最终基本弄清楚了Android事件分发的流程,但是这个过程中究竟发生了什么,这个过程到底是怎么进行的,还需深入View的源码进行探究,以后我会寻找时间认真研究一下,并将我的学习成果分享出来。