您的位置:首页 > 移动开发 > Android开发

Android Touch事件的传递机制

2017-06-14 14:20 260 查看
android中会经常遇到多个View ViewGroup嵌套的问题,如果想要快速的解决这种问题,就需要对View的事件传递有较深入的理解。一次完整的事件传递机制,主要是三个阶段,分别是事件的分发,拦截和消费。

1 触摸事件的主要类型

触摸事件对应的是MotionEvent类,事件的类型主要有如下三种。

ACTION_DOWN:用户手指按下的操作,一个按下操作标志着一次触摸事件的开始。

ACTION_MOVE:用户手指按压屏幕后,在松开之前,如果移动的距离超过一定的阈值,那么会被判定为ACTION_MOVE的操作,一般情况下,手指的轻微移动都会触发一系列的移动事件。

ACTION_UP:用户手指离开 屏幕的操作,一次抬起操作标志者这一次触摸事件的结束。

在一次触摸事件操作中,ACTION_DOWN和ACTION_UP这两个事件是必须的,而ACTION_MOVE视情况而定,如果用户只点击了一下屏幕,那么可能只会监听到按下和抬起的动作。

2 事件传递的三个阶段

分发(Dispatch):事件的分发对应着dispatchTouchEvent方法,在Android系统中,所有的触摸事件都是通过这个方法来分发的,方法原型如下:

// 方法值返回true,表示事件被当前视图消费掉,不再继续分发事件
// 方法值返回super.dispatchTouchEvent 表示继续分发该事件

public boolean dispatchTouchEvent(MotionEvent ev)


拦截(Intercept):事件的拦截对应着onInterceptTouchEvent方法,这个方法只在ViewGroup及其子类中才存在,在View和Activity中是不存在的,方法原型如下:

// 方法值返回true,表示拦截这个事件,不继续分发给子视图,同时交由自身的onTouchEvent方法进行消费
// 方法值返回false或者super.onInterceptTouchEvent表示不对事件进行拦截,需要继续传递给子视图

public boolean onInterceptTouchEvent(MotionEvent ev)


消费(Consume):事件的消费对应着onTouchEvent方法,方法原型如下:

// 方法值返回true,表示当前视图可以处理对应的事件,事件将不会向上传递给父视图
// 方法值返回false,表示当前视图不处理这个事件,事件会被传递给父视图的onTouchEvent方法处理

public boolean onTouchEvent(MotionEvent ev)


在Android 系统中,拥有事件传递能力的类有以下三种:

Activity:拥有dispatchTouchEvent和onTouchEvent两
12bcb
个方法

ViewGroup:拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法

View:拥有dispatchTouchEvent和onTouchEvent两个方法

注意:这里的View是不包括ViewGroup的View控件,比如Button,TextView等本身已经是最小的单位。

3 View的事件传递机制

// 自定义MyTextView
public class MyTextView extends TextView {
private static final String TAG = "MyTextView";

public MyTextView(Context context) {
super(context);
}

public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}


自定义一个MyTextView 重写dispatchTouchEvent()和onTouchEvent()方法,将每个事件触发都打印Log日志。在定义一个MainActivity,在MainActivity中监听MyTextView对象的触摸和点击事件。

public class MainActivity extends AppCompatActivity implements View.OnTouchListener, View.OnClickListener {
private static final String TAG = "MainActivity";
private TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.my_text_view);
mTextView.setOnClickListener(this);
mTextView.setOnTouchListener(this);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}

@Override
public boolean onTouch(View v, MotionEvent event) {
switch (v.getId()) {
case R.id.my_text_view :
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG, "MyTextView onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE :
Log.e(TAG, "MyTextView onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP :
Log.e(TAG, "MyTextView onTouch ACTION_UP");
break;
default:
break;
}
break;
default:
break;
}
return false;
}

@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.my_text_view:
Log.e(TAG, "MyTextView onClick");
break;
default:
break;
}
}
}


