1. 铺垫
1.1 MotionEvent : 触屏事件
int ACTION_DOWN=0 : 代表down
Int ACTION_MOVE=2 ; 代表move
Int ACTION_UP=1 : 代表up
1.2 Activity
boolean dispatchTouchEvent(MotionEvent event) : 分发事件
boolean onTouchEvent(MotionEvent event) : 处理事件的回调
1.3 View
boolean dispatchTouchEvent(MotionEvent event) : 分发事件
void setOnTouchListener(OnTouchListener l) : 设置事件监听器
boolean onTouchEvent(MotionEvent event) : 处理事件的回调方法
1.4 ViewGroup
boolean dispatchTouchEvent(MotionEvent ev) : 分发事件
boolean onInterceptTouchEvent(MotionEvent ev) : 拦截事件的回调方法
1.5 若onTouchEvent或onTouchEvent返回true,则表示事件被消费了,就不会进一步分发。
2.
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context="com.sagereal.motionevent.MainActivity">
<com.sagereal.motionevent.MyImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
android:layout_margin="30dp"
android:id="@+id/iv_test"/>
</LinearLayout>
public class MyImageView extends ImageView {
private static final String TAG = "MyImageView";
//布局 用两个参数的构造 : 属性集合 AttributeSet
public MyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
Log.e(TAG, "MyImageView()");
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "dispatchTouchEvent: action = " + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: action = " + event.getAction());
return super.onTouchEvent(event);
}
}
public class MainActivity extends Activity{
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.iv_test).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("MyImageView", "Listener onTouch: action = " + event.getAction());
return false;
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "dispatchTouchEvent: action =" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent: action =" + event.getAction());
return super.onTouchEvent(event);
}
}
对imageView来说,其回调监听setOnTouchListener默认返回false,即处理事件但不消费事件。
2.1 点击图标之外的位置
对事件down来说,Activity的dispatchTouchEvent方法分发事件,但没有子view来处理、消费,只好调用自己的onTouchEvent分发来处理消费。事件move(1)/up(2)类似。
01-03 10:26:19.660 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =0
01-03 10:26:19.662 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =0
01-03 10:26:19.685 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:26:19.686 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:26:19.702 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:26:19.702 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:26:19.722 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:26:19.722 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:26:19.724 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =1
01-03 10:26:19.725 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =1
2.2 点击图标
对事件down来说,Activity的dispatchTouchEvent方法分发事件,分发到子view,会先调用imgView的dispatchTouchEvent分发,
但它是最后一层,无子view,就由imageView来处理、消费。
对imageView来说,回调监听(setOnTouchListener)的优先级大于回调方法(onTouchEvent)的优先级,若回调监听(setOnTouchListener)、回调方法(onTouchEvent)都不处理消费该事件,则由Activity的onTouchEvent方法处理。
因为imageView连down事件都不处理,故move/up事件也不会分发到imageView,直接由Activity的onTouchEvent方法处理了。
01-03 10:31:14.237 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =0
01-03 10:31:14.239 3437-3437/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 0
01-03 10:31:14.239 3437-3437/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 0
01-03 10:31:14.239 3437-3437/com.sagereal.motionevent E/MyImageView: onTouchEvent: action = 0
01-03 10:31:14.240 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =0
01-03 10:31:14.262 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:31:14.262 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:31:14.272 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:31:14.272 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:31:14.288 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 10:31:14.289 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-03 10:31:14.291 3437-3437/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =1
01-03 10:31:14.291 3437-3437/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =1
3. 让imageView的回调监听返回true
findViewById(R.id.iv_test).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("MyImageView", "Listener onTouch: action = " + event.getAction());
//return false;
return true;
}
});
因为imgView的回调监听(setOnTouchListener)的优先级大于回调方法(onTouchEvent)的优先级,
当消息从Activity分发到imageView后,就在setOnTouchListener内被消费了(返回了true),故不会走到回调方法onTouchEvent中
down/move/up事件的处理类似。
点击图标,对应的日志:
01-03 12:32:41.807 5681-5681/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =0
01-03 12:32:41.809 5681-5681/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 0
01-03 12:32:41.809 5681-5681/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 0
01-03 12:32:41.839 5681-5681/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 12:32:41.840 5681-5681/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 2
01-03 12:32:41.840 5681-5681/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 2
01-03 12:32:41.849 5681-5681/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-03 12:32:41.850 5681-5681/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 2
01-03 12:32:41.850 5681-5681/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 2
01-03 12:32:41.852 5681-5681/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =1
01-03 12:32:41.853 5681-5681/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 1
01-03 12:32:41.854 5681-5681/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 1
4. 让imageView的down事件对应的回调监听返回true,move/up事件都返回false
findViewById(R.id.iv_test).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("MyImageView", "Listener onTouch: action = " + event.getAction());
//return false;
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return true;
}
return false;
}
});
down事件是被分发到imageView,被setOnTouchListener消费,但move/up事件并没有被消费,仍然返回false,故最终会被Activity的onTouchEvent处理消费。
点击图标,对应的日志:
01-01 19:40:02.793 5725-5725/com.sagereal.motionevent E/MyImageView: MyImageView()
01-01 19:40:09.469 5725-5725/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =0
01-01 19:40:09.472 5725-5725/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 0
01-01 19:40:09.472 5725-5725/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 0
01-01 19:40:09.477 5725-5725/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =2
01-01 19:40:09.478 5725-5725/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 2
01-01 19:40:09.479 5725-5725/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 2
01-01 19:40:09.479 5725-5725/com.sagereal.motionevent E/MyImageView: onTouchEvent: action = 2
01-01 19:40:09.480 5725-5725/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =2
01-01 19:40:09.484 5725-5725/com.sagereal.motionevent E/MainActivity: dispatchTouchEvent: action =1
01-01 19:40:09.485 5725-5725/com.sagereal.motionevent E/MyImageView: dispatchTouchEvent: action = 1
01-01 19:40:09.486 5725-5725/com.sagereal.motionevent E/MyImageView: Listener onTouch: action = 1
01-01 19:40:09.486 5725-5725/com.sagereal.motionevent E/MyImageView: onTouchEvent: action = 1
01-01 19:40:09.487 5725-5725/com.sagereal.motionevent E/MainActivity: onTouchEvent: action =1
二.补充
事件序列,一般是指从手指触摸到屏幕在到离开屏幕这么一个过程。在这个过程中其实会产生多个事件,一般是以ACTION_DOWN作为开始,中间存在多个ACTION_MOVE,最后以ACTION_UP结束。我们称一次ACTION_DOWN-->ACTION_MOVE-->ACTION_UP过程称为一个事件序列。
事件分发主要3个方法:
dispatchTouchEvent:负责事件的传递分发,事件到达view时一定回调此方法。返回值表示是否消费此事件,受onTouchEvent和子view的dispatchTouchEvent返回值影响。
onInterceptTouchEvent:用于判断是否拦截事件,也是返回值的含义。在dispatchTouchEvent内部调用。view拦截了某个事件,那后续这一事件序列都会默认拦截,不再调用此方法。只有ViewGroup才有这个方法,View无此方法,即View不可以拦截事件。
onTouchEvent:用于处理事件,返回值表示是否消费此事件。在dispatchTouchEvent内部调用。如果不消耗某一事件,那当前view不再接受同一事件序列的事件。
三者关系:事件到达view时,会调用dispatchTouchEvent,然后内部调用onInterceptTouchEvent判断是否拦截,如果不拦截就调用子view的dispatchTouchEvent;若拦截,则执行onTouchEvent方法处理这个事件。
/**
* 点击事件产生后
*/
// 步骤1:调用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否会消费事件
// 步骤2:判断是否拦截事件
if (onInterceptTouchEvent(ev)) {
// a. 若拦截,则将该事件交给当前View进行处理
// 即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不拦截,则将该事件传递到下层
// 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
// 直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}
// 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
return consume;
}
一般有三种返回:true
、false
和super
引用父类对应方法。
dispatchTouchEvent 返回 true:表示改事件在本层不再进行分发且已经在事件分发自身中被消费了。
dispatchTouchEvent 返回 false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent
方法进行消费。dispatchTouchEvent 返回 super : 默认会调用自己的 onInterceptTouchEvent 方法
onInterceptTouchEvent 返回true:表示将事件进行拦截,并将拦截到的事件交由本层控件 的onTouchEvent
进行处理。
onInterceptTouchEvent 返回false:表示不对事件进行拦截,事件得以成功分发到子View
。并由子View
的dispatchTouchEvent
进行处理。
onTouchEvent 返回 true:表示onTouchEvent
处理完事件后消费了此次事件。此时事件终结,将不会进行后续的传递。
onTouchEvent 返回 false:事件在onTouchEvent
中处理后继续向上层View传递,且有上层View
的onTouchEvent
进行处理。
除此之外还有一个方法也是经常用到的:requestDisallowInterceptTouchEvent(),这个方法能够影响父ViewGroup是否拦截事件,true表示 不拦截事件,false表示拦截事件。
《Android开发艺术探索》里总结了11条关于事件传递的结论:
-
同一个事件序列是指手机接触屏幕那一刻起,到离开屏幕那一刻结束,有一个down事件,若干个move事件,一个up事件构成。
-
某个View一旦决定拦截事件,那么这个事件序列之后的事件都会由它来处理,并且不会再调用onInterceptTouchEvent。
-
正常情况下,一个事件序列只能被一个View拦截并消耗。这个原因可以参考第2条,因为一旦拦截了某个事件,那么这个事件序列里的其他事件都会交给这个View来处理,所以同一事件序列中的事件不能分别由两个View同时处理,但是我们可以通过特殊手段做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
-
一个View如果开始处理事件,如果它不处理down事件(onTouchEvent里面返回了false),那么这个事件序列的其他事件就不会交给它来继续处理了,而是会交给它的父元素去处理。
-
如果一个View处理了down事件,却没有处理其他事件,那么这些事件不会交给父元素处理,并且这个View还能继续受到后续的事件。而这些未处理的事件,最终会交给Activity来处理。
-
ViewGroup的onInterceptToucheEvent默认返回false,也就是默认不拦截事件。
-
View没有InterceptTouchEvent方法,如果有事件传过来,就会直接调用onTouchEvent方法。
-
View的onTouchEvent方法默认都会消耗事件,也就是默认返回true,除非它是不可点击的(longClickable和clickable同时为false)。注意:View的longClickable默认都为false,clickable要根据控件属性判断。
-
View的enable属性不会影响onTouchEvent的默认返回值。就算一个View是不可见的,只要它是可点击的(clickable或者longClickable有一个为true),它的onTouchEvent默认返回值也是true。
-
onClick方法会执行的前提是当前View是可点击的,并且它收到了down和up事件。
-
事件传递过程是由外向内的,也就是事件会先传给父元素在向下传递给子元素。但是子元素可以通过requestDisallowInterceptTouchEvent来干预父元素的分发过程,但是down事件除外(因为down事件方法里,会清除所有的标志位)。
参考: