1. 基础知识
1.1 事件MotionEvent
当用户触摸屏幕时,就会产生点击事件MotionEvent。
MotionEvent中记录了触摸的位置,时间、历史记录、手势动作等信息。
1.2 事件种类
- MotionEvent.ACTION_DOWN:按下View(所有事件的开始)
- MotionEvent.ACTION_MOVE:滑动View
- MotionEvent.ACTION_UP:抬起View(与DOWN对应)
- MotionEvent.ACTION_CANCEL:非人为原因结束本次事件,注意,当ViewGroup中途拦截之前传给其子View的事件时,就会传一个ACTION_CANCEL给子View。
1.3 事件序列
从手指接触屏幕至手指离开屏幕,整个过程的触摸事件。
一个事件序列以DOWN事件开始,中间有无数个MOVE事件,最后以UP事件结束。
1.4 事件分发
将事件传递给某个View进行处理的过程。
1.5 事件分发的对象
硬件 ViewRootImpl DecorView PhoneWindow Activity PhoneWindow DecorView ->DecorView的子View
开发中能够接触到的是:
Activity -> ViewGroup -> View
1.6 事件分发的顺序
- ViewGrouo优先与View。 事件会从顶层ViewGroup开始向下传递,ViewGroup可以选择拦截事件,这样就不会再往下传递。默认情况下不会拦截,所以会一直传到最下层的View。如果该View还是不消费该事件,则将该事件从下往上传递。
- 用户设置的监听优先与系统回调。消费一个事件分为两种情况:1 用户给View设置了监听onTouchListener并且返回true 2 回调系统自带的View的OnTouchEvent()并且返回true。注意,只有返回true才是消费了该事件。即如果存在第一种情况,则事件会被onTouchListener 消费掉,不再回调OnTouchEvent。
2 事件分发的主要方法(概览篇)
忽略ViewRootImpl DecorView PhoneWindow这三者。
2.1 Activity(伪代码)
public boolean dispatchTouchEvent(MotionEvent event)
{
//省略代码
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
}
public boolean onTouchEvent(MotionEvent event)
{
//省略代码
return false;
}
2.2 ViewGroup(伪代码)
public boolean dispatchTouchEvent(MotionEvent event){
if(event不是ACTION_DOWN && mFirstTouchTarget == null){
return;
}
if(!disallowIntercept && onInterceptTouchEvent(event) ){
return super.dispatchTouchEvent();
}
if(child.dispatchTouchEvent(event)){
mFirstTouchTarget.add(child);
return true;
} else{
return super.dispatchTouchEvent();
}
super.dispatchTouchEvent()伪代码为:
if(onTouchListener.onTouch()){
mFirstTouchTarget.add(this);
return true;
}
if(onTouchEvent(event) ){
mFirstTouchTarget.add(this);
return true;
}
return false;
public boolean onInterceptTouchEvent(MotionEvent event){
//默认返回false;
return false;
}
//继承自View,ViewGroup并没有重写该方法
public boolean onTouchEvent(MotionEvent event)
2.3 View
public boolean dispatchTouchEvent(MotionEvent event){
if(设置了touchListener && touchListener.onTouch()){
return true;
}
return onTouchEvent();
}
public boolean onTouchEvent(MotionEvent event) {
if(不可用但是clickable){
return true;
}
if(CLICKABLE || LONG_CLICKABLE || CONTEXT_CLICKABLE ){
performClick();
return true;
}
return false;
}
3 事件分发的主要方法(讲解篇)
3.1 Activity
3.1.1 boolean dispatchTouchEvent(MotionEvent event)
表示如何分发事件,事件首先会传递到该方法。
- 1 DOWN事件发生后,会调用该方法,并把事件往下传递。
- 2 如果有View进行消费,则getWindow().superDispatchTouchEvent(ev)会返回true,则该方法也会返回true,不调用onTouchEvent()。
- 3 如果没View消费该事件,getWindow().superDispatchTouchEvent(ev)会返回false,则该方法会调用Activity的onTouchEvent()。
注意:如果是这种情况,则同一事件序列的后续事件,Activity传递到DecorView的dispatchTouchEvent方法中以后,基于某些判断就不会再往下传递(具体原因后面会讲到)。
public boolean dispatchTouchEvent(MotionEvent event)
{
//省略代码
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
}
3.1.2 boolean onTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
{
//省略代码
return false;
}
3.2 ViewGroup
3.2.1 boolean dispatchTouchEvent(MotionEvent event)
表示如何分发事件,事件首先会传递到该方法。
-
1 DOWN事件传递到这里之后,该ViewGroup(后续简称为VG)首先判断是否要拦截该事件。
- 如果拦截,则调用是否消费事件的方法super.dispatchTouchEvent()(由于ViewGroup为View的子类,所以会走到View的dispatchTouchEvent方法中),View的dispatchTouchEvent方法中会进行如下操作:
若用户设置的监听不为空(即mTouchListener不为null),则调用onTouchListener.onTouch(),如果onTouchListener.onTouch()返回true,则表示消费该事件,跳出。如果返回false,会接着调用onTouchEvent(),返回值代表是否消费该事件;
若用户设置的监听为空(即没有设置该监听),则直接调用onTouchEvent(),返回值代表是否消费该事件。
注意:存在VG拦截事件但是并不消费事件的情况,例如onInterceptTouchEvent返回true,onTouchEvent返回false。如果是DOWN事件,这种情况就是前面讲的,没有View消费该事件。-
如果不拦截,则找到包含点击位置的子控件,调用子控件的dispatchTouchEvent()方法。
子控件如果也不消费,即子控件的dispatchTouchEvent()返回false。此时该事件会由下往上传递,进入到
父控件的super.dispatchTouchEvent(),再次询问是否以及如何消费该事件。
2 VG通过disallowIntercept 标志以及onInterceptTouchEvent(event)去判断是否需要拦截该事件。
3 DOWN事件后续的其它事件,如果是该VG自身消费了前面的DOWN事件,则直接调用super.dispatchTouchEvent()。如果是其子View消费了前面的DOWN事件,则先判断是否拦截,再根据结果决定进行后续处理(如果不拦截,则调用子view的dispatchTouchEvent。如果拦截,则传递一个CANCEL事件给子View。同时后续的事件,都直接交给VG处理,不再往下传递)。
public boolean dispatchTouchEvent(MotionEvent event){
//如果不是ACTION_DOWN,且之前同一事件序列的ACTION_DOWN事件没有view进行处理(即mFirstTouchTarget 为null),则丢弃该事件。
//这就是为什么如果没有View处理ACTION_DOWN,后续事件传递到DecorView之后就不会再往下传递了。
//即使设置了disallowIntercept = true也没用,因为根本走不到disallowIntercept 的校验。
if(event不是ACTION_DOWN && mFirstTouchTarget == null){
return;
}
if(!disallowIntercept && onInterceptTouchEvent(event) ){
//走到这里,表示父布局进行拦截
//返回值表示父布局是否消费该事件;
//父布局如果消费,则mFirstTouchTarget就不为空。
return super.dispatchTouchEvent();
}
//走到这里说明没有被父布局拦截
//遍历child,根据滑动点的坐标值找到滑动的child
if(child.dispatchTouchEvent(event)){
mFirstTouchTarget.add(child);
return true;
} else{
//走到这里说明没有被拦截,但是子视图也没有消费该事件,
//则调用view的dispatchTouchEvent()。
return super.dispatchTouchEvent();
}
3.2.2 boolean onInterceptTouchEvent(MotionEvent event)
表示是否要拦截该事件。
-
注意:在子View消费DOWN事件的前提下,ViewGroup可以在事件序列中途拦截MOVE事件。这种情况下,会传递一个CANCEL事件给其子View.后续的MOVE事件就都交由ViewGroup处理,不再往下传递。
什么原因?- ViewGroup如果没有拦截DOWN事件,且该事件被子view消费,则后续的事件依然会
走到ViewGroup的dispatchTouchEvent()中,如果没有设置
requestDisallowInterceptTouchEvent(true)的话,还会走到onInterceptTouchEvent()方法中,最终才传到子view 的dispatchTouchEvent(); - 所以完全可以在onInterceptTouchEvent中根据某些条件(例如水平滑动距离达到临界值)去中途拦截MOVE事件。
- ViewGroup如果没有拦截DOWN事件,且该事件被子view消费,则后续的事件依然会
public boolean onInterceptTouchEvent(MotionEvent event){
//默认返回false
return false;
}
3.2.2 boolean onTouchEvent(MotionEvent event)
表示是否以及如何消费事件
- ViewGroup并没有重写该方法,具体见下面的View。
public boolean onTouchEvent(MotionEvent event)
3.3 View
3.3.1 boolean dispatchTouchEvent(MotionEvent event)
表示如何分发事件,事件首先会传递到该方法。
- 1 如果给view设置了mOnTouchListener ,且mOnTouchListener.onTouch返回true,则dispatchTouchEvent直接返回true,表示消费了该事件。
- 2 如果条件1不满足,则会调用onTouchEvent()方法。
public boolean dispatchTouchEvent(MotionEvent event){
if(设置了touchListener && touchListener.onTouch()){
return true;
}
return onTouchEvent();
}
3.3.2 boolean onTouchEvent(MotionEvent event)
表示是否以及如何消费事件
public boolean onTouchEvent(MotionEvent event) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
if(不可用但是clickable){
return true;
}
if(CLICKABLE || LONG_CLICKABLE || CONTEXT_CLICKABLE ){
//检测到ACTION_UP事件,performClick()中会调用OnClickListener(如果不为空的话)
performClick();
return true;
}
return false;
}
4 总结
-
- 默认情况下,滑动某个View,DOWN事件会由自上而下传递。即从Activity传递到ViewGroup、再传递到View。
如果该View消费了该事件,则DOWN事件以及同一事件序列的其它事件的调用模式一致:
如果该View不消费DOWN事件,则DOWN事件会回传给父控件的dispatchTouchEvent,其中调用onTouchEvent方法。
- 默认情况下,滑动某个View,DOWN事件会由自上而下传递。即从Activity传递到ViewGroup、再传递到View。
-
如果ViewGroup消费了DOWN事件(拦截消费或者回传消费),则后续事件调用模式为:
-
5 实例讲解
闲话少说,布局如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
android:background="#ffffff"
tools:context="com.study.test.DispatchActivity">
<com.study.test.dispatch.ViewOut
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="@color/colorPrimary" >
<com.study.test.dispatch.ViewIn
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorAccent"/>
<com.study.test.dispatch.ViewIn
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#ff0023"
android:clickable="true"/>
</com.study.test.dispatch.ViewOut>
</android.support.constraint.ConstraintLayout>
ViewOut、ViewIn分别继承自ViewGroup与View,复写方法中直接调用父类的对应方法,打印出参数以及函数返回值。
5.1 滑动蓝色的区域(ViewGroup)
- 1 滑动蓝色区域ViewGroup,则事件只会传到该ViewGroup,不会往下传递(坐标点不在子View上)。
- 2 DOWN事件过来,VewGroup默认不消费事件,即onTouchEvent返回false,最终没有View消费该DOWN事件。最终Activity的dispatchTouchEvent()会返回true,且本次事件的mFirstTouchTarget 为null。
- 3 后续MOVE和UP事件,传到Activity,传到DecorView,就会终止向下传递。
5.2 滑动粉色区域(没有设置clickable的子View)
- 同样没有View消费DOWN事件。
5.3 滑动红色区域(设置clickable为true的子View)
- 1 DOWN事件过来后,由于该ViewIn为Clickable,则该ViewIn的onTouchEvent会返回true,即默认会消费该事件。
- 2 后续的MOVE事件,还是会先走到ViewGroup的dispatchTouchEvent()以及onInterceptTouchEvent(),然后走到该View的dispatchTouchEvent()以及onTouchEvent()。
5.4 滑动红色区域(设置clickable为true的子View),滑动距离大于10时,ViewOut进行拦截
ViewOut的原有代码为:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.w(DispatchActivity.TAG,"ViewOut onInterceptTouchEvent接收:"+ Utils.getActionString(ev.getAction()));
boolean flag = super.onInterceptTouchEvent(ev);
Log.w(DispatchActivity.TAG,"ViewOut onInterceptTouchEvent返回:"+flag);
return flag;
}
修改代码为:
float mStartX;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.w(DispatchActivity.TAG, "ViewOut onInterceptTouchEvent接收:" + Utils.getActionString(ev.getAction()));
boolean flag = super.onInterceptTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
if((ev.getRawX() - mStartX) > 10){
flag = true;
}
break;
default:
break;
}
Log.w(DispatchActivity.TAG, "ViewOut onInterceptTouchEvent返回:" + flag);
return flag;
}
这里只截取了一部分log。
- 被拦截的MOVE事件,并没有直接走到ViewGroup的onTouchEvent,而是转化成一个CANCEL事件传递给了子View,并且子View的onTouchEvent返回true。后续的MOVE事件,传到ViewGroup的dispatchTouchEvent()以及onTouchEvent(),不再调用onInterceptTouchEvent()。
6 tips
6.1 requestDisallowInterceptTouchEvent的用法
requestDisallowInterceptTouchEvent为ViewParent接口独有的方法,注意该方法会递归的设置所有祖先的disallowIntercept。
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
6.2 事件冲突处理
事件冲突一般通过设置事件分发函数的返回值或者设置requestDisallowInterceptTouchEvent(boolean disallowIntercept)这两种方式来处理。