事件冲突的场景
父子控件都支持滑动的嵌套布局会导致事件冲突,比如ScrollView和ListView、RecyclerView相互嵌套等,此时控件本身的事件处理机制不能够满足我们的业务需求,所以就需要我们自己来处理事件分发的逻辑。
主要关注的方法
Activity dispatchTouchEvent onTouchEvent
ViewGroup dispatchTouchEvent onInterceptTouchEvent onTouchEvent
View dispatchTouchEvent onTouchEvent
首先我们先看事件分发的日志,通过日志分析事件分发流程。
1.所有方法都采用默认的super
TouchActivity: dispatchTouchEvent: -------eventaction down------TouchActivity
TouchLayout: dispatchTouchEvent: -----eventaction down--------TouchLayout
TouchLayout: onInterceptTouchEvent: ------eventaction down-------TouchLayout
TouchView: dispatchTouchEvent: -------eventaction down------TouchView
TouchView: onTouchEvent: -------eventaction down-----TouchView
TouchLayout: onTouchEvent: ------eventaction down-------TouchLayout
TouchActivity: onTouchEvent: -------eventaction down------TouchActivity
TouchActivity: dispatchTouchEvent: -------eventaction up------TouchActivity
TouchActivity: onTouchEvent: -------eventaction up------TouchActivity
可以看到事件分发是从Activity dispatchTouchEvent方法的down事件开始
然后调用Layout的dispatchTouchEvent 和onInterceptTouchEvent方法
最后调用View的dispatchTouchEvent。
onTouch事件则是从View的onTouchEvent->Layout的onTouchEvent 再到Activity的onTouchEvent
如果设置了OnTouchListener 则优先调用OnTouchListener的onTouch方法。
可以看到如果我们都不处理down事件,最后会交给Activity的onTouchEvent 方法处理,而后续事件会直接走Activity 的事件方法。
2.Layout onInterceptTouchEvent返回true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchLayout: onTouchEvent: ------eventaction down
TouchActivity: onTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchActivity: onTouchEvent: -------eventaction up
当
Layout的onInterceptTouchEvent方法返回为true时,Layout拦截了事件,可以看到在onInterceptTouchEvent方法调用之后直接调用了TouchLayout的onTouchEvent方法,没有向下分发。
3.layout onInterceptTouchEvent返回false
完整的分发流程
4.layout 的dispatchTouchEvent 为false
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchActivity: onTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchActivity: onTouchEvent: -------eventaction up
layout 的
dispatchTouchEvent为false,事件会回到Activity的onTouchEvent方法中处理。
5.layout 的dispatchTouchEvent 为 true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchLayout: dispatchTouchEvent: -----eventaction up
layout 的
dispatchTouchEvent为true,事件会消失,默认不处理。
6.layout 的onTouchEvent为 true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchView: dispatchTouchEvent: -------eventaction down
TouchView: onTouchEvent: -------eventaction down
TouchLayout: onTouchEvent: ------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchLayout: dispatchTouchEvent: -----eventaction up
TouchLayout: onTouchEvent: ------eventaction up
当
onTouchEvent消耗了事件,后续的同一事件序列会直接给当前View消耗,比如layout onTouchEvent返回true,则后续UP事件直接交给了layout处理,而不需要交给子view。
7.layout 的onTouchEvent为 false
完整的分发消费逻辑
8.View的dispatchTouchEvent 为false
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchView: dispatchTouchEvent: -------eventaction down
TouchLayout: onTouchEvent: ------eventaction down
TouchActivity: onTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchActivity: onTouchEvent: -------eventaction up
View的dispatchTouchEvent为false会直接跳过View的onTouchEvent方法,调用layout的onTouchEvent。
9.View的dispatchTouchEvent 为 true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchView: dispatchTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchLayout: dispatchTouchEvent: -----eventaction up
TouchLayout: onInterceptTouchEvent: ------eventaction up
TouchView: dispatchTouchEvent: -------eventaction up
View的dispatchTouchEvent为true,代表默认消耗了事件。
10.View的onTouchEvent为 true
TouchActivity: dispatchTouchEvent: -------eventaction down
TouchLayout: dispatchTouchEvent: -----eventaction down
TouchLayout: onInterceptTouchEvent: ------eventaction down
TouchView: dispatchTouchEvent: -------eventaction down
TouchView: onTouchEvent: -------eventaction down
TouchActivity: dispatchTouchEvent: -------eventaction up
TouchLayout: dispatchTouchEvent: -----eventaction up
TouchLayout: onInterceptTouchEvent: ------eventaction up
TouchView: dispatchTouchEvent: -------eventaction up
TouchView: onTouchEvent: -------eventaction up
onTouchEvent消耗了事件 所以事件走到onTouchEvent就已经消费了,所以不会再调用父布局的onTouchEvent。
11.View的onTouchEvent为 false
完整的事件分发机制
从源码中验证事件分发的流程
入口:Activity .dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
getWindow().superDispatchTouchEvent(ev)拦截,返回true,否则调用Activity .onTouchEvent。
getWindow()返回的是Window实例,而Window是抽象类型,其唯一实例为PhoneWindow。
The only existing implementation of this abstract class is
android.view.PhoneWindow, which you should instantiate when needing a
Window.
追溯到PhoneWindow.superDispatchTouchEvent(ev)
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor为DecorView实例,本质上是FrameLayout,所以其调用的super其实是ViewGroup里的方法。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
那么我们来分析下ViewGroup的dispatchTouchEvent方法。
// 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;
}
以上是dispatchTouchEvent的一个片段,在Down事件下和mFirstTouchTarget 不等于空的情况下会调用,初始化intercepted字段,那么mFirstTouchTarget是什么呢?后续方法中可以看到在dispatchTransformedTouchEvent返回true也就是说子view被消耗的的情况下,mFirstTouchTarget不为空并指向该子View。所以在子view消耗事件或者down事件情况下,会访问Viewgroup的onInterceptTouchEvent方法,如果不是的话则被Viewgroup拦截。也就是说,一旦View决定拦截事件,后续同一序列的事件都会由这个view处理。
大家也注意到了FLAG_DISALLOW_INTERCEPT字段,貌似可以影响事件的拦截,这个字段是通过requestDisallowInterceptTouchEvent方法设置的,一般在子view中调用。此方法一旦设置了true,那么ViewGroup将无法拦截除Down以外的事件,为什么说除Down以外的事件,那是因为在代码中每次down事件会初始化状态。代码如下:
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
综上所述requestDisallowInterceptTouchEvent虽然可以影响事件分发,但是却影响不了down事件。还有就是onInterceptTouchEvent方法并不是每次都能调用的,所以要想每次事件都能处理,就在dispatchTouchEvent方法中处理。
下面来看看ViewGroup不处理交给子View处理的逻辑:
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 there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !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();
}
上述代码中可以看到,当ViewGroup不拦截时会遍历子View,将事件传递给子View,可以看到dispatchTransformedTouchEvent就是传递给子View事件的方法。大体上可以看出如果子view的dispatchTouchEvent返回true,就是说消耗了事件那么会对mFirstTouchTarget进行赋值,并中断循环。如果返回为false,那么会继续将事件分发给下一个子元素。
mFirstTouchTarget真正赋值是在addTouchTarget方法中,可以看到mFirstTouchTarget其实就是单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为空,默认拦截同一序列的所有事件,这一点在最开始就已经分析了。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
当所有子元素都没有被合适的处理时,即viewGroup没有子元素,或者子元素处理了事件但是onTouch返回为false。这两种情况下ViewGroup自己处理事件。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
下面是dispatchTransformedTouchEvent方法的代码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// Perform any necessary transformations and dispatch.
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
我们可以看到子view为空的情况下调用super.dispatchTouchEvent,也就是View.dispatchTouchEvent。子View不为空的情况下调用子View的dispatchTouchEvent。从而完成分发的步骤。
view对事件的处理
下面我们看一下View.dispatchTouchEvent的代码
public boolean dispatchTouchEvent(MotionEvent event) {
....
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;
}
}
.....
}
onFilterTouchEventForSecurity此过滤触摸事件策略是判断屏幕是否被遮挡。如果是enable状态,并且通过鼠标输入处理滚动条拖动的话result返回true。如果TouchListener不为空,View是enable状态并且TouchListener.OnTouch返回为True的话,result为true。
最后如果result为false并且onTouchEvent(event)返回为true,result为true。
这边可以得出结论,TouchListener.OnTouch的优先级高于onTouchEvent(),并且如果TouchListener.OnTouch返回为true消耗了事件,onTouchEvent不会再触发。
而在onTouchEvent方法中,UP事件处理过程中会调用performClickInternal方法,此方法最终调用performClick方法:
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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;
}
上述代码是看是否有mOnClickListener,有的话调用OnClickListener的onClick方法。可以看到我们的onClick方法是在onTouch方法中,处于事件的最底层。自此事件分发的过程全部梳理完成。
经典案例
现如今控件功能越来越完善,以前的一些滑动异常经典案例在现在的控件上面竟然大多复现不了,真是有点小失望呢!
笔者选取ScrollView嵌套ScrollView的案例,业务需求为:
1.外层未滑动到底部,内层不允许滑动。
2.内层未滑动到顶部,外层不允许滑动。
依据业务逻辑可采用两种方式拦截事件,一种是外部拦截,另一种是内部拦截。
依据之前的源码分析有两个条件控制是否拦截事件,分别是onInterceptTouchEvent返回和FLAG_DISALLOW_INTERCEPT字段的赋值,而外部拦截法就是通过控制onInterceptTouchEvent返回来控制事件的分发。内部拦截则是子view控制FLAG_DISALLOW_INTERCEPT字段的值来控制事件的分发。
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;
}
外部拦截法的基本代码:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int y = (int) ev.getY();
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
if(canIntercept(y)) {// 根据冲突的不同情况自己判断
intercepted = true;
}
else {
intercepted = false;
}
break;
default:
break;
}
mLastY = y; // 用于判断是否拦截的条件
return intercepted;
}
private boolean canIntercept(int y) {
View view = findViewById(R.id.recycleView);
if (y - mLastY > 0){
return !view.canScrollVertically(-1);
} else {
return canScrollVertically(1);
}
}
基本流程如上,其中canIntercept方法的返回是
是否拦截的条件。
笔者发现一个奇怪的现象,像上述代码down事件未拦截的仅拦截的是move事件,那么会后续move事件走到了onTouchEvent方法,但是ScrollView自身的滚动事件失效了,导致整体不能滑动,这块不知道是什么原因?笔者处理方式是在onTouchEvent方法中自己处理滑动事件和fling事件来达到滚动的效果。不知道还有没有比较好的处理方式,希望有大神能够指点一下谢谢。
内部拦截法的基本代码:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (null != mParentRecycleView) {
mParentRecycleView.requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
if (isIntercept()){
if (null != mParentRecycleView ) {
mParentRecycleView.requestDisallowInterceptTouchEvent(false);
}
return false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (null != mParentRecycleView) {
mParentRecycleView.requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(event);
}
大体代码如下isIntercept是
父布局拦截的条件,可自行定义规则。内部拦截法需要父布局
onInterceptTouchEvent down事件返回为false也就是说父布局不拦截down事件才能生效,否则事件分发不到子布局的dispatchTouchEvent方法中。
相对于外部拦截的方式,笔者更喜欢内部拦截。因为我们的子控件有可能不仅仅只有一个有可能有多个,而如果在外层一次性处理那么多的冲突似乎有点不合适。这时候使用内部处理的话就优雅很多了,我们只需要在各自的控件中针对外层控件做下处理就行了,并不影响外层控件的代码。
总结
事件分发流程图流程图

梳理
1.同一事件序列指的是从手机接触屏幕的那一刻起,到手指离开屏幕那一刻结束。整体事件流程从down -> move ...move ->up。
2.同一事件序列一次只能被一个view消耗,因为一旦view拦截了事件,同一序列的后续事件都会直接给他处理。
3.某个view一旦决定拦截,那么这一事件序列都只能交给他处理。从源码上分析就是一旦拦截了那么mFirstTouchView就不会赋值了,所以不会走分发事件直接交给了super.dispatchTouchEvent方法从而交给了onTouchEvent处理。
4.如果view的onTouchEvent不消耗Down事件,那么同一序列的其他事件都不会交给他处理,从业逐层询问父布局。如果都不处理,最终交给Activity的onTouchEvent。
5.View没有onInterceptTouchEvent方法,一旦事件交给他,onTouchEvent就会被调用。
6.requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,down事件除外,因为down事件会重置状态。此方法是内部拦截方法核心。
7.View的onTouchEvent默认消耗事件,除非clickable和longClickable同时为false。View的longClickable默认为false,而clickable分情况,比如Button是true,TextView是false。同时enable不影响onTouchEvent的返回值。