android的Touch事件解析(dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent)
(2012-05-20 12:34:04)android中的事件类型分为按键事件和屏幕触摸事件,Touch事件是屏幕触摸事件的基础事件,
有必要对它进行深入的了解。 一个最简单的屏幕触摸动作触发了一系列Touch事件:
ACTION_DOWN->ACTION_MOVE->ACTION_MOVE->ACTION_MOVE...->ACTION_MOVE->ACTION_UP
当屏幕中包含一个ViewGroup,而这个ViewGroup又包含一个子view,这个时候android系统如何
处理Touch事件呢?到底是ViewGroup来处理Touch事件,还是子view来处理Touch事件呢?我只能
很肯定的对你说不一定。呵呵,为什么呢?看看下面我的调查结果你就明白了。
android系统中的每个View的子类都具有下面三个和TouchEvent处理密切相关的方法:
1)public boolean dispatchTouchEvent(MotionEvent ev)这个方法用来分发TouchEvent
2)public boolean onInterceptTouchEvent(MotionEvent ev)这个方法用来拦截TouchEvent
3)public boolean onTouchEvent(MotionEvent ev)这个方法用来处理TouchEvent
当TouchEvent发生时,首先Activity将TouchEvent传递给最顶层的View,TouchEvent最先到达最顶层
view 的 dispatchTouchEvent ,然后由 dispatchTouchEvent 方法进行分发,
1、如果dispatchTouchEvent返回true,则交给这个view的onTouchEvent处理,
2、如果dispatchTouchEvent返回 false ,则交给这个view的interceptTouchEvent方法来决定是否
要拦截这个事件,如果 interceptTouchEvent 返回 true,也就是
3、若果interceptTouchEvent返回true,表示拦截掉了,则交给它的onTouchEvent 来处理,
4、如果 interceptTouchEvent 返回 false,那么就传递给子view ,由子 view 的 d
ispatchTouchEvent 再来开始这个事件的分发。
5、如果事件传递到某一层的子 view 的onTouchEvent上了,这个方法返回了 false ,那么这个事件
会从这个view 往上传递,都是 onTouchEvent来接收。
6、如果传递到最上面的 onTouchEvent 也返回 false的话,这个事件就会“消失”,而且接收
不到下一次事件。
通过语言描述这个处理逻辑很抽象,下面我就用代码来具体说明一下。
layout配置文件 main.xml
< ?xml version="1.0"encoding="utf-8"?>
< test.lzqdiy.MyLinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center" >
< test.lzqdiy.MyTextView
android:layout_width="200px"
android:layout_height="200px"
android:id="@+id/tv"
android:text="lzqdiy"
android:textSize="40sp"
android:textStyle="bold"
android:background="#FFFFFF"
android:textColor="#0000FF"/>
< /test.lzqdiy.MyLinearLayout>
节点层次很简单,一个LinearLayout中添加了一个TextView。
下面是java代码:
package test.lzqdiy;
import android.app.Activity;
import android.os.Bundle;
public class TestTouchEventApp extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
package test.lzqdiy;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class MyLinearLayout extends LinearLayout {
private final String TAG = "MyLinearLayout";
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, TAG);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "dispatchTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "dispatchTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "dispatchTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "dispatchTouchEvent action:ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onInterceptTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onInterceptTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onInterceptTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "onInterceptTouchEvent action:ACTION_CANCEL");
break;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "---onTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "---onTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "---onTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "---onTouchEvent action:ACTION_CANCEL");
break;
}
return true;
}
}
package test.lzqdiy;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;
public class MyTextView extends TextView {
private final String TAG = "MyTextView";
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "dispatchTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "dispatchTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "dispatchTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "onTouchEvent action:ACTION_CANCEL");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "---onTouchEvent action:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "---onTouchEvent action:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "---onTouchEvent action:ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "---onTouchEvent action:ACTION_CANCEL");
break;
}
return true;
}
}
为了指代方便,下面将MyLinearLayout简称为L,将MyTextView简称为T,
L.onInterceptTouchEvent=true表示的含义为MyLinearLayout中的onInterceptTouchEvent
方法返回值为true,通过程序运行时输出的Log来说明调用时序。
第1种情况L.onInterceptTouchEvent=false&&L.onTouchEvent=true
&&T.onTouchEvent=true输出下面的Log:
D/MyLinearLayout(11865): dispatchTouchEventaction:ACTION_DOWN
D/MyLinearLayout(11865): onInterceptTouchEventaction:ACTION_DOWN
D/MyTextView(11865): dispatchTouchEvent action:ACTION_DOWN
D/MyTextView(11865): ---onTouchEvent action:ACTION_DOWN
D/MyLinearLayout(11865): dispatchTouchEventaction:ACTION_MOVE
D/MyLinearLayout(11865): onInterceptTouchEventaction:ACTION_MOVE
D/MyTextView(11865): dispatchTouchEvent action:ACTION_MOVE
D/MyTextView(11865): ---onTouchEvent action:ACTION_MOVE
...........省略其他的ACTION_MOVE事件Log
D/MyLinearLayout(11865): dispatchTouchEvent action:ACTION_UP
D/MyLinearLayout(11865): onInterceptTouchEventaction:ACTION_UP
D/MyTextView(11865): dispatchTouchEvent action:ACTION_UP
D/MyTextView(11865): ---onTouchEvent action:ACTION_UP
结论:TouchEvent完全由TextView处理。
第2种情况L.onInterceptTouchEvent=false&&L.onTouchEvent=true
&&T.onTouchEvent=false输出下面的Log:
D/MyLinearLayout(13101): dispatchTouchEventaction:ACTION_DOWN
D/MyLinearLayout(13101): onInterceptTouchEventaction:ACTION_DOWN
D/MyTextView(13101): dispatchTouchEvent action:ACTION_DOWN
D/MyTextView(13101): ---onTouchEvent action:ACTION_DOWN
D/MyLinearLayout(13101): ---onTouchEvent action:ACTION_DOWN
D/MyLinearLayout(13101): dispatchTouchEventaction:ACTION_MOVE
D/MyLinearLayout(13101): ---onTouchEvent action:ACTION_MOVE
...........省略其他的ACTION_MOVE事件Log
D/MyLinearLayout(13101): dispatchTouchEvent action:ACTION_UP
D/MyLinearLayout(13101): ---onTouchEvent action:ACTION_UP
结论:TextView只处理了ACTION_DOWN事件,LinearLayout处理了所有的TouchEvent。
第3种情况L.onInterceptTouchEvent=true&&L.onTouchEvent=true 输出下面的Log:
D/MyLinearLayout(13334): dispatchTouchEventaction:ACTION_DOWN
D/MyLinearLayout(13334): onInterceptTouchEventaction:ACTION_DOWN
D/MyLinearLayout(13334): ---onTouchEvent action:ACTION_DOWN
D/MyLinearLayout(13334): dispatchTouchEventaction:ACTION_MOVE
D/MyLinearLayout(13334): ---onTouchEvent action:ACTION_MOVE
...........省略其他的ACTION_MOVE事件Log
D/MyLinearLayout(13334): dispatchTouchEvent action:ACTION_UP
D/MyLinearLayout(13334): ---onTouchEvent action:ACTION_UP
结论:LinearLayout处理了所有的TouchEvent。
第4种情况L.onInterceptTouchEvent=true&&L.onTouchEvent=false 输出下面的Log:
D/MyLinearLayout(13452): dispatchTouchEventaction:ACTION_DOWN
D/MyLinearLayout(13452): onInterceptTouchEventaction:ACTION_DOWN
D/MyLinearLayout(13452): ---onTouchEvent action:ACTION_DOWN
结论:LinearLayout只处理了ACTION_DOWN事件,那么其他的TouchEvent被谁处理了呢?
答案是LinearLayout最外层的Activity处理了TouchEvent。
onInterceptTouchEvent用于改变事件的传递方向。决定传递方向的是返回值,返回为false时事件会传递给子控件,返回值为true时事件会传递给当前控件的onTouchEvent(),这就是所谓的Intercept(拦截)。
[tisa ps:正确的使用方法是,在此方法内仅判断事件是否需要拦截,然后返回。即便需要拦截也应该直接返回true,然后由onTouchEvent方法进行处理。]
onTouchEvent用于处理事件,返回值决定当前控件是否消费(consume)了这个事件。尤其对于ACTION_DOWN事件,返回true,表示我想要处理后续事件;返回false,表示不关心此事件,并返回由父类进行处理。
可能你要问是否消费了又区别吗,反正我已经针对事件编写了处理代码?答案是有区别!比如ACTION_MOVE或者ACTION_UP发生的前提是一定曾经发生了ACTION_DOWN,如果你没有消费ACTION_DOWN,那么系统会认为ACTION_DOWN没有发生过,所以ACTION_MOVE或者ACTION_UP就不能被捕获。
在没有重写onInterceptTouchEvent()和onTouchEvent()的情况下(他们的返回值都是false), 对上面这个布局,MotionEvent事件的传递顺序如下:
当某个控件的onInterceptTouchEvent()返回值为true时,就会发生截断,事件被传到当前控件的onTouchEvent()。如我们将LayoutView2的onInterceptTouchEvent()返回值为true,则传递流程变成:
如果我们同时将LayoutView2的onInterceptTouchEvent()和onTouchEvent()设置成true,那么LayoutView2将消费被传递的事件,同时后续事件(如跟着ACTION_DOWN的ACTION_MOVE或者ACTION_UP)会直接传给LayoutView2的onTouchEvent(),不传给其他任何控件的任何函数。同时传递给子空间一个ACTION_CANCEL事件。传递流程变成(图中没有画出ACTION_CANCEL事件):
[tisa ps:总体来看, onInterceptTouchEvent是自rootview向下传递, onTouchEvent正好相反。]
onInterceptTouchEvent()是ViewGroup的一个方法,目的是在系统向该ViewGroup及其各个childView触发onTouchEvent()之前对相关事件进行一次拦截.
-
down事件首先会传递到onInterceptTouchEvent()方法
-
如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。
-
如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。
-
如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。
-
如果最终需要处理事件的view 的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。
您的投票让 李剑波 声誉值增加了10分。
支持投票,不仅能让回答用户获得声誉值,让好答案排序靠前,更能帮助社区筛选出好的内容,构建高质量的知识库。
android官方文档有个标准解释,现摘录过来:
首先,看Android的官方文档正解
- onInterceptTouchEvent()与onTouchEvent()的机制:
- 1. down事件首先会传递到onInterceptTouchEvent()方法
- 2. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理
- 3. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。
- 4. 如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理
- 5. 如果最终需要处理事件的view 的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。
仅仅看这个官方文档解释,就能理解清楚这两个函数关系以及用途的绝对是富有经验的framework高手。
否则,一定需要一个案例来阐释。假设我们有这样一个layout,非常典型的
- <com.test.LayoutView1 xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical" android:layout_width="fill_parent"
- android:layout_height="fill_parent">
- <com.test.LayoutView2
- android:orientation="vertical" android:layout_width="fill_parent"
- android:layout_height="fill_parent" android:gravity="center">
- <com.test.MyTextView
- android:layout_width="wrap_content" android:layout_height="wrap_content"
- />
- </com.test.LayoutView2>
- </com.test.LayoutView1>
用一个示例图来解释这个layout:
通常外围的layoutview1,layoutview2,只是布局的容器不需要响应触屏的点击事件,仅仅Mytextview需要相应点击。但这只是一般情况,一些特殊的布局可能外围容器也要响应,甚至不让里面的mytextview去响应。更有特殊的情况是,动态更换响应对象。
那么首先看一下默认的触屏事件的在两个函数之间的传递流程。如下图:
如果仅仅想让MyTextView来响应触屏事件,让MyTextView的OnTouchEvent返回true,那么事件流就变成如下图,可以看到layoutview1,layoutview2已经不能进入OnTouchEvent:
另外一种情况,就是外围容器想独自处理触屏事件,那么就应该在相应的onInterceptTouchEvent函数中返回true,表示要截获触屏事件,比如layoutview1作截获处理,处理流变成如下图:
以此类推,我们可以得到各种具体的情况,整个layout的view类层次中都有机会截获,而且能看出来外围的容器view具有优先截获权。
当我们去做一些相对来讲具有更复杂的触屏交互效果的应用时候,经常需要动态变更touch event的处理对象,比如launcher待机桌面和主菜单(见下图),从滑动屏幕开始到停止滑动过程当中,只有外围的容器view才可以处理touch event,否则就会误点击上面的应用图标或者widget.反之在静止不动的状态下则需要能够响应图标(子view)的touch事件。摘取framework中abslistview代码如下
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- int action = ev.getAction();
-
- switch (action & MotionEvent.ACTION_MASK) {
- case MotionEvent.ACTION_DOWN: {
-
- if (touchMode == TOUCH_MODE_FLING) {
- return true; //fling状态,截获touch,因为在滑动状态,不让子view处理
- }
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- switch (mTouchMode) {
- case TOUCH_MODE_DOWN:
- final int pointerIndex = ev.findPointerIndex(mActivePointerId);
- final int y = (int) ev.getY(pointerIndex);
- if (startScrollIfNeeded(y - mMotionY)) {
- return true;//开始滑动状态,截获touch事件,不让子view处理
- }
- break;
- }
- break;
- }
- }
总结:
仅仅通过概览性的官方文档是很难理解onInterceptTouchEvent函数的用途的,只有通过演绎这个抽象的规则,配以图文才能获取这个重要的知识。很显然,默认是返回false,不做截获。返回true之后,事件流的后端控件就没有机会处理touch事件了,把默认的事件流中每个处理函数看作一个节点,这个节点只要返回true, 后续的事件就被截止了,这样想就很好理解。