说起Android事件分发,网上大大小小的文章不胜枚举,最近项目中遇到了些事件冲突的问题,发现自己对Android事件分发机制掌握的还不够好,于是最近整体学习了一波,虽然不是多么高端的技术,但作为Android知识图谱里的重要一环,必须得牢固掌握,下面这张图是前几天学习过程中记录整理的:
Android事件分发简单的来说就是当手指触摸屏幕之后,产生的一系列ACTON_DOWN,ACTON_MOVE(多个),ACTON_UP等事件的去向,一般我们所分析的是Action_down事件的分发过程,因为move和up事件与其基本相同,对于App开发来说,最直观的页面就是Activity,事件分发是从Activity开始一直延伸到最深处的view,形如Activity-ViewGroup-View的形式。
事件分发主要设计三个重要的方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent,就像它们的名字一样,dispatchTouchEvent是最先调用的方法,它负责事件的分发工作,onInterceptTouchEvent方法只存在于ViewGroup中(View中不存在onInterceptTouchEvent方法),表示是否拦截Event事件,onTouchEvent方法就是具体处理事件地方了。
1.Activity事件分发
下面我们就从Activity说起,当我们点击屏幕的时候,就会产生Event事件,此时会首先调用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方法的作用是当触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法,所以可以用在屏保应用上。
紧接着调用了getWindow().superDispatchTouchEvent(ev),即调用了Window的superDispatchTouchEvent,Window是抽象类,其子类为PhoneWindow,找到此方法源码如下:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
调用的是mDecor的superDispatchTouchEvent方法,mDecor是DecorView类型,继续查看其源码:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView调用的是其父类,它的父类是Framelayout,Framelayout并没有重写dispatchTouchEvent方法,所以最终调用的自然是其父类ViewGroup的dispatchTouchEvent.
在分析ViewGroup事件分发之前,先给出Activty的事件分发结论:
Activty事件分发很简单,如果重写了dispatchTouchEvent方法,无论是返回值是false还是true,此时事件分发结束,即不再向下传递,只有返回super.dispatchTouchEvent()时,才会正常往下传递;
2.ViewGroup事件分发
Activity如果没有消费事件,就会传递给ViewGroup来处理,此时首先会调用ViewGroup的dispatchTouchEvent()方法,精简源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
---> 1
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
//会清空mFirstTouchTarget,重置FLAG_DISALLOW_INTERCEPT
resetTouchState();
}
//intercepted表示是否拦截事件
final boolean intercepted;
//如果是ACTION_DOWN事件或者mFirstTouchTarget!=null,进入条件,当一条事件序列来临时,mFirstTouchTarget默认为null
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//判断disallowIntercept的值,此值是子view通过调用getParent().requestDisallowInterceptTouchEvent(flag)
//来控制的,即主动要求父控件是否拦截,true表示不要拦截,false表示拦截
//注意:requestDisallowInterceptTouchEvent控制的是下次event事件,当前的事件(一般指Action_down)是无法控制的
if (!disallowIntercept) {
//不拦截则调用onInterceptTouchEvent方法,根据其返回值决定是否拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
---> 2
//循环遍历子view
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 (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//找到child view
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//根据dispatchTransformedTouchEvent返回值(返回值由该子View的dispatchTouchEvent决定)判断子view是否处理该事件
--->3
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
}
}
}
//没有找到子view
--->4
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;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
//事件cancel,up等重置一些变量
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
return handled;
}
一开始判断了onFilterTouchEventForSecurity()的返回值,关于onFilterTouchEventForSecurity的作用,点击此处查看,-->1处可以看到,当ACTION_DOWN事件来临时,会进行把一些标志位清空或者恢复初始状态,mFirstTouchTarget就是在resetTouchState()方法里进行清空的,那么mFirstTouchTarget是在哪里赋值的呢,往下寻找可以发现,当找到接收Event事件的子View时 ,mFirstTouchTarget被赋值。
紧接着一个if判断,条件是actionMasked == MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null,ACTION_DOWN事件一开始肯定为true,因为最开始产生的Event事件就是它,进入判断之后,有一个disallowIntercept变量,它的值由(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0来决定,那么它又是干嘛的呢,通过搜索我们发现它是在requestDisallowInterceptTouchEvent方法中进行赋值的;
requestDisallowInterceptTouchEvent方法的作用就是子View可以控制父View是否拦截事件,子view通过调用getParent().requestDisallowInterceptTouchEvent(true/false)方法来控制父view是否拦截事件,true是不拦截,false是拦截。
默认情况下是false,所以此时会调用onInterceptTouchEvent方法来进行拦截操作,true表示拦截事件,交由ViewGroup自身的onTouchEvent来处理该事件,注意,一旦onInterceptTouchEvent返回true之后,在此事件序列中,之后的所有事件不再调用onInterceptTouchEvent,而是直接交由自身onTouchEvent处理,为什么呢?看这一小段代码:
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
因为要想执行onInterceptTouchEvent方法,actionMasked == MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null必须满足其中之一,ACTION_DOWN肯定是false,因为后续事件不会再出现ACTION_DOWN事件,mFirstTouchTarget上面说了,是找到子view后才会赋值,此时还没开始找子view,父view就自己来处理事件了,所以mFirstTouchTarget此时为null,两个条件都不满足,不再继续调用onInterceptTouchEvent拦截后续事件,而是直接intercepted为true,交由自身onTouchEvent处理;
-->2处的代码表示开始循环遍历子View,寻找当前触摸的子view,找到之后,--->3处的代码通过判断dispatchTransformedTouchEvent方法的返回值来决定事件的流向,dispatchTransformedTouchEvent方法精简源码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
}
可以看到,无论找不找的到Child View,都会调用View的dispatchTouchEvent(event)方法,此时事件分发由ViewGroup传递到View,--->4处的代码表示,如果没有子View,调用dispatchTransformedTouchEvent方法时child会传递null,接着会调用 super.dispatchTouchEvent(event),走默认的事件分发逻辑,最终还是会传递到自身来处理,因为super.dispatchTouchEvent(event)默认是不处理事件的,致此,ViewGroup的事件分发分析完毕,下面就是View的事件分发了。
3.View事件分发
对于View来说,是没有拦截一说的,即没有onInterceptTouchEvent方法,当View接收到Event事件的时候,同样会先触发dispatchTouchEvent方法,dispatchTouchEvent方法精简版源码如下:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
//如果控件设置了mOnTouchListener,并且控件是ENABLED状态的,并且onTouch的返回值为true,则会拦截此事件,不再继续分发。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果控件没有设置mOnTouchListener,返回值就由自身的onTouchEvent决定
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
通过上面的代码可以看出,onTouch方法是先于onTouchEvent执行的,如果View设置了mOnTouchListener,并且控件是ENABLED状态的,并且onTouch的返回值为true,则会拦截此事件,不再继续分发。如果View没有设置mOnTouchListener,返回值就由自身的onTouchEvent决定,onTouchEvent返回true表示自己消费事件,否则不消费,继续传递。
我们再来看下onTouchEvent的关键源码:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//控件是否可以点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//如果控件的状态是DISABLED的,那么直接返回可点击的状态clickable
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
//控件可点击则进入判断
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
//触发onClick事件
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
......
//检测长按事件
setPressed(true, x, y);
checkForLongClick(0, x, y);
......
break;
case MotionEvent.ACTION_CANCEL:
//进行一些重置操作
break;
case MotionEvent.ACTION_MOVE:
if (!pointInView(x, y, mTouchSlop)) {
//移除长按等回调
removeTapCallback();
removeLongPressCallback();
}
break;
}
return true;
}
return false;
}
首先检测View是否是clickable,以及是否禁用等逻辑,如果控件可用可点击的状态下,进入switch判断,ACTION_DOWN主要检测长按事件的触发,主要逻辑在ACTION_UP中,ACTION_UP中执行了performClick()方法,如果设置了mOnClickListener监听,则会触发onClick事件;如果控件不可点击,则直接返回false,不拦截,事件继续传递,交由;
onLongClick是在什么时候调用的呢?
其实就是在ACTIOIN_DOWN开启长按检测,执行顺序是checkForLongClick() ->performLongClick(mX, mY)->performLongClick() ->performLongClickInternal,一旦达到长按的阈值(private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500ms),就会调用如下方法:
private boolean performLongClickInternal(float x, float y) {
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
//关键代码,此处触发了onLongClick回调
handled = li.mOnLongClickListener.onLongClick(View.this);
}
return handled;
}
所以View的事件分发顺序是:dispatchTouchEvent->onTouch->onTouchEvent->on(Long)Click。
4.为什么onInterceptTouchEvent返回true可以拦截事件?
首先当onInterceptTouchEvent返回true的时候,
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//看这里
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
即intercepted的值为ture,那么此时:if (!canceled && !intercepted) {}这个判断是进不去的,也就无法传递到子view了,因为这个判断里的逻辑就是循环遍历子view,进行事件分发。
继续往下看,因为并未进入if (!canceled && !intercepted) {}判断,所以此时mFirstTouchTarget为null,会执行dispatchTransformedTouchEvent方法,由于事件并未分发到子View,所以此时参数child为null:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
}
}
通过上面代码可以知道,当child为null的时候,会执行super.dispatchTouchEvent(event),也就是会调用自身父类的dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,而在View的dispatchTouchEvent方法中:
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
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;
}
}
可以看到,如果控件没有设置onTouch相关监听的话,就会去调用 onTouchEvent(event)方法,这就是onInterceptTouchEvent返回true分发到onTouchEvent的逻辑,如果设置了mOnTouchListener,并且返回了true,那么onTouchEvent方法是不会调用的。
5.滑动冲突的一般解决方式
1.通过重写父类ViewGroup的onInterceptTouchEvent方法,根据具体业务需求来决定是否要对事件进行拦截,注意:ACTION_DOWN事件不要返回true,否则子view是无法接收到事件的。
2.子View通过调用getParent().requestDisallowInterceptTouchEvent(true/false)来干预父ViewGroup的onInterceptTouchEvent的事件分发过程。
6.一些重要结论
1.当onInterceptTouchEvent返回ture时,若onTouchEvent返回true,后续事件将不再经过该ViewGroup的onInterceptTouchEvent方法,直接交由该ViewGroup的onTouchEvent方法处理;若onTouchEvent方法返回false,后续事件都将交由父ViewGroup处理,不再经过该ViewGroup的onInterceptTouchEvent方法和onTouchEvent方法。
2.当onInterceptTouchEvent返回false时,事件继续向子View分发;
3.对于子View,当onTouchEvent返回true,父ViewGroup派发过来的touch事件已被该View消费,后续事件不会再向上传递给父ViewGroup,后续的touch事件都将继续传递给子View。
4.对于子View,onTouchEvent返回false,表明该View并不消费父ViewGroup传递来的down事件,而是向上传递给父ViewGroup来处理;后续的move、up等事件将不再传递给该View,直接由父ViewGroup处理掉。
5.onTouch先于onTouchEvent调用,onClick事件是在onTouchEvent中ACTION_UP中触发的。