1、前言
Android学习一段时间,需求做多了必然会遇到滑动冲突问题,比如在一个ScrollView中要嵌套一个地图View,这时候触摸移动地图或者放大缩小地图就会变得不太准确甚至没有反应,这就是遇到了滑动冲突,ScrollView中上下滑动与地图的触摸手势发生冲突。想要解决滑动冲突就不得不提到Android的事件分发机制,只有吃透了事件分发,才能对滑动冲突的解决得心应手。
作者:胖宅老鼠
链接:https://juejin.im/post/6844903829482242056
B站视频讲解:https://www.bilibili.com/video/BV1oK4y1a7ep
2、事件分发机制相关方法
Android事件分发机制主要相关方法有以下三个:
- 事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
- 事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
- 事件响应:public boolean onTouchEvent(MotionEvent ev)
以下是这三个方法在Activity、ViewGroup和View中的存在情况:
相关方法 | Activity | ViewGroup | View |
---|---|---|---|
dispatchTouchEvent | yes | yes | yes |
onInterceptTouchEvent | no | yes | no |
onTouchEvent | yes | yes | yes |
这三个方法都返回一个布尔类型,根据返回的不同对事件进行不同的分发拦截和响应。一般有三种返回true
、false
和super
引用父类对应方法。
dispatchTouchEvent 返回true:表示改事件在本层不再进行分发且已经在事件分发自身中被消费了。
dispatchTouchEvent 返回 false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent
方法进行消费。
onInterceptTouchEvent 返回true:表示将事件进行拦截,并将拦截到的事件交由本层控件 的onTouchEvent
进行处理。
onInterceptTouchEvent 返回false:表示不对事件进行拦截,事件得以成功分发到子View
。并由子View
的dispatchTouchEvent
进行处理。
onTouchEvent 返回 true:表示onTouchEvent
处理完事件后消费了此次事件。此时事件终结,将不会进行后续的传递。
onTouchEvent 返回 false:事件在onTouchEvent
中处理后继续向上层View传递,且有上层View
的onTouchEvent
进行处理。
除此之外还有一个方法也是经常用到的:
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
它的作用是子View用来通知父View不要拦截事件。下面先写一个简单的Demo
来看一下事件分发和传递:
简单的日志的Demo:
这里的代码只是自定义了两个ViewGroup
和一个View
,在其对应事件分发传递方法中打印日志,来查看调用顺序情况,所有相关分发传递方法返回皆是super
父类方法。
例如: MyViewGroupA.java:
public class MyViewGroupA extends RelativeLayout {
public MyViewGroupA(Context context) {
super(context);
}
public MyViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_UP");
break;
} return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
break;
} return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
break;
} return super.onTouchEvent(event);
}
}
其他的代码都是类似的,这里再贴一下Acitivity里的布局:
<?xml version="1.0" encoding="utf-8"?>
<com.example.sy.eventdemo.MyViewGroupA xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/viewGroupA"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context=".MainActivity">
<com.example.sy.eventdemo.MyViewGroupB
android:id="@+id/viewGroupB"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_centerInParent="true"
android:background="@android:color/white">
<com.example.sy.eventdemo.MyView
android:id="@+id/myView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:background="@android:color/holo_orange_light" />
</com.example.sy.eventdemo.MyViewGroupB>
</com.example.sy.eventdemo.MyViewGroupA>
Demo中的Activity布局层级关系:
除去外层Activity和Window的层级,从MyViewGroup开始是自己定义的打印日志View。接下来运行Demo查看日志:
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
D/MyViewGroupB: onTouchEvent:ACTION_DOWN
D/MyViewGroupA: onTouchEvent:ACTION_DOWN
D/MainActivity: onTouchEvent:ACTION_DOWN
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MainActivity: onTouchEvent:ACTION_MOVE
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MainActivity: onTouchEvent:ACTION_UP
结合日志可以大概看出(先只看ACTION_DOWN
事件):
事件的分发顺序:Activity-->MyViewGroupA-->MyViewGroupB-->MyView
自顶向下分发
事件的响应顺序:MyView-->MyViewGroupB-->MyViewGroupA-->Activity
自底向上响应消费
同时这里通过日志也发现一个问题:
- 问题一:为什么这里只有
ACTION_DOWN
事件有完整的从Activity到ViewGroup再到View的分发拦截和响应的运行日志,为什么ACTION_MOVE
和ACTION_UP
事件没有?
接着再测试一下之前提的requestDisallowInterceptTouchEvent
方法的使用。现在布局文件中将MyView添加一个属性android:clickable="true"
。此时在运行点击打印日志是这样的:
/-------------------ACTION_DOWN事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/-------------------ACTION_MOVE事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
D/MyView: dispatchTouchEvent:ACTION_MOVE
D/MyView: onTouchEvent:ACTION_MOVE
/-------------------ACTION_UP事件------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: onInterceptTouchEvent:ACTION_UP
D/MyView: dispatchTouchEvent:ACTION_UP
D/MyView: onTouchEvent:ACTION_UP
这下ACTION_MOVE
和ACTION_UP
事件也有日志了。接下来在MyViewGroupB的onInterceptTouchEvent
的方法中修改代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
return false;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
return true;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
return true;
}
return false;
}
也就是拦截下ACTION_MOVE
和ACTION_UP
事件不拦截下ACTION_DOWN
事件,然后在运行查看日志:
/------------------ACTION_DOWN事件------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/------------------ACTION_MOVE事件-----------------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
/------------------ACTION_UP事件-------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: onTouchEvent:ACTION_UP
D/MainActivity: onTouchEvent:ACTION_UP
根据日志可知ACTION_MOVE
和ACTION_UP
事件传递到MyViewGroupB就没有再向MyView传递了。接着在MyView的onTouchEvent方法中调用requestDisallowInterceptTouchEvent
方法通知父容器不要拦截事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
再次运行查看日志:
/------------------ACTION_DOWN事件------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
D/MyView: dispatchTouchEvent:ACTION_DOWN
D/MyView: onTouchEvent:ACTION_DOWN
/------------------ACTION_MOVE事件-----------------------------
D/MainActivity: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
D/MyView: dispatchTouchEvent:ACTION_MOVE
D/MyView: onTouchEvent:ACTION_MOVE
/------------------ACTION_UP事件-------------------------------
D/MainActivity: dispatchTouchEvent:ACTION_UP
D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
D/MyView: dispatchTouchEvent:ACTION_UP
D/MyView: onTouchEvent:ACTION_UP
这时可以发现ACTION_MOVE
和ACTION_UP
事件又传递到了MyView中并且两个ViewGroup中都没有执行onInterceptTouchEvent
方法。 明显是requestDisallowInterceptTouchEvent
方法起了作用。但是又出现了两个新问题。
- 问题二:为什么将设置
clickable="true"
之后ACTION_MOVE
和ACTION_UP
事件就会执行了? - 问题三:
requestDisallowInterceptTouchEvent
方法是怎样通知父View不拦截事件,为什么连onInterceptTouchEvent
方法也不执行了?
想弄明白这些问题就只能到源码中寻找答案了。
3、事件分发机制源码
在正式看源码之前先讲一个概念:事件序列
我们常说的事件,一般是指从手指触摸到屏幕在到离开屏幕这么一个过程。在这个过程中其实会产生多个事件,一般是以ACTION_DOWN
作为开始,中间存在多个ACTION_MOVE
,最后以ACTION_UP
结束。我们称一次ACTION_DOWN-->ACTION_MOVE-->ACTION_UP
过程称为一个事件序列。
ViewGroup中有一个内部类TouchTarget,这个类将消费事件的View封装成一个节点,使得可以将一个事件序列的DOWN
、MOVE
、UP
事件构成一个单链表保存。ViewGroup中也有个TouchTarget
类型的成员mFirstTouchTarget
用来指向这个单链表头。在每次DOWN
事件开始时清空这个链表,成功消费事件后通过TouchTarget.obtain
方法获得一个TouchTarget
,将消费事件的View传入,然后插到单链表头。后续MOVE
、UP
事件可以通过判断mFirstTouchTarget
来知道之前是否有能够消费事件的View。
TouchTarget的源码:
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
//接受事件的View
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
//下一个TouchTarget的地址
public TouchTarget next;
private TouchTarget() {
}
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) {
throw new IllegalArgumentException("child must be non-null");
}
final TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
}
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
}
Activity中的dispatchTouchEvent方法:
接下来正式按照分发流程来阅读源码,从Activity的dispatchTouchEvent
方法开始看起,事件产生时会先调用这个方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
方法中先判断事件类型是ACTION_DOWN
事件会执行onUserInteraction
方法,onUserInteraction
方法在Activity中是一个空实现,在当前Activity下按下Home或者Back键时会调用此方法,这里不是重点,这里重点是关注下ACTION_DOWN
事件,ACTION_DOWN
类型事件的判断,在事件传递的逻辑中非常重要,因为每次点击事件都是以ACTION_DOWN
事件开头,所以ACTION_DOWN
事件又作为一次新的点击事件的标记。
紧接着看,在第二个