android中会经常遇到多个View ViewGroup嵌套的问题,如果想要快速的解决这种问题,就需要对View的事件传递有较深入的理解。一次完整的事件传递机制,主要是三个阶段,分别是事件的分发,拦截和消费。
1 触摸事件的主要类型
触摸事件对应的是MotionEvent类,事件的类型主要有如下三种。
- ACTION_DOWN:用户手指按下的操作,一个按下操作标志着一次触摸事件的开始。
- ACTION_MOVE:用户手指按压屏幕后,在松开之前,如果移动的距离超过一定的阈值,那么会被判定为ACTION_MOVE的操作,一般情况下,手指的轻微移动都会触发一系列的移动事件。
- ACTION_UP:用户手指离开 屏幕的操作,一次抬起操作标志者这一次触摸事件的结束。
在一次触摸事件操作中,ACTION_DOWN和ACTION_UP这两个事件是必须的,而ACTION_MOVE视情况而定,如果用户只点击了一下屏幕,那么可能只会监听到按下和抬起的动作。
2 事件传递的三个阶段
- 分发(Dispatch):事件的分发对应着dispatchTouchEvent方法,在Android系统中,所有的触摸事件都是通过这个方法来分发的,方法原型如下:
// 方法值返回true,表示事件被当前视图消费掉,不再继续分发事件
// 方法值返回super.dispatchTouchEvent 表示继续分发该事件
public boolean dispatchTouchEvent(MotionEvent ev)
- 拦截(Intercept):事件的拦截对应着onInterceptTouchEvent方法,这个方法只在ViewGroup及其子类中才存在,在View和Activity中是不存在的,方法原型如下:
// 方法值返回true,表示拦截这个事件,不继续分发给子视图,同时交由自身的onTouchEvent方法进行消费
// 方法值返回false或者super.onInterceptTouchEvent表示不对事件进行拦截,需要继续传递给子视图
public boolean onInterceptTouchEvent(MotionEvent ev)
- 消费(Consume):事件的消费对应着onTouchEvent方法,方法原型如下:
// 方法值返回true,表示当前视图可以处理对应的事件,事件将不会向上传递给父视图
// 方法值返回false,表示当前视图不处理这个事件,事件会被传递给父视图的onTouchEvent方法处理
public boolean onTouchEvent(MotionEvent ev)
在Android 系统中,拥有事件传递能力的类有以下三种:
- Activity:拥有dispatchTouchEvent和onTouchEvent两个方法
- ViewGroup:拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法
- View:拥有dispatchTouchEvent和onTouchEvent两个方法
注意:这里的View是不包括ViewGroup的View控件,比如Button,TextView等本身已经是最小的单位。
3 View的事件传递机制
// 自定义MyTextView
public class MyTextView extends TextView {
private static final String TAG = "MyTextView";
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
自定义一个MyTextView 重写dispatchTouchEvent()和onTouchEvent()方法,将每个事件触发都打印Log日志。在定义一个MainActivity,在MainActivity中监听MyTextView对象的触摸和点击事件。
public class MainActivity extends AppCompatActivity implements View.OnTouchListener, View.OnClickListener {
private static final String TAG = "MainActivity";
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.my_text_view);
mTextView.setOnClickListener(this);
mTextView.setOnTouchListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (v.getId()) {
case R.id.my_text_view :
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG, "MyTextView onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE :
Log.e(TAG, "MyTextView onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP :
Log.e(TAG, "MyTextView onTouch ACTION_UP");
break;
default:
break;
}
break;
default:
break;
}
return false;
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.my_text_view:
Log.e(TAG, "MyTextView onClick");
break;
default:
break;
}
}
}
当我们点击MyTextView的对象时,打印log如下:
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onClick
可以看出一下结论:
1. 触摸事件的传递流程从dispatchTouchEvent开始,如果不进行人为干预,则事件会依靠嵌套从外层向里层传递,到达最里层的view时,就由它的onTouchEvent处理,如果它消费不了就依次从里向外传递,由外层view的onTouchEvent方法进行处理。
2. 如果事件在向内层传递过程中人为干预,事件处理函数返回true,则会导致事件提前被消费掉,内层view将不会接收到这个事件。
3. view控件的事件触发顺序是先执行onTouch方法,在最后才执行onClick方法,如果onTouch返回true,则事件不会继续传递,最后也不会调用onClick方法,如果onTouch返回false,则事件继续传递。
4. 另外,dispatchTouchEvent()方法中还有“记忆”的功能,如果第一次事件向下传递到某View,它把事件继续传递交给它的子View,它会记录该事件是否被它下面的View给处理成功了,(怎么能知道呢?如果该事件会再次被向上传递到我这里来由我的onTouchEvent()来处理,那就说明下面的View都没能成功处理该事件);当第二次事件向下传递到该View,该View的dispatchTouchEvent()方法机会判断,若上次的事件由下面的view成功处理了,那么这次的事件就继续交给下面的来处理,若上次的事件没有被下面的处理成功,那么这次的事件就不会向下传递了,该View直接调用自己的onTouchEvent()方法来处理该事件。
4 ViewGroup的事件传递机制
ViewGroup是作为view控件的容器存在的,ViewGroup拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法,可以看出和View的唯一区别是多了一个onInterceptTouchEvent方法。我们自定义一个MyRelativeLayout 继承RelativeLayout。
// 自定义RelativeLayout
public class MyRelativeLayout extends RelativeLayout{
private static final String TAG = "MyRelativeLayout";
public MyRelativeLayout(Context context) {
super(context);
}
public MyRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE :
Log.e(TAG,"onInterceptTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP :
Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onInterceptHoverEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
修改xml文件,将这个Layout作为MyTextView的容器,如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.jmf.advancedlevel.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.jmf.advancedlevel.MyTextView
android:id="@+id/my_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请点击我"
android:textSize="20sp" />
</com.example.jmf.advancedlevel.MyRelativeLayout>
运行,点击MyTextView,打印Log日志如下:
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onClick
从上面的日志情况得到如下结论:
1. 触摸事件的传递顺序是由Activity到ViewGroup,再由ViewGroup递归传递给它的子View
2. ViewGroup通过onInterceptTouchEvent方法对事件进行拦截,如果该方法返回true,则事件不会继续传递给子View,如果返回false或者super.onInterceptTouchEvent,则事件会继续传递给子View。
3. 在子View中对事件进行消费后,ViewGroup将接收不到任何事件
5关于事件的拦截和反拦截
1.5.1 怎么拦截事件?很简单,复写ViewGroup的onInterceptTouchEvent方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
return true;
case MotionEvent.ACTION_MOVE :
Log.e(TAG,"onInterceptTouchEvent ACTION_MOVE");
return true;
case MotionEvent.ACTION_UP :
Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
return true;
}
return true;
}
默认返回false 或者super.onInterceptTouchEvent,表示是不拦截事件,现在运行,如下:
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_MOVE
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_MOVE
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_UP
Log中可以看到MyTextView中什么事件也接受不到,如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;
1.5.2 怎么实现反拦截,就是不让父类拦截子View?
Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// getParent().requestDisallowInterceptTouchEvent(true); 这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。
getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
从源码也可以解释:
ViewGroup MOVE和UP拦截的源码是这样的:
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)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
当我们把disallowIntercept设置为true时,!disallowIntercept直接为false,于是拦截的方法体就被跳过了~
注:如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN里面直接return true了,那么子View是木有办法的捕获事件的~~~
6 总结
1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;