一、基本概念
在实际开发中,经常会遇到与触屏事件有关的问题,最典型的一个就是滑动冲突。比如在使用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)//表示滑出屏幕了
当然还有一些其他的事件,如图所示:
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);
- }
- }
流程图如下:
通过日志输出可以看到,点击屏幕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);
- }
- public void onUserInteraction() {}
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);
那么改一下代码,使dispatchTouchEvent(MotionEvent ev)方法返回true,log输出如下:
流程图如下:
- System.out.println( "super.dispatchTouchEvent(ev):"+ super.dispatchTouchEvent(ev)+"" );
这说明默认情况下,如果消息没有被消费(处理),会返回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);
- }
- }
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;
- }
- }
- }
发现有两个特别的地方,一个是在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 &
- BLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
- return true;
- }
- if (onTouchEvent(event)) {
- return true;
- }
- }
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
- }
- return false;
- }
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;
- }
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;
- }
3、加入容器控件情况
首先自定义一个ViewGroup的子类,为了简单起见,在这里以继承于RelativeLayout为例。同样地,重写dispatchTouchEvent()方法以及onTouchEvent()方法。而且,在ViewGroup类里面还有有一个特有的方法:onInterceptTouchEvent()方法,ViewGroup的源码中看看该方法的实现。
ViewGroup.java
该方法的实现很简单,只返回了一个false。这说明在默认情况下,这个方法是不会拦截消息的。这个方法的存在也是容器控件和显示控件(如TextView、Button、ImageView等)的一个重要区别。容器控件代码如下:
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- return false;
- }
- 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);
- }
- }
- <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;
- }
发现不是之前想象的那样,还是调用了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),事件默认会被拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理。
事件响应: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);
- }
转载:http://blog.csdn.net/wangjinyu501/article/details/22584465