当我们点击MyTextView的对象时,打印log如下:

com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onClick


可以看出一下结论:

1. 触摸事件的传递流程从dispatchTouchEvent开始,如果不进行人为干预,则事件会依靠嵌套从外层向里层传递,到达最里层的view时,就由它的onTouchEvent处理,如果它消费不了就依次从里向外传递,由外层view的onTouchEvent方法进行处理。

2. 如果事件在向内层传递过程中人为干预,事件处理函数返回true,则会导致事件提前被消费掉,内层view将不会接收到这个事件。

3. view控件的事件触发顺序是先执行onTouch方法,在最后才执行onClick方法,如果onTouch返回true,则事件不会继续传递,最后也不会调用onClick方法,如果onTouch返回false,则事件继续传递。

4. 另外,dispatchTouchEvent()方法中还有“记忆”的功能,如果第一次事件向下传递到某View,它把事件继续传递交给它的子View,它会记录该事件是否被它下面的View给处理成功了,(怎么能知道呢?如果该事件会再次被向上传递到我这里来由我的onTouchEvent()来处理,那就说明下面的View都没能成功处理该事件);当第二次事件向下传递到该View,该View的dispatchTouchEvent()方法机会判断,若上次的事件由下面的view成功处理了,那么这次的事件就继续交给下面的来处理,若上次的事件没有被下面的处理成功,那么这次的事件就不会向下传递了,该View直接调用自己的onTouchEvent()方法来处理该事件。

4 ViewGroup的事件传递机制

ViewGroup是作为view控件的容器存在的,ViewGroup拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法,可以看出和View的唯一区别是多了一个onInterceptTouchEvent方法。我们自定义一个MyRelativeLayout 继承RelativeLayout。

// 自定义RelativeLayout
public class MyRelativeLayout extends RelativeLayout{

private static final String TAG = "MyRelativeLayout";

public MyRelativeLayout(Context context) {
super(context);
}

public MyRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE :
Log.e(TAG,"onInterceptTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP :
Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onInterceptHoverEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}


修改xml文件,将这个Layout作为MyTextView的容器,如下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.jmf.advancedlevel.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.example.jmf.advancedlevel.MyTextView
android:id="@+id/my_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请点击我"
android:textSize="20sp" />

</com.example.jmf.advancedlevel.MyRelativeLayout>


运行,点击MyTextView,打印Log日志如下:

com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onClick


从上面的日志情况得到如下结论:

1. 触摸事件的传递顺序是由Activity到ViewGroup,再由ViewGroup递归传递给它的子View

2. ViewGroup通过onInterceptTouchEvent方法对事件进行拦截,如果该方法返回true,则事件不会继续传递给子View,如果返回false或者super.onInterceptTouchEvent,则事件会继续传递给子View。

3. 在子View中对事件进行消费后,ViewGroup将接收不到任何事件

5关于事件的拦截和反拦截

1.5.1 怎么拦截事件?很简单,复写ViewGroup的onInterceptTouchEvent方法:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
return true;
case MotionEvent.ACTION_MOVE :
Log.e(TAG,"onInterceptTouchEvent ACTION_MOVE");
return true;
case MotionEvent.ACTION_UP :
Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
return true;
}
return true;
}


默认返回false 或者super.onInterceptTouchEvent,表示是不拦截事件,现在运行,如下:

com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_MOVE
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_MOVE
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_UP


Log中可以看到MyTextView中什么事件也接受不到,如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;

1.5.2 怎么实现反拦截,就是不让父类拦截子View?

Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// getParent().requestDisallowInterceptTouchEvent(true);  这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。
getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}


从源码也可以解释:

ViewGroup MOVE和UP拦截的源码是这样的:

if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}


当我们把disallowIntercept设置为true时,!disallowIntercept直接为false,于是拦截的方法体就被跳过了~

注:如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN里面直接return true了,那么子View是木有办法的捕获事件的~~~

6 总结

1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;

2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法

3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: