上一篇讲解了View的事件分发机制,查看点击链接View事件分发机制查看。本文基于Android9.0的源码进行分析ViewGroup的事件分发机制和事件冲突解决方案,源码点击https://github.com/Oaman/Forward查看。
本文分如下几个步骤分析
ViewGroup的dispatchTouchEvent是由Activity的dispatchTouchEvent,到PhoneWindow的superDispatchTouchEvent,再到DecorView的superDispatchTouchEvent,最后才到ViewGroup的dispatchTouchEvent的, 流程图如下:
ViewGroup的dispatchTouchEvent是用来进行事件分发的,我们将其分为三个部分进行分析,后面分析中主要是对这三个大的步骤进行分析(后面分析中提到的步骤1|2|3指的就是这里)。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 此处会清除requestDisallowInterceptTouchEvent设置的FLAG
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 1 判断是否拦截事件 intercepted代表是否拦截,
// intercepted的值是根据disallowIntercept和onInterceptTouchEvent(ev)共同决定的
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;
}
// 2 如果不拦截的话就走这里分发的逻辑,不过这里只能在DOWN事件的时候分发
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN || ...) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
// 3 根据上面事件处理情况,继续下一步的事件分发和处理,最终返回结果handled
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
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;
}
}
}
}
return handled;
}
三个步骤分别是:
根据父容器是否拦截,这里分为两种情况来分析源码
这一种情况比较简单,因为onInterceptTouchEvent返回true, 所以在上面的三步分析中,第一步intercepted为true, 那么第二步不会进入,直接走到第三步,源码如下:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
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) {
// 1
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());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
上面进入到dispatchTransformedTouchEvent方法中,因为参数child为null, 所以直接走到了注释1处的super.dispatchTouchEvent中,就是进入到了View的事件分发处理中,Down事件结束。点击链接查看View事件分发机制。
如果父容器不拦截的话,那么intercepted就为false, 就会走第二步的事件Down的分发逻辑,源码如下:
// 1
if (!canceled && !intercepted) {
// 2
if (actionMasked == MotionEvent.ACTION_DOWN || ...) {
final View[] children = mChildren;
//3
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 4
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 5
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 6
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
上面因为注释1处的intercepted为false,所以进入if条件。在注释2处,当前是Down事件,所以进入注释2处的if判断,在注释3处for循环获取子View,在注释4处判断获取的View是否满足事件分发的条件,比如点击的坐标是否位于View内部等;在注释5处真正的用来判断是否view能处理这个Down事件,如果所有View都不能处理的话,还是走到类似于父类拦截的那种情况,最终是由父容器处理此次Down事件。
如果有子View处理Down事件的话,注释5这里返回true,就会走注释6处的addTouchTarget方法,并且将alreadyDispatchedToNewTouchTarget赋值true, 然后break跳出for循环, addTouchTarget源码如下:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
上面主要实现的目的是将mFirstTouchTarget赋值不为null, 它是一个TouchTarget类型,里面包含了处理此次Down事件的View,并且内部维护了一个next指针指向下一个TouchTarget。这里mFirstTouchTarget和alreadyDispatchedToNewTouchTarget的值在后面会用到。接下来我们分析三大步骤中的第三步,源码如下:
// 1
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
// 2
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 3
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
因为mFirstTouchTarget这时候已经不为null了,所以这里不会进入到注释1处,在注释2处将mFirstTouchTarget赋值给target,在注释3处很明显两个值都满足条件,所以进入注释3的if判断,然后handled为true, 返回true,Down事件结束。
我们接着上面Down事件的情况(父容器不拦截)分析,Move事件在第一个步骤中,intercepted为false,在步骤2中是对Down事件进行分发的,Move事件是进入不到步骤2中的,接着进入到步骤3中的源码,源码如下:
// 1
boolean alreadyDispatchedToNewTouchTarget = false;
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
// 2
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 3
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 3
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
Move事件在上面注释1处会将alreadyDispatchedToNewTouchTarget赋值为false,在注释2处将mFirstTouchTarget赋值给target, 在注释3处执行事件的分发,如何找到处理事件的View的呢?我们看到就是通过target.child找到的,也就是我们在Down事件中保存的view,到此正常的Move事件分发分析完毕。
Move事件因为不做事件分发处理,所以直接找到在Down事件中保存的view来处理Move事件即可。
(demo源码见https://github.com/Oaman/Forward)
首先要明确一点,就是滑动事件冲突的解决都是在Move事件中解决的。我们首先通过源码来分析事件冲突应该怎么做?
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 此处会清除requestDisallowInterceptTouchEvent设置的FLAG
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// 0
resetTouchState();
}
// 1 判断是否拦截事件 intercepted代表是否拦截,
// intercepted的值是根据disallowIntercept和onInterceptTouchEvent(ev)共同决定的
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 2
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
// 3
intercepted = false;
}
} else {
intercepted = true;
}
// 如果不拦截的话就走这里分发的逻辑,不过这里只能在DOWN事件的时候分发
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN || ...) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
// 根据上面事件处理情况,继续下一步的事件分发和处理,最终返回结果handled
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
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;
}
}
}
}
return handled;
}
我们前面说过,第一步中的intercepted的最终取值也受disallowIntercept的影响,在Down事件分发的时候,从代码上看如果disallowIntercept为true的话,那么直接就会走到注释3的地方,将intercepted赋值为false。而disallowIntercept取值是由ViewGroup.requestDisallowInterceptTouchEvent决定的,源码如下:
/**
* @param disallowIntercept if true means son will get this event.
*/
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
//0x10000 = 524288
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
//~ 1000 0000 0000 0000 0000
//~ 0 0111 1111 1111 1111 1111
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
从上面方法注释处知道,如果这里将disallowIntercept赋值为true的话,理论上是可以禁止父类拦截的,则会由子类得到事件。但是实际上真的是这样吗?经过测试就会发现,Down事件如果父容器不主动分发给子View的话,子View是拿不到的,源码逻辑见resetTouchState(),这个方法在Down事件的时候会触发,会重置requestDisallowInterceptTouchEvent设置的FLAG值。所以如果想要在Down事件的时候将Down事件分发给子View的话,需要父容器协助,下面分析滑动冲突的解决思路。
一般滑动事件冲突解决方案有两种,内部拦截法和外部拦截法,
我们这里首先看内部拦截法,假如ViewPage嵌套了ListView(子ListView上下滑动,父ViewPager左右滑动),那么这种冲突的处理通过内部拦截法的代码如下:
public class MyListView extends ListView {
...
private int mLastX, mLastY;
/**
* 处理之间冲突 - 内部拦截法
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 1
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 2
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}
我们自定义了ListView, 重写了它的dispatchTouchEvent,在Down事件的时候,注释1处调用了getParent().requestDisallowInterceptTouchEvent(true),在注释2的Move事件的时候调用了getParent().requestDisallowInterceptTouchEvent(false), 上面分析过了,需要父容器协助来帮助子View得到Down事件,所以父容器的代码如下:
public class MyViewPager extends ViewPager {
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
super.onInterceptTouchEvent(ev);
boolean intercept = ev.getAction() != MotionEvent.ACTION_DOWN;
return intercept;
}
}
就是说在Down事件的时候,父容器不拦截事件,并且子ListView设置了getParent().requestDisallowInterceptTouchEvent(true),那么Down事件就由子View处理了,当左右滑动的时候,子ListView设置getParent().requestDisallowInterceptTouchEvent(false)将事件重新交给父ViewPager处理,就实现了滑动事件冲突的解决。
public class MyViewPager extends ViewPager {
...
/**
* 处理之间冲突 - 外部拦截法
*/
private int mLastX, mLastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return true;
}
}
mLastX = x;
mLastY = y;
return super.onInterceptTouchEvent(ev);
}
}
外部拦截法的实现思想和内部拦截法一样的,下面我们从源码层分析子ListView是如何将Move事件还给父ViewPager的。
因为现在分析的是Move事件,假如子ListView左右滑动就会将Move事件交给父ViewPager,下面从源码层分析如何实现的。
因为左右滑动的时候执行了requestDisallowInterceptTouchEvent(false),所以此时intercepted=true,接着就走到了下面的代码中:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 1
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 2
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
// 3
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
}
}
因为mFirstTouchTarget不等与null, 所以直接走到了注释1处,因为intercepted为true,所以cancelChild就为true,就会执行注释2处的dispatchTransformedTouchEvent方法,在注释3处将mFirstTouchTarget置为null方法源码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// 1
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) {
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());
}
// 2
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
上面注释1处,因为cancel为true,所以会执行child, 也就是ListView的CANCEL事件,所以CANCEL事件什么时候执行?CANCEL事件在事件被上层拦截的时候触发。
接着执行到注释2处的代码,此Move事件结束,此次的Move事件其实对于ViewPager没有任何影响,不过因为Move事件有多个,我们继续看下一个Move事件。
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;
}
这里因为是Move事件并且mFirstTouchTarget=null, 所以intercepted = true,所以直接走到了第三步骤的if,源码如下:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
到这里就将Move又交给了父容器,实现了滑动冲突的处理,另外在此实战中,左右滑动时候上下滑动是不可能的,因为父类处理了事件,是不会再给子类处理的;但是上下滑动时候可以左右滑动,因为事件是可以再次给父类的。就是父容器可以抢子view的事件。