Android View - 事件分发,拦截,处理机制
当我们触碰手机屏幕,便会产生一个触碰事件。由于View体系是以一种树状结构存在的(参考
在我的博文Android View - 控件架构),那么哪个View或者ViewGroup会响应这个事件呢?Android系统提供了一套完善的事件分发,拦截,处理机制,帮助开发者完成准确的事件分发和处理。
在《Android群英传》中有这么一个例子,可以帮助我们理解事件分发,拦截,处理机制。
假设有一家公司的员工分级如下:
总经理,级别最高。
软件开发部部长,级别次于总经理。
软件开发者,级别最低。
总经理从董事会接收到一个开发任务(添加xx功能),那么总经理就会把任务分给软件开发部部长,部长又把任务安排给软件开发者。开发者完成任务后,报告部长完成任务,部长觉得任务完成得不错,便报告总经理,总经理看了,也觉得不错,就签了名,交给董事会。那么这个任务也就算是完成了。
其实上面例子就是View分发处理机制的原理。
我们先理解3个与事件分发,拦截,处理机制相关方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent。
dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event)
用于事件的分发。如果事件能传递到View(A),那么View(A)的dispatchTouchEvent一定会被调用。
onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent event)
用于事件的拦截。如果返回true,表示拦截事件,事件就不再往子View传递。如果返回false,则相反。
onTouchEvent
public boolean onTouchEvent(MotionEvent event)
用于事件的处理。如果返回true,表示不用父View处理事件,也就是事件不再回到父View,那么事件到此结束。父View的onTouchEvent方法就不会被调用。如果返回false,则相反。
稍微了解了这3个方法,我们再代码理解上面“总经理-部长-开发者”的例子:
总经理 - ViewGroupA
部长 - ViewGroupB
开发者 - MyView
ViewGroupA
package com.johan.testviewevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class ViewGroupA extends FrameLayout {
public ViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewGroupA(Context context) {
super(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupA dispatchTouchEvent ---");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "ViewGroupA onTouchEvent ---");
return super.onTouchEvent(event);
}
}
ViewGroupB
package com.johan.testviewevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class ViewGroupB extends FrameLayout {
public ViewGroupB(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewGroupB(Context context) {
super(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupB dispatchTouchEvent ---");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "ViewGroupB onTouchEvent ---");
return super.onTouchEvent(event);
}
}
MyView
package com.johan.testviewevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context) {
super(context);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "MyView dispatchTouchEvent ---");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "MyView onTouchEvent ---");
return super.onTouchEvent(event);
}
}
发现View比View少重写一个方法:onInterceptTouchEvent,因为这个方法是判断是否拦截事件,View是不包含其他View的,不用拦截事件,只处理就好。
xml布局文件
<com.johan.testviewevent.ViewGroupA 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"
tools:context="com.johan.testviewevent.MainActivity" >
<com.johan.testviewevent.ViewGroupB
android:layout_width="400dp"
android:layout_height="400dp"
android:background="@android:color/holo_blue_bright"
>
<com.johan.testviewevent.MyView
android:id="@+id/my_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/holo_green_light"
/>
</com.johan.testviewevent.ViewGroupB>
</com.johan.testviewevent.ViewGroupA>
此时,View的结构是这样的:
activity
package com.johan.testviewevent;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
MainActivity只是设置了布局文件。
我们打开应用,触碰一下MyView绿色色块。打印结果如下:
从打印结果我们可以知道事件的传递顺序和处理顺序。
事件传递顺序
ViewGroupA(总经理)-> ViewGroupB(部长)-> MyView(开发者)
事件处理顺序
MyView(开发者)-> ViewGroupB(部长)-> ViewGroupA(总经理)
从打印结果我们还知道,ViewGroup接收到事件(调用dispatchTouchEvent)后,然后会判断是否拦截事件(调用onInterceptTouchEvent),默认是不拦截事件的。
董事会再分配了一个任务,但是这个任务只能由总经理处理,此时总经理需要任务拦下来,不用分配给部长,更不用安排给开发者,自己处理。
我们从代码中实现,只要重写ViewGroupA的onInterceptTouchEvent返回true就可:
// ViewGroupA类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");
return true;
}
打印结果:
只有ViewGroupA(总经理)处理了事件。
董事会又有一个任务来了,如需要确认某些需求可以做。总经理接收到任务后,因为此事需要软件开发部部长来判断,所以把任务安排到了部长,身为部长,肯定有一定的资历,所以觉得这个任务自己就可以搞定,并不用开发者来做。所以部长就把任务给拦下来了,自己处理。处理完后,需要报告总经理,然后由总经理向董事会说明。
代码实现:
恢复ViewGroupA的代码,ViewGroupA(总经理)不能拦截事件,否则ViewGroupB(部长)不能接收到事件:
// ViewGroupA类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");
return super.onInterceptTouchEvent(ev);
}
// ViewGroupB类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
return true;
}
打印结果:
董事会又来了一个小开发任务,不太care的小任务。同样,总经理接收到任务后,安排给部长,部长自然把任务分到开发者手中。开发者接收到任务后,可能公司给得待遇不能满足开发者的要求,开发者想跳槽了,就随便做,报告部长完成任务。当然部长觉得开发者完成的不行,不敢报告总经理,而且这个任务不太重要,所以决定不上报,任务到此结束。
我们用代码模拟这个场景:
我们只需要改ViewGroupB的代码,不再拦截事件,但是要处理事件,不在返回给父View(ViewGroupA)。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "ViewGroupB onTouchEvent ---");
return true;
}
打印结果:
事件回到ViewGroupB就结束了,因为ViewGroupB的onTouchEvent方法返回true。
其实不止打印了上面的结果,还有下面的结果:
我们待会再分析。
董事会觉得上次的任务没有收到报告,虽然不是什么大事,但是还是觉得有必要做一下。总经理接收到任务,安排到了部长,部长看到了,还是这个任务,分给开发者,并督促一定要做好,否则滚蛋。此时,开发者已经找到心仪的公司了,做人要有始有终嘛,便完成了任务,但是不打算报告部长,任务到此结束。
我们在用代码模拟一下:
// MyView类
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "MyView onTouchEvent ---");
return true;
}
打印结果:
这里也一样,还打印了其他结果:
为什么onTouchEvent返回true时,会重复执行多次呢??
因为onTouchEvent返回true,表示要处理触碰事件。一个完整的事件,应该包括 按下手指-移动(或许没有)- 抬起手指,毕竟要处理事件嘛,当然想要获取所有用户手指触碰的事件,所以会分发每个触碰事件。
我们从代码去理解,改一下MyView的onTouchEvent方法的代码:
// MyView类
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "MyView onTouchEvent --- ");
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");
break;
case MotionEvent.ACTION_MOVE :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");
break;
case MotionEvent.ACTION_UP :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");
break;
default:
break;
}
return true;
}
看看打印结果你就知道了:
这你应该清楚了吧。我们再来看看如果返回false,是什么事件呢:
// MyView类
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "MyView onTouchEvent --- ");
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");
break;
case MotionEvent.ACTION_MOVE :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");
break;
case MotionEvent.ACTION_UP :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");
break;
default:
break;
}
return false;
}
打印结果:
接收的是一个down事件,由于我们不处理这个事件,返回给上级处理,所以也就不会执行多次了。
requestDisallowInterceptTouchEvent
我们在开发中可能会遇到这种情况:在ViewPager中嵌套了一个横滑列表,在拖动横滑列表时同样可能导致ViewPager的tab切换。
now,我们可以在横滑列表中使用这个方法,来阻止ViewPager拦截滑动事件。
我们先看一下ViewGroup的dispatchTouchEvent方法:
注意,代码中有一个标志:FLAG_DISALLOW_INTERCEPT,这个标志是决定ViewGroup是否要执行onInterceptTouchEvent方法。子View可以通诺getParent方法获取到父View,调用父View的requestDisallowInterceptTouchEvent设置这个标志,从而阻止父View拦截事件,子View才有机会响应事件。但是ViewGroup在分发ACTION_DOWN事件就已经重置了FLAG_DISALLOW_INTERCEPT,也就是说,如果父View重写onInterceptTouchEvent事件时,ACTION_DOWN事件返回true,也就是拦截了ACTION_DOWN事件,即使子类调用了父View的requestDisallowInterceptTouchEvent(true)也不能阻止父View拦截事件。
我们代码测试一下:
// ViewGroupB类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
return true;
}
// MyView类
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(getClass().getName(), "MyView onTouchEvent --- ");
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");
break;
case MotionEvent.ACTION_MOVE :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");
// 阻止ViewGroupB拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP :
Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");
// 允许ViewGroupB拦截事件
getParent().requestDisallowInterceptTouchEvent(false);
break;
default:
break;
}
return true;
}
打印结果为:
结果显示,MyView并没有能阻止ViewGroupB拦截事件。我们改一下代码,让ViewGroupB拦截ACTION_DOWN事件:
// ViewGroupB类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
打印结果:
MyView响应了事件,成功阻止ViewGroupB拦截除ACTION_DOWN以为的事件。
我们处理在ViewPager中嵌套了一个横滑列表,在拖动横滑列表时同样可能导致ViewPager的tab切换这种情况也是这么处理(因为没找到ViewPager源码,估计ViewPager也是没有拦截ACTION_DOWN事件吧,暂时还不知道)。
点击事件
如果MyView设置了点击事件(setOnClickListener)和触碰事件(setOnTouchListener),流程会怎么样呢?
代码实现:(ViewGroupA,ViewGroupB,MyView代码回到原始,请看上面原始代码)
package com.johan.testviewevent;
import android.app.Activity;
import android.os.Bundle;
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 {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.my_view).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.e(getClass().getName(), "MyView setOnClickListener --");
}
});
findViewById(R.id.my_view).setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(getClass().getName(), "MyView setOnTouchListener --");
return false;
}
});
}
}
我们看看结果:
省略重复一段log:
我们可以看到,MyView先响应我们设置的触碰事件(setOnTouchListener),然后再响应MyView重写的onTouchEvent,最后才响应我们设置的点击事件(setOnClickListener),这样方便开发者在外界设置响应事件。
从流程可以知道,如果我们在触碰事件(setOnTouchListener)中返回true,那么在MyView重写的onTouchEvent方法不会响应,设置的点击事件(setOnClickListener)也不会响应。如果在MyView重写的onTouchEvent方法中返回true,设置的点击事件(setOnClickListener)不会响应。setOnLongClickListener和setOnClickListener同理。
如果MyView设置点击事件或者触碰事件,都代表MyView已经处理了事件,事件到此结束,ViewGroupB的onTouchEvent就不再执行。
有兴趣的可以阅读《Android开发艺术探索》的View事件体系一章。
下一篇: Android事件机制深入探讨(二)