版本:2.0
日期:2014.3.21 2014.3.29
版权:© 2014 kince 转载注明出处
一、基本概念
在实际开发中,经常会遇到与触屏事件有关的问题,最典型的一个就是滑动冲突。比如在使用SliddingMenu菜单的时候,可能会与ViewPager或者其他的一些带有滑动事件的View相冲突,再比如ScrollView嵌套ListView相冲突等等。还有就是在自定义控件的时候,需要处理一些事件时候,也必须把逻辑处理好,父 view 和子view 都需要接收事件,然后处理。如果不明白事件传递机制,很难开发出需要的效果。因此就需要对Android消息传递机制有一个基本的理解与认识,这样才有可能解决开发过程中的需求与问题。
对于触摸(Touch)触发的事件,在Android中,事件主要包括点按(onClick)、长按(onLongClick)、拖拽(onDrag)、滑动(onScroll)等,点按又包括单击和双击,另外还包括单指操作和多指操作。其中Touch的第一个状态是 ACTION_DOWN, 表示按下了屏幕。之后,touch将会有后续事件,比如移动、抬起等,一个Action_DOWN, n个ACTION_MOVE, 1个ACTION_UP,就构成了Android中众多的事件。
- 按下(ACTION_DOWN) //表示用户按下了屏幕
- 移动(ACTION_MOVE) //表示用户在屏幕移动
- 抬起(ACTION_UP) //表示用户离开屏幕
- 取消手势(ACTION_CANCEL) //表示,不会由用户产生,而是由程序产生的
- 划出屏幕(ACTION_OUTSIDE)//表示滑出屏幕了
当然还有一些其他的事件,如图所示:
所有的操作事件首先必须执行的是按下操作(ACTIONDOWN),之后所有的操作都是以此作为前提,当按下操作完成后,接下来可能是一段移动(ACTIONMOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。因为所有的事件操作都发生在触摸屏上,而在屏幕上与用户交互的就是各种各样的视图组件(View),在Android中,所有的视图都继承于View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。所有的UI控件例如Button、TextView都是继承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是继承于ViewGroup。所以,事件操作主要就是发生在View和ViewGroup之间,那么View和ViewGroup中主要有哪些方法来对这些事件进行响应呢?有如下3个方法:
可以看到,触屏消息已经不会传递到OnTouchEvent(MotionEvent event)方法里面了,因为
dispatchTouchEvent方法返回
true,说明消息不会继续分发到onTouchEvent()方法
。然后在这个两个方法的返回之前加入输出,也就是把父类方法的返回值打印出来,
ACTION_DOWN以及ACTION_UP事件传递流程图如下:
在开发中,经常会在Activity中设置setOnTouchListener或者setOnClickListener,下面就在MainActivity里面添加。
事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
在外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系统默认的 super.dispatchTouchEvent(ev) 情况下,事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件拦截逻辑如下:
原因:android对Touch Event的分发逻辑是View从上层分发到下层(dispatchTouchEvent函数),然后下层优先开始处理Event(先mOnTouchListener,再onTouchEvent)并向上返回处理情况(boolean值),若返回true,则上层不再处理。
于是难题出现了,你若把Touch Event都想办法给传到上层了(只能通过返回false来传到上层),那么下层的各种子View就不能处理后续事件了。
解决方案:
开始仅着眼于Touch Event处理完后的回传过程,想了N久不得,毕竟我想实现的是一个需要打破android事件处理逻辑的效果(就是一个连续性操作,只有不满足上层要求时,才轮到下层处理)。然后突然想到事件的分发过程,便豁然开朗:
覆写最上层的View的dispatchTouchEvent函数,代码如下:
1、public boolean dispatchTouchEvent(MotionEvent event)
2、 public boolean onTouchEvent(MotionEvent event)
3、 public boolean onInterceptTouchEvent(MotionEvent event)
在View和ViewGroup中都存在dispatchTouchEvent和onTouchEvent方法,特别的,在ViewGroup中还有一个onInterceptTouchEvent方法。这些方法的返回值全部都是boolean型,都返回true或者是false,这是因为事件传递的过程就是一个接一个,某一个点后根据方法boolean的返回值判断是否要继续往下传递。在Android中,所有的事件都是从开始经过传递到完成事件的消费,这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。
2、 public boolean onTouchEvent(MotionEvent event)
3、 public boolean onInterceptTouchEvent(MotionEvent event)
在View和ViewGroup中都存在dispatchTouchEvent和onTouchEvent方法,特别的,在ViewGroup中还有一个onInterceptTouchEvent方法。这些方法的返回值全部都是boolean型,都返回true或者是false,这是因为事件传递的过程就是一个接一个,某一个点后根据方法boolean的返回值判断是否要继续往下传递。在Android中,所有的事件都是从开始经过传递到完成事件的消费,这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。
接下来就是这些方法的参数,都接受了一个MotionEvent类型的参数,MotionEvent继承于InputEvent,用于标记各种动作事件。之前提到的ACTIONDOWN、ACTIONMOVE、ACTION_UP、ACTION_CANCEL都是MotinEvent中定义的常量。我们通过MotionEvent传进来的事件类型来判断接收的是哪一种类型的事件。到现在,这三个方法的返回值和参数你应该都明白了,接下来就解释一下这三个方法分别在什么时候处理事件。
- dispatchTouchEvent方法用于事件的分发,Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发,事件没有被消费。返回false则继续往下分发,如果是ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件。
- onInterceptTouchEvent是ViewGroup中才有的方法,View中没有,它的作用是负责事件的拦截,返回true的时候表示拦截当前事件,不继续往下分发,交给自身的onTouchEvent进行处理。返回false则不拦截,继续往下传。这是ViewGroup特有的方法,因为ViewGroup中可能还有子View,而在Android中View中是不能再包含子View的。
- onTouchEvent方法用于事件的处理,返回true表示消费处理当前事件,返回false则不处理,交给子控件进行继续分发。Android中事件的构成以及事件处理方法的基本概念介绍到这,接下来就通过一系列的测试来验证以及梳理总结。
二、实验测试
测试的时候,分为以下几种情况,不同的情况下事件的传递机制是不一样的,但是事件传递原理都一样,所以不要混淆。
1、无子控件情况
新建一个工程,在MainActivity里面重写dispatchTouchEvent(MotionEvent ev)以及onTouchEvent(MotionEvent event)方法,代码如下:
package com.example.toucheventdemo;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
public class MainActivity extends Activity {
private CustomButton mButton_top;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton_top = (CustomButton) this.findViewById(R.id.cusbutton_top);
mButton_top.setOnClickListener(this);
mButton_top.setOnTouchListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MainActivity--dispatchTouchEvent:"
+ "---MotionEvent.ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MainActivity--dispatchTouchEvent:"
+ "---MotionEvent.ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
System.out.println("MainActivity--dispatchTouchEvent:"
+ "---MotionEvent.ACTION_UP---");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("MainActivity--onTouchEvent:"
+ "---MotionEvent.ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("MainActivity--onTouchEvent:"
+ "---MotionEvent.ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
System.out.println("MainActivity--onTouchEvent:"
+ "---MotionEvent.ACTION_UP---");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
xml布局文件为空,不添加控件,运行程序,点击屏幕,LogCat输出如下:
流程图如下:
通过日志输出可以看到,点击屏幕ACTION_DOWN时候首先执行了Activity的dispatchTouchEvent方法进行事件分发,然后执行了onTouchEvent(MotionEvent event)方法;在屏幕上移动ACTION_MOVE的时候,也先是调用了Activity的dispatchTouchEvent方法,接着调用了onTouchEvent(MotionEvent event)方法,离开屏幕ACTION_UP的时候同理。dispatchTouchEvent方法的返回值是super.dispatchTouchEvent(event),因此调用了父类方法,进入Activity.java的源码中看看具体实现。
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方法,代码如下:
public void onUserInteraction() {}
可以看到该方法是一个空方法,所以其实可以在Activity中被重写,在事件被分发前会调用该方法。该方法的返回值是void型,不会对事件传递结果造成影响,接着会判断getWindow().superDispatchTouchEvent(ev)的执行结果,看看它的源码:
Activity.java
/**
* 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方法了。这也和打印输出的log一致。
那么改一下代码,使dispatchTouchEvent(MotionEvent ev)方法返回true,log输出如下:
流程图如下:
System.out.println( "super.dispatchTouchEvent(ev):"+ super.dispatchTouchEvent(ev)+"" );
log输出如下:
这说明默认情况下,如果消息没有被消费(处理),会返回false,接着事件会向下传递。如果最后也没有被处理消费,消息会向上返回回去,直到完成一个传递的过程。
2、加入显示控件情况
自定义一个Button:CustomButton,代码如下:
package com.example.toucheventdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
public class CustomButton extends Button {
public CustomButton(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public CustomButton(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public CustomButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomButton--dispatchTouchEvent", "MotionEvent.ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomButton--dispatchTouchEvent", "MotionEvent.ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i("CustomButton--dispatchTouchEvent", "MotionEvent.ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomButton--onTouchEvent", "MotionEvent.ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomButton--onTouchEvent", "MotionEvent.ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i("CustomButton--onTouchEvent", "MotionEvent.ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
在Button里面,重写了 dispatchTouchEvent(MotionEvent event) 以及onTouchEvent(MotionEvent event)方法,并在里面加入了日志输出。运行程序,点击自定义的Button,输出如下(感觉Android Log输出比java System方便观看,之后使用Log):
ACTION_DOWN以及ACTION_UP事件传递流程图如下:
可以看到,按下时候触屏消息先是传递到Activity,由Activity的dispatchTouchEvent方法接受,然后传递到CustomButton的dispatchTouchEvent方法,最后传递到CustomButton的onTouchEvent方法。抬起时候触屏消息也是先从Activity的dispatchTouchEvent方法接受,然后是CustomButton的dispatchTouchEvent方法,最后是CustomButton的onTouchEvent方法。这种方式就像数据结构中的队列一样,先进先出的模式。下面点击button以外的部分,输出如下图中的第二个矩形,和之前没有button一样。
重点是第三个矩形,这个是在把CustomButton中的onTouchEvent返回改为false,也就是button不消费点击事件。那么会由Activity的onTouchEvent方法接受处理,以后消息也不会再由CustomButton处理了,而是由activity来处理,注意看和第一个矩形的区别。
当把CustomButton中的onTouchEvent返回改为true的情况下,其实是和默认情况下一样的,这也说明默认情况下button消费了点击事件。log输出如下:
紧接着,把对onTouchEvent的更改取消,把CustomButton的dispatchTouchEvent方法的返回值设为true,也就是让消息到这里就不往下传递了。log输出如下:
可以看到,CustomButton的dispatchTouchEvent方法已经把消息处理了,然后返回到activity里处理。下面再把activity的dispatchTouchEvent方法返回值改为true,不难猜出结果是消息被activity处理了,看log输出:
结果确实也是这样。截止到这里先进行一个总结,在消息传递的过程中,首先由Activity的dispatchTouchEvent方法进行事件分发,如果返回值为true,则消息不往下分发,之后只由activity处理。如果返回值为false,则消息往下传递,传递到view 视图里面,再由容器或者控件来处理。具体流程如下图所示:
代码如下:
package com.example.toucheventdemo;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
public class MainActivity extends Activity implements OnClickListener,
OnTouchListener {
private CustomButton mButton_top ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout. activity_main);
mButton_top = (CustomButton) this.findViewById(R.id.cusbutton_top);
mButton_top.setOnClickListener(this);
mButton_top.setOnTouchListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log. i("MainActivity--dispatchTouchEvent", "MotionEvent.ACTION_DOWN" );
break;
case MotionEvent.ACTION_MOVE:
Log. i("MainActivity--dispatchTouchEvent", "MotionEvent.ACTION_MOVE" );
break;
case MotionEvent.ACTION_UP:
Log. i("MainActivity--dispatchTouchEvent", "MotionEvent.ACTION_UP" );
break;
default:
break;
}
return super .dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log. i("MainActivity--onTouchEvent", "MotionEvent.ACTION_DOWN" );
break;
case MotionEvent.ACTION_MOVE:
Log. i("MainActivity--onTouchEvent", "MotionEvent.ACTION_MOVE" );
break;
case MotionEvent.ACTION_UP:
Log. i("MainActivity--onTouchEvent", "MotionEvent.ACTION_UP");
break;
default:
break;
}
return super .onTouchEvent(event);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (v.getId()) {
case R.id.cusbutton_top :
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log. i("MainActivity--onTouch", "MotionEvent.ACTION_DOWN" );
break;
case MotionEvent.ACTION_MOVE:
Log. i("MainActivity--onTouch", "MotionEvent.ACTION_MOVE" );
break;
case MotionEvent.ACTION_UP:
Log. i("MainActivity--onTouch", "MotionEvent.ACTION_UP" );
break;
default:
break;
}
break;
default:
break;
}
return false ;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.cusbutton_top :
Log. i("MainActivity--onClick", "clicked");
break;
default:
break;
}
}
}
可以看到,在activity里面,也加入了
onTouch以及
onClick方法,
然后运行程序,点击button,log输出如下:
发现有两个特别的地方,一个是在CustomButton的dispatchTouchEvent方法执行完后消息并没有传递到CustonButton的onTouchEvent()方法,而是传递到Activity的的onTouch()方法,这个是在Activity中重载的方法,也是一般为控件设置点击事件时常用的手段。它出现在CustomButton的dispatchTouchEvent方法之后,拿肯定和这个方法有关系。
到CustomButton中的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;
}
挑选关键代码进行分析,可以看到这里有几个条件,当几个条件都满足时该方法就返回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中为CustomButton设置的onTouchListener,条件(mViewFlags & ENABLED_MASK) == ENABLED判断的是当前View是否是ENABLE的,默认都是ENABLE状态的。接着就是li.mOnTouchListener.onTouch(this, event)条件,这里调用了onTouch方法,该方法的调用就是在MainActivity.java中为CustomButton设置的监听回调,如果该方法返回true,则整个条件都满足,dispatchTouchEvent就返回true,表示该事件就不继续向下分发了,因为已经被onTouch消费了。如果onTouch返回的是false,则这个判断条件不成立,接着执行onTouchEvent(event)方法进行判断,如果该方法返回true,表示事件被onTouchEvent处理了,则整个事件分发dispatchTouchEvent就返回true。到目前为止,ACTION_DOWN的事件经过了从Activity到CustomButton的分发,然后经过onTouch和onTouchEvent的处理,最终,ACTION_DOWN事件交给了CustomButton得onTouchEvent进行处理。从屏幕抬起时,会发生ACTION_UP事件。从之前输出的日志中可以看到,ACTION_UP事件同样从Activity开始到CustomButton进行分发和处理,最后,由于注册了onClick事件,当onTouchEvent执行完毕后,就调用了onClick事件,那么onClick是在哪里被调用的呢?继续回到View.java的源代码中寻找。由于onTouchEvent在View.java中的源码比较长,这里贴重点,通过源码阅读,在ACTION_UP的处理分支中可以看到一个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);这句代码,这里就执行了CustomButton实现的onClick方法,onClick是在onTouchEvent中被执行的,并且,onClick要后于onTouch的执行。
3、加入容器控件情况
首先自定义一个ViewGroup的子类,为了简单起见,在这里以继承于RelativeLayout为例。同样地,重写dispatchTouchEvent()方法以及onTouchEvent()方法。而且,在ViewGroup类里面还有有一个特有的方法:onInterceptTouchEvent()方法,ViewGroup的源码中看看该方法的实现。
ViewGroup.java
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
该方法的实现很简单,只返回了一个false。这说明在默认情况下,这个方法是不会拦截消息的。这个方法的存在也是容器控件和显示控件(如TextView、Button、ImageView等)的一个重要区别。容器控件代码如下:
package com.example.toucheventdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.RelativeLayout;
public class CustomRelativeLayout extends RelativeLayout {
public CustomRelativeLayout(Context context) {
super(context);
}
public CustomRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomRelativeLayout(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomRelativeLayout--dispatchTouchEvent",
"MotionEvent.ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomRelativeLayout--dispatchTouchEvent",
"MotionEvent.ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i("CustomRelativeLayout--dispatchTouchEvent",
"MotionEvent.ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomRelativeLayout--onInterceptTouchEvent",
"MotionEvent.ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomRelativeLayout--onInterceptTouchEvent",
"MotionEvent.ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i("CustomRelativeLayout--onInterceptTouchEvent",
"MotionEvent.ACTION_UP");
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomRelativeLayout--onTouchEvent",
"MotionEvent.ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomRelativeLayout--onTouchEvent",
"MotionEvent.ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.i("CustomRelativeLayout--onTouchEvent", "MotionEvent.ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
xml布局文件如下:
<RelativeLayout 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:paddingBottom= "@dimen/activity_vertical_margin"
android:paddingLeft= "@dimen/activity_horizontal_margin"
android:paddingRight= "@dimen/activity_horizontal_margin"
android:paddingTop= "@dimen/activity_vertical_margin"
tools:context= ".MainActivity" >
<com.example.toucheventdemo.CustomButton
android:id="@+id/cusbutton_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_margin="10dp"
android:background="@android:color/holo_red_dark"
android:text="CustomButton" />
<com.example.toucheventdemo.CustomRelativeLayout
android:id="@+id/layout_rl2"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_below="@id/cusbutton_top"
android:layout_centerHorizontal="true"
android:background="@android:color/holo_green_dark"
android:orientation="vertical" >
<com.example.toucheventdemo.CustomButton
android:id="@+id/cusbutton_middle1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_margin="10dp"
android:background="@android:color/holo_red_dark"
android:text="CustomButton1" />
<com.example.toucheventdemo.CustomButton
android:id="@+id/cusbutton_middle2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/cusbutton_middle1"
android:layout_centerHorizontal="true"
android:layout_margin="10dp"
android:background="@android:color/holo_red_dark"
android:padding="10dp"
android:text="CustomButton2" />
<com.example.toucheventdemo.CustomLinearLayout
android:id="@+id/layout_rl3"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_below="@id/cusbutton_middle2"
android:layout_centerHorizontal="true"
android:background="@android:color/holo_blue_bright"
android:orientation="vertical" >
<com.example.toucheventdemo.CustomButton
android:id="@+id/cusbutton_middle3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/cusbutton_middle1"
android:layout_centerHorizontal="true"
android:layout_margin="10dp"
android:background="@android:color/holo_red_dark"
android:text="CustomButton3" />
</com.example.toucheventdemo.CustomLinearLayout>
</com.example.toucheventdemo.CustomRelativeLayout >
</RelativeLayout>
运行程序界面如下:
关于布局的结构,相信也都看得懂,不多做介绍。下面同样分别以几种情形来展开说明:
1、点击绿色区域,也就是不点击显示控件,只点击容器控件。先不看log输出,按照之前的经验,简单分析一下。首先Activity的dispatchTouch()方法会接受触屏消息,如果没有消费对的话消息将传递到容器控件,然后由容器控件的dispatchTouch()方法接受,如果在这里消息也还是没有消费的话,将继续分发到容器控件的onTouchEvent()方法。到这里消息已经传递到最底层了,然后如果没有消费的话,消息将返回到Activity的onTouchEvent接着处理。分析完后,看一下log输出:
可以看到之前的分析是正确的,确实是按照流程走的。但是多了一个 onInterceptTouchEvent()方法,当然已经在容器控件里面添加这个方法了,重点是它出现的位置。是在容器控件CustomRelativeLayout的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则拦截当前事件。之前介绍过,dispatchTouchEvent()用于事件的分发,onInterceptTouchEvent()用于事件的拦截,onTouchEvent()用于事件的处理。这好比一条河流,在源头处分发水源,河流两侧有水闸,在不需要的地方进行拦截,在需要的地方进行处理。如果把onInterceptTouchEvent()返回值改为true,也就是消费了消息,按照经验应该是在CustomRelativeLayout里面不会传递到onTouch()方法,而是直接返回到Activity的onTouch()方法处理。看结果如何:
发现不是之前想象的那样,还是调用了CustomRelativeLayout的onTouch()方法,这就说明onInterceptTouchEvent()方法没有拦截容器控件的触屏事件,那是不是是显示控件呢?带着疑问测试一下,点击红色的CustomButton1或者CustomButton2都一样。log输出如下:
发现并没有CustomButton的任何事情,那把对onInterceptTouchEvent()方法的改动取消,看一下结果如何:
不出所料,消息已经传递到CustomButton了。这就说明onInterceptTouchEvent()方法方法是用于对其他控件起作用,不包括自身。过程就不再细说了,根据之前的图示,相信也都看得懂。最后看一个稍微复杂一点的情况,点击CustomButton3,也就是多了容器控件CustomLinearLayout,看一下log输出:
虽然情况复杂了一点,但无非也就是多了一个容器控件的消息的判断与传递的过程。
三、总结
Android系统中将1个down事件、n个move事件、1个up事件整体作为一次逻辑上的触控操作,Down事件已经确定了处理事件的对象,则后续的move、up事件也确定了处理事件的对象。,事件是通过层级传递的,一次事件传递对应一个完整的层级关系。事件传递是从ViewGroup传递到View的,而不是反过来传递的。
触摸事件发生时(ACTION_DOWN),由系统调用Activity的dispatchTouchEvent方法,分发该事件。根据触摸事件的坐标,将此事件传递给容器控件或者显示控件的dispatchTouchEvent处理,如果是容器控件则调用onInterceptTouchEvent 判断事件是由自己处理,还是继续分发给子View。此处如果容器控件不处理Touch事件,故根据事件发生坐标,将事件传递给它的直接子View(容器控件或者显示控件)。
onTouch事件要先于onClick事件执行,onTouch在事件分发方法dispatchTouchEvent中调用,而onClick在事件处理方法onTouchEvent中被调用,onTouchEvent要后于dispatchTouchEvent方法的调用。其他控件事件处理过程同上。另外如果控件是clickable 表示其能处理Touch事件。
1) Touchevent 中,返回值是 true ,则说明消耗掉了这个事件,返回值是 false ,则没有消耗掉,会继续传递下去,这个是最基本的。
2) 事件传递的两种方式:
- 隧道方式:从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递。
- 冒泡方式:从最内层子元素依次往外传递直到根元素或在中间某一元素中由于某一条件停止传递。
Touch
事件相关方法
|
方法功能
|
ViewGroup
|
View
|
Activity
|
public boolean dispatchTouchEvent(MotionEvent ev)
|
事件分发
|
Yes
|
Yes
|
Yes
|
public boolean onInterceptTouchEvent(MotionEvent ev)
|
事件拦截
|
Yes
|
No
|
No
|
public boolean onTouchEvent(MotionEvent ev)
|
事件处理
|
Yes
|
Yes
|
Yes
|
事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
Touch 事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会以隧道方式(从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递)将事件传递给最外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由该 View 的 dispatchTouchEvent(MotionEvent ev) 方法对事件进行分发。dispatchTouchEvent 的事件分发逻辑如下:
Touch 事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会以隧道方式(从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递)将事件传递给最外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由该 View 的 dispatchTouchEvent(MotionEvent ev) 方法对事件进行分发。dispatchTouchEvent 的事件分发逻辑如下:
- 如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;
- 如果 return false,事件分发分为两种情况:
- 如果当前 View 获取的事件直接来自 Activity,则会将事件返回给 Activity 的 onTouchEvent 进行消费;
- 如果当前 View 获取的事件来自外层父控件,则会将事件返回给父 View 的 onTouchEvent 进行消费。
事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
在外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系统默认的 super.dispatchTouchEvent(ev) 情况下,事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件拦截逻辑如下:
- 如果 onInterceptTouchEvent 返回 true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;
- 如果 onInterceptTouchEvent 返回 false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;
- 如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),事件默认不会被拦截。
事件响应:public boolean onTouchEvent(MotionEvent ev)
在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。onTouchEvent 的事件响应逻辑如下:
在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。onTouchEvent 的事件响应逻辑如下:
- 如果事件传递到当前 View 的 onTouchEvent 方法,而该方法返回了 false,那么这个事件会从当前 View 向上传递,并且都是由上层 View 的 onTouchEvent 来接收,如果传递到上面的 onTouchEvent 也返回 false,这个事件就会“消失”,而且接收不到下一次事件。
- 如果返回了 true 则会接收并消费该事件。
- 如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。
四、应用
需求:要做一个完全通过flip手势来切换的界面。在最上层用一个ViewFlipper作为容器,并检测flip手势操作。
难题:ViewFlipper的flip手势检测需要的MotionEvent会被各种子View的触摸检测给拦截了。比如界面上有一个Button,则当手指按下Button(还没有抬起)然后flip出Button,则最上层的flip手势检测无效。
原因:android对Touch Event的分发逻辑是View从上层分发到下层(dispatchTouchEvent函数),然后下层优先开始处理Event(先mOnTouchListener,再onTouchEvent)并向上返回处理情况(boolean值),若返回true,则上层不再处理。
于是难题出现了,你若把Touch Event都想办法给传到上层了(只能通过返回false来传到上层),那么下层的各种子View就不能处理后续事件了。
解决方案:
开始仅着眼于Touch Event处理完后的回传过程,想了N久不得,毕竟我想实现的是一个需要打破android事件处理逻辑的效果(就是一个连续性操作,只有不满足上层要求时,才轮到下层处理)。然后突然想到事件的分发过程,便豁然开朗:
覆写最上层的View的dispatchTouchEvent函数,代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (_flipDetector.onTouchEvent(event)) {
event.setAction(MotionEvent.ACTION_CANCEL);
}
return super.dispatchTouchEvent(event);
}
于是效果实现。也就是在分发之前便进行手势检测处理,若检测成功,则取消下层的一切处理过程。