Android 事件分发机制
最近在做安卓相册的时候,我遇到了一个棘手的问题:左右翻页使用 ViewPager2
,而 ViewPager2
是继承了 RecyclerView
的控件,它有一个默认消费 onTouchEvent
的操作。这导致如果想简单通过 ImageView
的 Listener 来实现监听 ImageView
上的放大缩小操作变得不可行。因为除了 ACTION_DOWN
事件会被传递到 ImageView
外,其他操作都被 ViewPager2
拦截了。尽管 ViewPager2
没有传统意义上的拦截器操作,但这时候就需要了解一个重要的概念:事件分发。
事件序列
在 Android 中,传统的点击事件(onTouchEvent
)从开始到结束一般分为四个过程:
- 按下(ACTION_DOWN)
- 移动(ACTION_MOVE)
- 抬起(ACTION_UP)
- 取消(ACTION_CANCEL)
这些过程的顺序如下:
- 当手指触碰到屏幕时,产生
ACTION_DOWN
事件。 - 手指在屏幕上移动时,产生
ACTION_MOVE
事件。(在ACTION_CANCEL
或ACTION_UP
之前可以有多个ACTION_MOVE
事件) - 事件被其他操作意外中断时,会产生
ACTION_CANCEL
事件。 - 手指从屏幕抬起时,产生
ACTION_UP
事件。
下面是一个处理事件的简单 switch
方法示例:
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// Handle ACTION_DOWN
break;
case MotionEvent.ACTION_MOVE:
// Handle ACTION_MOVE
break;
case MotionEvent.ACTION_UP:
// Handle ACTION_UP
break;
case MotionEvent.ACTION_CANCEL:
// Handle ACTION_CANCEL
break;
}
return super.onTouchEvent(ev);
}
通过以上代码,可以在 onTouchEvent
中进行简单的逻辑处理。
事件分发的关键方法
在事件分发的过程中,有三个关键方法:
dispatchTouchEvent()
:负责分发事件,当点击事件能够被传递到当前View
时,该方法被调用。onInterceptTouchEvent()
:用于判断是否拦截某个事件,只有ViewGroup
中存在此方法。onTouchEvent()
:用于处理点击事件,所有面向点击结果的操作逻辑都在这里实现。
dispatchTouchEvent()
dispatchTouchEvent()
是事件分发的入口。它负责将事件分发给子视图。如果返回 true
,表示事件被消费;如果返回 false
,则继续向下传递。
onInterceptTouchEvent()
onInterceptTouchEvent()
是拦截器,只存在于 ViewGroup
。它用于判断是否拦截某个事件,阻止事件继续传递给子视图。
onTouchEvent()
onTouchEvent()
是事件处理的关键方法。它负责处理 View
上的点击事件,并返回 true
表示事件被消费。
事件传递的对象
在 Android 中,事件传递的对象包括 Activity
、ViewGroup
和 View
:
Activity
:统筹管理整个 UI,比如视图的添加、显示、以及其他方法与View
和Window
的回调交互等。View
:我们熟悉的控件,如Button
、ImageView
等,都是继承自View
。ViewGroup
:View
的子类,表示一组View
。如LinearLayout
、RelativeLayout
等,它们可以包含多个子View
。
为什么只有 ViewGroup有拦截器?
ViewGroup
负责管理多个子 View
,所以在事件分发的过程中,需要判断是否拦截某个事件并传递给子 View
。
但是,子 View
也有反向拦截的方法,这将在下面讨论。
事件分发的过程
为了更好地理解事件分发机制,我们来看一个简单的模型:
最简单的事件分发模型
public class MyViewGroup extends ViewGroup {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 判断是否拦截事件
boolean intercept = onInterceptTouchEvent(ev);
if (intercept) {
// 如果拦截,则自己处理事件
return onTouchEvent(ev);
} else {
// 如果不拦截,则分发给子 View
final int action = ev.getAction();
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (dispatchTransformedTouchEvent(ev, child, i, intercept)) {
return true;
}
}
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 在这里可以拦截某些事件,比如滑动事件
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 处理 ViewGroup 的触摸事件
return super.onTouchEvent(event);
}
}
public class MyView extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
// 处理 View 的触摸事件
return super.onTouchEvent(event);
}
}
在这个模型中,我们定义了一个自定义 ViewGroup
和一个自定义 View
。在 ViewGroup
中,事件流先进入 dispatchTouchEvent()
,经过拦截器判断是否拦截。如果拦截,则交由 onTouchEvent
处理;如果不拦截,则分发给子 View
使用。
事件消费
事件消费是事件分发中一个重要的概念。无论在 dispatchTouchEvent()
、onInterceptTouchEvent()
还是 onTouchEvent()
中,一旦某一层返回了 true
,事件就被消费,无法继续向下传递。
public class MyViewGroup extends ViewGroup {
private final String TAG = "MyViewGroup";
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "Down");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "Move");
break;
}
return super.onTouchEvent(event);
}
}
public class MyView extends View {
private final String TAG = "MyView";
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "Down");
return true;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "Move");
break;
}
return super.onTouchEvent(event);
}
}
在以上代码中,由于 MyView
在 ACTION_DOWN
事件中返回 true
,所以 MyViewGroup
无法打印出 Move
事件。
ViewGroup 的拦截器
如果我们在 MyView
中添加一个拦截器:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 在这里可以拦截某些事件,比如滑动事件
return true;
}
那么子 View
的所有操作都不会被触发。
子类反向拦截父类
在 ViewGroup
中,有一个叫 requestDisallowInterceptTouchEvent
的方法
,它可以反向拦截父类的事件。它接受一个布尔参数:
- 如果传入
true
,则父类不再处理事件,事件直接传递给子类。 - 如果传入
false
,则父类可以正常处理事件。
public class MyView extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 请求父类不再拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "Move");
break;
}
return super.onTouchEvent(event);
}
}
在这个例子中,我们在 ACTION_DOWN
事件中请求父类不再拦截事件,从而使子类可以处理 Move
事件。
应用场景
为了更好地理解事件分发机制,我们来看看如何在实际项目中应用这些知识。
实现可滑动的 ViewPager
在某些情况下,我们需要实现一个可滑动的 ViewPager
,其中的子 View
可以自行处理滑动事件。
public class MyViewPager extends ViewPager {
private boolean isPagingEnabled = true;
public MyViewPager(Context context) {
super(context);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return isPagingEnabled && super.onInterceptTouchEvent(event);
}
public void setPagingEnabled(boolean enabled) {
this.isPagingEnabled = enabled;
}
}
通过重写 onInterceptTouchEvent()
方法,我们可以控制 ViewPager
的滑动行为。通过调用 setPagingEnabled()
方法,我们可以动态控制 ViewPager
的滑动开关。
解决 RecyclerView 与 ViewPager 的事件冲突
当 RecyclerView
嵌套在 ViewPager
中时,我们可能会遇到事件冲突的问题。这是因为 RecyclerView
和 ViewPager
都有滑动处理逻辑。
public class MyRecyclerView extends RecyclerView {
public MyRecyclerView(Context context) {
super(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 请求父类不再拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return super.onTouchEvent(event);
}
}
通过在 RecyclerView
的 ACTION_DOWN
事件中调用 requestDisallowInterceptTouchEvent(true)
,我们可以防止 ViewPager
拦截滑动事件,从而解决事件冲突。
最后说几点要注意的
dispatchTouchEvent()
是事件分发的入口。onInterceptTouchEvent()
是事件拦截的判断依据,仅在ViewGroup
中存在。onTouchEvent()
是事件处理的主要场所。- 在事件消费后,事件将不再向下传递。
- 子
View
可以通过requestDisallowInterceptTouchEvent
方法反向拦截父类事件。