1.前言
Android touch事件分发有几个方向可以深入分析
- touch事件是如何从驱动层传递给Framework层的InputManagerService?
- WMS是如何通过ViewRootImpl将事件传递到目标窗口的?
- touch 事件到达DecorView后,是如何一步步传递到内部的子View中的?
ViewGroup
ViewGroup是一组View的组合,在其内部有可能包含多个子View,当手指触摸屏幕上时,手指所在区域既能在ViewGroup显示范围内,也可能在其内部View控件上。
- 所以ViewGroup的事件分发的重点是处理当前ViewGroup和子View之间的逻辑关系:
- 当前ViewGroup是否需要拦截touch事件?
- 是否需要将touch事件继续分发给子View?
- 如何将touch事件分发给子View?
View
- View是一个单纯的控件,不能再被细分,内部也并不会存在子View,所以它的事件分发的重点是当前View如何去处理touch事件,并根据相应的手势逻辑进行一系列的效果展示
- 是否存在TouchListener
- 是否自己接收处理touch事件(主要逻辑在onTouchEvent方法中)
2.事件分发核心 dispatchTouchEvent
-
整个View之间的事件分发,实质上就是一个大的递归函数,而这个递归函数就是dispatchTouchEvent方法
- 在递归的过程中会适时调用onInterceptTouchEvent方法来拦截事件
- 或者调用 onTouchEvent 方法来处理事件
-
从宏观角度看,ViewGroup的dispatchTouchEvent方法的源码逻辑如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
/**
* 步骤1:检查当前ViewGroup是否需要拦截事件
*/
...
/**
* 步骤2:将事件分发给子View
*/
...
/**
* 步骤3:根据mFirstTouchTarget,再次分发事件
*/
...
}
- 解析:事件分发主要分为3大步骤:
步骤一:判断当前ViewGroup是否需要拦截此touch事件,如果拦截则此次touch事件不再会传递给子View(或者以CANCEL的方式通知子View)
步骤二:如果没有拦截,则将事件分发给子View继续处理,如果子View将此次事件捕获(消耗),则将变量mFirstTouchTarget赋值给捕获touch事件的View
步骤三:根据mFirstTouchTarget重新分发事件。
2.1.步骤一代码具体实现
/**
* 步骤1:检查当前ViewGroup是否需要拦截事件
*/
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
- 解析:下面这个if判断标出了是否需要事件拦截的条件:
- if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
- 如果事件为DOWN事件,则调用onInterceptTouchEvent方法,返回是否拦截的标记intercepted;
- ViewGroup的事件拦截方法onInterceptTouchEvent默认返回false,表示不拦截事件
- 或者mFirstTouchTarget不为null,代表已经有子View捕获了这个事件。
- 子View的dispatchTouchEvent返回true就是代表捕获touch事件
2.2.步骤二具体代码实现:
/**
* 步骤2:将事件分发给子View
*/
if (!canceled && !intercepted) {
...
//TODO 1.事件分发的前提是事件为DOWN事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int childrenCount = mChildrenCount;
//TODO 2.遍历所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex();
final View child = getAndVerifyPreorderedView();
//TODO 3.判断事件坐标是否处在当前子View坐标范围内,并且子View没有处在动画状态
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null))
continue;
}
//TODO 4.调用dispatchTransformedTouchEvent方法将事件分发给子View,如果子View捕获事件成功,则将newTouchTarget赋值给子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
...
}
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
- 解析
- 1.事件分发的前提是事件为DOWN事件
- 2.遍历所有子View
- 3.判断事件坐标是否处在当前子View坐标范围内,并且子View没有处在动画状态
- 4.调用dispatchTransformedTouchEvent方法将事件分发给子View,如果子View捕获事件成功,则将newTouchTarget赋值给子View
2.3.步骤三具体代码实现:
/**
* 步骤3:根据 mFirstTouchTarget,再次分发事件
*/
if (mFirstTouchTarget == null) {
//TODO 1.如果mFirstTouchTarget为null,说明ViewGroup中的子view没有对事件进行捕获,
// 调用dispatchTransformedTouchEvent方法,传入的参数child为null,最终会调用 // ViewGroup自身的onTouchEvent方法
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
//TODO 1.1.如果当前是DOWN事件,子View捕获了事件,则进入该判断,handled设置为true并返回
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//TODO 2.mFirstTouchTarget 不为null,说明有子View对touch事件进行了捕获,则直接将当前以及后续的事件交给mFirstTouchTarget指向的View进行处理
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
}
dispatchTransformedTouchEvent方法源码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 如果cancel为true,则将Action设置为ACTION_CANCEL,并往下传递
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
if (child == null) {
// child为null,则调用ViewGroup的父类(View)的事件分发方法
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
// child不为null,调用子View的事件分发方法
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
- 解析:步骤3有2个分支判断
- 1.如果此时mFirstTouchTarget 为null,说明上述的事件分发中并没有子View对事件进行捕获操作。
- 这种情况下,直接调用dispatchTransformedTouchEvent 方法,并传入child为null,最终会调用super.dispatchTouchEvent方法,实际上最终会调用自身的onTouchEvent方法,进行touch事件处理。
- 也就是说:如果没有子View捕获处理touch事件,ViewGroup会通过自身的onTouchEvent方法进行处理
- 2.如果mFirstTouchTarget 不为null,说明ViewGourp中有子View对touch事件进行捕获,则直接将当前以及后续的事件交给mFirstTouchTarget 指向的View进行处理。
- 1.如果此时mFirstTouchTarget 为null,说明上述的事件分发中并没有子View对事件进行捕获操作。
2.4.自定义控件演示touch事件分发流程
- xml文件设置
<?xml version="1.0" encoding="utf-8"?>
<com.timmy.demopractice.touch.DownInterceptGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context=".touch.TouchDemoActivity">
<com.timmy.demopractice.touch.CaptureTouchView
android:layout_width="300dp"
android:layout_height="400dp"
android:background="@color/colorAccent" />
</com.timmy.demopractice.touch.DownInterceptGroup>
- 自定义ViewGroup和自定义View
/**
* 查看ViewGroup 事件拦截方法
*/
public class DownInterceptGroup extends FrameLayout {
private static final String TAG = "DownInterceptGroup";
//事件分发
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "dispatchTouchEvent :" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
//事件拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "onInterceptTouchEvent :" + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
}
/**
* 自定义View,对事件进行捕获
*/
public class CaptureTouchView extends View {
private static final String TAG = "CaptureTouchView";
//事件分发
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "dispatchTouchEvent :" + event.getAction());
boolean result = super.dispatchTouchEvent(event);
Log.e(TAG, "dispatchTouchEvent result is :" + result);
return result;
}
//事件捕获 -返回true,表示事件消耗
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent :" + event.getAction());
return true;
}
}
- 操作及日志输出
手指触摸CaptureTouchView并滑动一段距离后谈起,最终打印log如下:
com.timmy.demopractice E/DownInterceptGroup: dispatchTouchEvent :ACTION_DOWN
com.timmy.demopractice E/DownInterceptGroup: onInterceptTouchEvent :ACTION_DOWN
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_DOWN
com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_DOWN
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true
...
com.timmy.demopractice E/DownInterceptGroup: dispatchTouchEvent :ACTION_MOVE
com.timmy.demopractice E/DownInterceptGroup: onInterceptTouchEvent :ACTION_MOVE
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_MOVE
com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_MOVE
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true
...
com.timmy.demopractice E/DownInterceptGroup: dispatchTouchEvent :ACTION_UP
com.timmy.demopractice E/DownInterceptGroup: onInterceptTouchEvent :ACTION_UP
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_UP
com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_UP
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true
- 解析
- 手指触摸时,首先分发的是DOWN事件
- 并且先调用DownInterceptGroup 的事件分发方法dispatchTouchEvent,因为是DOWN事件,所以会调用DownInterceptGroup 的拦截方法onInterceptTouchEvent (默认不拦截);
- 又因为是DOWN事件,且DownInterceptGroup没有对事件拦截,则会遍历子View,调用dispatchTransformedTouchEvent 方法,并将事件传递给子View。在dispatchTransformedTouchEvent方法中会调用子view的事件分发方法(child.dispatchTouchEvent())
- 事件传递到CaptureTouchView的事件分发方法,会调用CaptureTouchView 的dispatchTouchEvent方法,在View的事件分发方法中,会调用CaptureTouchView的onTouchEvent方法,因为该方法返回true,表示子view-CaptureTouchView捕获消费了这个DOWN事件。
- 这种情况下,CaptureTouchView会被添加到父视图(DownInterceptGroup)中的mFirstTouchTarget 中。
- 因此后续的MOVE和UP事件都会经过DownInterceptGroup的拦截方法onInterceptTouchEvent 进行判断。
3.特殊点
3.1.DOWN事件
- 所有的touch事件都是从DOWN事件开始的,而且DOWN事件的处理结果会直接影响后续MOVE,UP事件的逻辑
- 在上诉步骤2中,只有DOWN事件会传递给子View进行捕获判断,一旦子View捕获成功,后续的MOVE和UP事件是通过遍历mFirstTouchTarget链表,查找之前接受ACTION_DOWN的子View,并将触摸事件分配给这些子View。
- 也就是说后续的MOVE,UP等事件的分发交给谁,取决于他们的起始DOWN是由谁捕获的。
3.2.mFirstTouchTarget的作用
mFirstTouchTarget的部分源码如下:
private static final class TouchTarget {
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child 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.
public TouchTarget next;
private TouchTarget() {
}
}
- 解析
- 从源码中可以看出 mFirstTouchTarget 是一个TouchTarget类型的链表结构。
- 而这个mFirstTouchTarget 的作用就是用来记录捕获了DOWN事件的View,具体赋值给TouchTarget的child变量。
- 为什么选择使用链表结构?
- 因为Android设备是支持多指操作的,每一个手指的DOWN事件都可以当作一个TouchTarget保存起来。
- 在步骤3中判断如果 mFirstTouchTarget不为null,则再次将事件分发给相应的TouchTarget。
4.容易被遗漏的CANCEL事件
在上面步骤3中,ViewGroup向子View分发事件的代码中,ViewGroup的dispatchTouchEvent方法中存在一段有趣的逻辑:
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
-
解析
-
代码进入到这个地方,说明之前已经有子View捕获了touch事件的DOWN事件,如果变量 intercepted 的值为true时,cancelChild的值也会为true。
boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
-
接着会调用dispatchTransformedTouchEvent方法,且参数cancelChild值为true,这种情况下,事件的主导权又重新回到父视图ViewGroup中
dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)
-
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
- 在dispatchTransformedTouchEvent方法中,参数cancel为true,切child不为null,最终这个事件会被包装为一个ACTION_CANCEL事件传递给child
4.1.触发上述逻辑的情况
- 在DOWN事件中,父视图ViewGroup的事件拦截方法onInterceptTouchEvent先返回false,代表不拦截down事件,down会传递给子View,子View对事件捕获(子View的dispatchTouchEvent方法返回ture),并且mFirstTouchTarget会进行赋值。
- 在接下来的MOVE事件分发过程中,因为mFirstTouchTarget不为null,会先调用父容器的事件拦截方法onInterceptTouchEvent,如果父容器对事件进行拦截返回true,则变量intercepted 会为true
- 此时就会出现上诉逻辑情况,子View就会收到 ACTION_CANCEL的touch事件。
4.2.经典例子
- 当在ScrollView中添加自定义View时,ScrollView默认在DOWN事件中并不会进行拦截,事件会被传递给ScrollView内的子控件。
- 当手指进行滑动并达到一定的距离后,ScrollView 的事件拦截方法onInterceptTouchEvent 就会返回true,并处罚ScrollView的滑动效果。
- 当ScrollView进行滚动的瞬间,内部的子View会接收到一个CANCEL事件,并丢失touch焦点。
如下代码:
<?xml version="1.0" encoding="utf-8"?>
<com.timmy.demopractice.touch.InterceptScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context=".touch.TouchDemoActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.timmy.demopractice.touch.CaptureTouchView
android:layout_width="300dp"
android:layout_height="400dp"
android:layout_margin="40dp"
android:background="@color/colorAccent" />
<com.timmy.demopractice.touch.CaptureTouchView
android:layout_width="300dp"
android:layout_height="400dp"
android:layout_margin="40dp"
android:background="@color/colorAccent" />
...
</LinearLayout>
</com.timmy.demopractice.touch.InterceptScrollView>
- 自定义InterceptScrollView 和 CaptureTouchView
/**
* 查看ViewGroup 事件拦截方法
*/
public class InterceptScrollView extends ScrollView {
private static final String TAG = InterceptScrollView.class.getSimpleName();
//事件分发
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "dispatchTouchEvent :" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
//事件拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean interceptor = super.onInterceptTouchEvent(ev);
Log.e(TAG, "onInterceptTouchEvent :" + ev.getAction() + " ,interceptor:" + interceptor);
return interceptor;
}
}
/**
* 自定义View,对事件进行捕获
*/
public class CaptureTouchView extends View {
private static final String TAG = CaptureTouchView.class.getSimpleName();
//事件分发
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e(TAG, "dispatchTouchEvent :" + event.getAction());
boolean result = super.dispatchTouchEvent(event);
Log.e(TAG, "dispatchTouchEvent result is :" + result);
return result;
}
//事件捕获 -返回true,表示事件消耗
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent :" + event.getAction());
return true;
}
}
-
解析
- InterceptScrollView继承自ScrollView,并监听事件的分发过程;CaptureTouchView的onTouchEvent返回ture,表示他会将接收到的touch事件进行捕获消费。
- 当手指点击屏幕时DOWN事件会被传递给 CaptureTouchView,手指滑动时InterceptScrollView会进行滑动
- 刚开始MOVE事件还是由CaptureTouchView 来消费处理,当滚动了一段距离后,InterceptScrollView在事件拦截方法会返回true,而CaptureTouchView 会接收到一个CANCEL事件,并不再接受后续的touch事件,具体log如下:
com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_DOWN com.timmy.demopractice E/InterceptScrollView: onInterceptTouchEvent :0 ,interceptor:false com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_DOWN com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_DOWN com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true ... com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_MOVE com.timmy.demopractice E/InterceptScrollView: onInterceptTouchEvent :2 ,interceptor:false com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_MOVE com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_MOVE com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true ... com.timmy.demopractice E/InterceptScrollView: onInterceptTouchEvent :2 ,interceptor:true com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_CANCEL 10124-10124/com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_CANCEL com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_MOVE com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_MOVE com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_MOVE com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_UP
总结:
touch事件分发机制主要在ViewGroup的dispatchTouchEvent方法中,主要分3部分:
- 判断事件是否需要拦截 -》 主要根据onInterceptTouchEvent 方法的返回值来决定是否拦截
- 在DOWN事件中将touch事件分发给子View,—》这一过程如果有子View捕获消费了touch事件,会对变量 mFirstTouchTarget 进行赋值
- 最后一步,DOWN,MOVE,UP事件会根据 mFirstTouchTarget是否为null,决定是自己处理touch事件,还是再次分发给子View
网友评论