概述
本文主要分享Android常见的事件冲突处理,处理方式有两种:
- 外部拦截:父容器处理冲突
- 内部拦截:子控件处理冲突
在介绍这两种处理方法之前,我们必须先了解两件事情:
- 事件在控件中是如何传递的
- 事件冲突产生的根本原因
事件在控件中是如何传递的
先来看一张事件分发的大致流程图:
通过流程图可知,事件的分发是从Activity的dispatchTouchEvent开始传递的,然后调用PhoneWindow的superDispatchTouchEvent,再调用DecorView的superDispatchTouchEvent,再调用到ViewGroup的dispatchTouchEvent方法,ViewGroup要先走分发流程,再走处理流程,而View只能走处理流程。下面便从ViewGroup的dispatchTouchEvent方法分析事件的传递流程。
DOWN事件
事件的分发是从Down事件开始的,Down事件只有一个,ViewGroup的dispatchTouchEvent方法对Down事件的处理方式有以下两种:
- 拦截事件
- 不拦截事件
接下来结合源码分析这两种处理方式有什么区别。
拦截事件
跟踪ViewGroup中dispatchTouchEvent方法针对ACTION_DOWN处理的关键代码:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
上述中有一句关键代码:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
disallowIntercept表示是否不允许父控件拦截,由于在MotionEvent.ACTION_DOWN中调用了resetTouchState方法:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
所以在MotionEvent.ACTION_DOWN时disallowIntercept的值为fasle,此时会调用ViewGroup的onInterceptTouchEvent,因为拦截了Down事件,所以onInterceptTouchEvent返回true,此时事件停止向子控件分发,交给自身处理即mFirstTouchTarget==null,然后会调用到以下的关键代码:
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)
跟踪dispatchTransformedTouchEvent方法的关键代码:
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());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
由源码可知,由于child==null会调用到 super.dispatchTouchEvent方法,即调用到View的dispatchTouchEvent方法,关键代码如下:
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
由源码可知,View的dispatchTouchEvent中会根据mOnTouchListener.onTouch或onTouchEvent是否返回true,来判断是否消费该事件。到这里拦截事件的基本流程就结束了。
这里补充一个小知识点,由于mOnTouchListener.onTouch是优先与onTouchEvent,所以当mOnTouchListener.onTouch返回true时,以下代码不会执行:
if (!result && onTouchEvent(event)) {
result = true;
}
那如果此时控件同时设置了onClick事件便会失效,因为在onTouchEvent的ACTION_UP事件中调用了 performClick() 方法:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
不拦截事件
ViewGroup的onInterceptTouchEvent返回false(默认返回false)时,此时会将事件分发给子控件处理,如果子控件都不处理则自己处理该事件,关键代码如下:
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
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 idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
遍历子控件集合时,会根据子控件的dispatchTransformedTouchEvent方法判断是否有子控件处理了事件,若有子控件处理,会执行如关键代码:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
这里采用了链表来存储目标控件,此时mFirstTouchTarget不为空,那如果子控件都没有处理,是如何将事件再交给父控件处理呢?继续跟踪源码:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
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;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
当没有子控件都没有处理事件时,mFirstTouchTarget=null,此时会调用ViewGroup的dispatchTransformedTouchEvent方法自己处理该事件,如果有子控件处理,会执行以下判断:
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
因为有子控件处理,此时alreadyDispatchedToNewTouchTarget = true、mFirstTouchTarget=newTouchTarget、 target.next=null即满足上述条件,while循环只会执行一次,到这里不拦截事件的基本流程就结束了。
MOVE事件
Move事件的传递也是通过以下两种方式进行分析:
- 不拦截Move事件传递
- 拦截Move事件传递
Move事件正常传递
父控件不拦截事件时,Move事件的传递也是通过dispatchTouchEvent方法传递给目标控件的,关键代码如下:
boolean alreadyDispatchedToNewTouchTarget = false;
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
.....
}//此时是Move事件,不会执行这段代码
}
.....
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;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
这里需要注意由于此时alreadyDispatchedToNewTouchTarget=false,所以会走else分支,会执行以下关键代码:
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
将Move事件交给对应的目标控件(Down事件保存的Target),到这里正常的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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
通过上面Down事件分析可知,由于Down事件做了重置操作,所以disallowIntercept的值为false,即if分支的代码一定会执行,此时拦截子控件的Move事件,会执行以下关键代码:
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
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;
}
}
因为拦截了move事件,此时intercepted=true, cancelChild=true,此时会设置子控件的Action为MotionEvent.ACTION_CANCEL,取消子控件的事件,并且注意以下代码:
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;
}
由于此时cancelChild=true, mFirstTouchTarget被设置成null,本次Move事件就结束了,注意ViewGroup是在下一Move事件才能够接收到事件,因为下一次Move事件会重新走dispatchTouchEvent方法,关注以下代码:
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
由于此时是Move事件并mFirstTouchTarget=null,所以此时走else分支intercepted = true,Move事件会交给自身处理,关联代码:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
小结一下,当父控件拦截Move事件时,第一次会将子控件的事件类型设置为MotionEvent.ACTION_CANCEL并将mFirstTouchTarget赋值为null,此时第一次Move事件结束(由于子控件的dispatchTransformedTouchEvent返回true),第二次以后的Move事件才会传递到父控件。
UP与Cancel事件
一次完整的事件,首先有Down事件开始,中间有多个Move事件,最后由Up/Cancel事件结束,Up事件是正常结束,而Cancel事件是被父控件拦截后产生的
事件分发完整流程图
为了进一步理解上述的源码分析流程,下面提供一张完整的事件分发流程图:
896629-20171007002836974-997068426.png事件冲突处理
通过前面的铺垫,可以知道事件冲突只能在Move事件中处理,可以通过外部拦截和内部拦截处理事件冲突,这里以SwipeRefreshLayout嵌套ViewPager为例:
外部拦截
根据父控件的滑动逻辑在onInterceptTouchEvent方法中返回true/false,核心代码:
public class CustomSRL2 extends SwipeRefreshLayout {
//外部拦截成员变量
private float startX;
private float startY;
//ViewPager是否滚动
boolean mIsVpMove = false;
//触发移动事件的最小距离,如果小于这个距离就不触发移动控件,如Viewpager就是用这个距离来判断用户是否翻页
private int mTouchSlop;
public CustomSRL2(Context context) {
this(context, null);
}
public CustomSRL2(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
//外部拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
mIsVpMove = false;
break;
case MotionEvent.ACTION_MOVE:
//若此时ViewPager还在滑动,则返回false,不拦截
if (mIsVpMove) {
return false;
}
float x = ev.getX();
float y = ev.getY();
float deltaX = Math.abs(x - startX);
float deltaY = Math.abs(y - startY);
if (deltaX > mTouchSlop && deltaX > deltaY) {
mIsVpMove = true;
return false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsVpMove = false;
break;
}
return super.onInterceptTouchEvent(ev);
}
}
内部拦截
根据子控件的滑动逻辑调用父控的requestDisallowInterceptTouchEvent(true/false)方法通知父控件是否不拦截事件,核心代码:
public class CustomSRL2 extends SwipeRefreshLayout {
public CustomSRL2(Context context) {
super(context);
}
public CustomSRL2(Context context, AttributeSet attrs) {
super(context, attrs);
}
//以下代码为内部拦截代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//在ACTION_DOWN事件返回false,不拦截事件,将事件交给子控件处理
if(ev.getAction() == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(ev);
return false;
}
return true;//拦截事件
}
}
public class CustomVPInner extends ViewPager {
private float startX;
private float startY;
public CustomVPInner(Context context) {
super(context);
}
public CustomVPInner(Context context, AttributeSet attrs) {
super(context, attrs);
}
//内部拦截:使用ViewCompat.setNestedScrollingEnabled(this,true),参考以下代码
/**
* public void requestDisallowInterceptTouchEvent(boolean b) {
* // if this is a List < L or another view that doesn't support nested
* // scrolling, ignore this request so that the vertical scroll event
* // isn't stolen
* if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
* || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
* // Nope.
* } else {
* super.requestDisallowInterceptTouchEvent(b);
* }
* }
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
ViewCompat.setNestedScrollingEnabled(this,true);
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float x = ev.getX();
float y = ev.getY();
float deltaX = Math.abs(x - startX);
float deltaY = Math.abs(y - startY);
if (deltaX < deltaY) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
//打印ViewPager是否消费了该事件,如果没有,事件还是会交给SwipeRefreshLayout处理
boolean consume = super.dispatchTouchEvent(ev);
Log.e("fmt","consume=" + consume);
return super.dispatchTouchEvent(ev);
}
}
完整代码实现
百度链接
密码:1cq9
网友评论