系列文章:
Android的事件分发机制(上)|SquirrelNote
Android的事件分发机制(下)|SquirrelNote
前言
本篇将会从源码的角度进一步分析。
事件分发的源码解析
分为:
1.Activity对点击事件的分发过程
2.ViewGroup对点击事件的分发过程
3.View对点击事件的处理过程
一、Activity对点击事件的分发过程
点击事件使用MotionEvent来表示(这里对MotionEvent不了解的,可以先看上篇),当一个点击操作发生的时候,事件是最先传递给当前的Activity,由Activity的dispatchTouchEvent方法进行事件分发,具体的工作是由Activity内部的Window来完成。Window会将事件传递给当前界面的底层容器DectorView(即setContentView所设置的父容器),通过Activity.getWindow.getDecorView()可以获得。
这里先从Activity源码的dispatchTouchEvent开始分析:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* 调用处理触摸屏事件。您可以在它们被发送到窗口之前,重写它来拦截所有的触摸屏事件。
* 请务必调用此实现,以实现应正常处理的触摸屏事件。
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
/**
* 调用Activity内部的mWindow的superDispatchTouchEvent方法,mWindow其实是PhoneWindow的实例
* 如果getWindow().superDispatchTouchEvent(ev)返回false,即没有任何子View处理事件,
* 最终会执行Activity的onTouchEvent
*/
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从源码可以看出:首先会判断当前触摸事件的类型,如果是ACTION_DOWN事件,会触发onUserInteration方法。根据文档注释,当有任意一个按键、触屏或者轨迹球事件发生时,栈顶的Activity的onUserInteraction会被触发。如果我们需要知道用户是不是在和设置交互,可以在子类(MainActivity)重写这个方法,去获取通知(比如取消屏保这个场景)。
然后调用Activity内部的mWindow的superDispatchTouchEvent方法,mWindow其实是PhoneWindow的实例。如果getWindow().superDispatchTouchEvent(ev)返回false,即没有任何子View处理事件,最终会执行Activity的onTouchEvent:
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* 当触摸屏幕事件没有被它的任何视图处理时调用。这对于处理发生在窗口边界之外的触摸事件是最有用的,
* 因为没有视图可以接收它。
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
小结:
事件是从Activity的dispatchTouchEvent开始,通过DectorView向下传递,交由子View处理,如果事件没有被任何Activity的子View处理,将由Activity自己处理。
二、ViewGroup对点击事件的分发过程
ViewGroup对点击事件的分发过程,主要实现在ViewGroup的dispatchTouchEvent这个方法中,这个方法比较
长,这里进行分段说明。
- 判断事件是否需要被ViewGroup拦截
- 遍历所有子View,逐个分发事件
- 将事件交给ViewGroup自己或者目标子View处理
1.判断事件是否需要被ViewGroup拦截
下面描述的是当前View是否拦截点击事件这个逻辑,有注释:
// Check for interception.
final boolean intercepted;
/**
* 这里判断是否要拦截当前事件,判断类型actionMasked == MotionEvent.ACTION_DOWN
*|| mFirstTouchTarget != null。从后面的代码逻辑可以看出来,当事件由ViewGroup的子元素成功处
*理时,mFirstTouchTarget会被赋值并指向子元素,换种方式说,当ViewGroup不拦截事件并将事件交由
* 子元素处理时 mFirstTouchTarget != null。反过来,如果当前ViewGroup拦截事件时,
* mFirstTouchTarget != null不成立。那么当ACTION_MOVE和ACTION_UP事件到来时,由于
* (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)这个条件为false,
* 将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给
* 它处理。
*/
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;
}
当然,有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。如下:
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) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
/**
* 层层向上传递,告知所有父类不拦截事件
*/
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
源码分析:
首先会根据mGroupFlags判断是否可以执行onInterceptTouchEvent方法,它的值可以通过requestDisallowInterceptTouchEvent方法来设置。所以,我们在处理某些滑动冲突场景时,可以从子View中调用父View的requestDisallowInterceptTouchEvent方法,阻止父View拦截事件。
如果View没有设置FLAG_DISALLOW_INTERCEPT,就可以进入onInterceptTouchEvent方法,判断是否应该被自己拦截。
ViewGroup的onInterceptTouchEvent直接返回了false,即默认不拦截事件,ViewGroup的子类可以重写这个方法,内部判断拦截逻辑。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件,这一点从源码也可以看出来。
注意:
只有当事件类型是ACTION_DOWN或者mFirstTouchTarget不为空时,才会走是否需要拦截事件这一判断,如果事件是ACTION_DOWN的后续事件(如ACTION_MOVE、ACTION_UP等),且在传递ACTION_DOWN事件过程中没有找到目标子View时,事件将会直接被拦截,交给ViewGroup自己处理。
// Handle an initial down.
/**
* ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对
* FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用requestDisallowInterceptTouchEvent方法并
* 不能影响ViewGroup对ACTION_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();
}
从上面源码分析,可以得出结论:当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法。这证实了如下结论:
当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
FLAG_DISALLOW_INTERCEPT这个标志的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不ACTION_DOWN事件,这证实了如下结论:
事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
那么,这段分析对我们有什么价值呢?总结起来有如下两点:
- onInterceptTouchEvent不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法才能确保每次都会被调用,但是前提是事件能够传递到当前的ViewGroup;
- FLAG_DISALLOW_INTERCEPT标记位的作用给我们提供了一个思路,当面对滑动冲突的时候,我们是不是就可以考虑用这种方法去解决滑动冲突的问题?
2.遍历所有子View,逐个分发事件
当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理,如下:
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 preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(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;
}
执行遍历分发的条件:当前事件是ACTION_DOWN。如果是事件ACTION_DOWN的后续事件,如ACTION_UP事件,将不会进入遍历流程。
上面源码逻辑:首先是遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。
dispatchTransformedTouchEvent方法里:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
可以看到,dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法,这样事件就直接交由子元素去处理了,从而完成了一轮事件分发。
如果子元素的dispatchTouchEvent返回true,这时我们暂时不考虑事件在子元素内部是怎么分发的,就会把mFirstTouchTarget设置为child,即不为null,并将alreadyDispatchedToNewTouchTarget设置为true,然后跳出循环,事件不再继续传递给其他子View。如下:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
这几行代码完成了对mFirstTouchTarget的赋值并终止对子元素的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素(如果还有下一个子元素)。
可以理解为,这一步的主要作用是,在事件的开始,即传递ACTION_DOWN事件过程中,找到一个需要消费事件的子View,我们可以称之为目标子View,执行第一次事件传递,并把mFirstTouchTarget设置为这个目标子View。
3.将事件交给ViewGroup自己或者目标子View处理
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*
* 在列表的开头添加指定子的触摸目标。假设目标子尚未出现。
*/
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
可以看出,mFirstTouchTarget真正的赋值过程是在addTouchTarget内部完成的,从addTouchTarget方法的内部结构可以看出,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略。
如果mFirstTouchTarget仍然为空,说明没有任何一个子View消费事件,将同样会调用dispatchTransformedTouchEvent,但此时这个方法的View child参数为null,所以调用的其实是super.dispatchTouchEvent(event),即事件交给ViewGroup自己处理。ViewGroup是View的子View,所以事件将会使用View的dispatchTouchEvent(event)方法判断是否消费事件。
反之,如果mFirstTouchTarget不为null,说明上一次事件传递时,找到了需要处理事件的目标子View,此时,ACTION_DOWN的后续事件,如ACTION_UP等事件,都会传递至mFirstTouchTarget中保存的目标子View中。这里面还有一个小细节,如果在上一节遍历过程中已经把本次事件传递给子View,alreadyDispatchedToNewTouchTarget的值会被设置为true,代码会判断alreadyDispatchedToNewTouchTarget的值,避免做重复分发。
如果遍历所有的子元素后事件都没有被合适地处理,有两种情况:
- ViewGroup没有子元素
- 子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。
在这两种情况下,ViewGroup会自己处理点击事件。证实了如下结论:
某个View一旦开始处理事件,如果它不消耗Action_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。
小结:
dispatchTouchEvent方法首先判断事件是否需要被拦截,如果需要拦截会调用onInterceptTouchEvent,若该方法返回true,事件由ViewGroup自己处理,不在继续传递。
若事件未被拦截,将先遍历找出一个目标子View,后续事件也将交由目标子View处理。
若没有目标子View,事件由ViewGroup自己处理。此外,如果一个子View没有消费ACTION_DOWN类型的事件,那么事件将会被另一个子View或者ViewGroup自己消费,之后的事件都只会传递给目标子View(mFirstTouchTarget)或者ViewGroup自身。简单来说,就是如果一个View没有消费ACTION_DOWN事件,后续事件也不会传递进来。
三、View对点击事件的处理过程
注意:这里的View不包含ViewGroup。它的dispatchTouchEvent方法,如下:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result=false;
...
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
...
return result;
}
因为View(这里不包含ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。从源码可以看出:View对点击事件的处理过程,首先它会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可以发现onTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。
下面分析onTouchEvent的实现。当View处于不可用状态下点击事件的处理过程,如下所示(onTouchEvent方法里面):
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
/**
* 如果一个View处于DISABLED状态,但是CLICKABLE或者LONG_CLICKABLE的话,这个View仍然能消费事
*件
*/
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
可以看出,不可用状态下的View同样会消耗点击事件。
下面看一下onTouchEvent中对点击事件的具体处理,如下:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
}
break;
}
...
return true;
}
从上面代码看,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态,这证实了如下结论:
- View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(即clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,如:Button的clickable默认为true,而TextView的默认为false。
- View的enable属性不影响onTouchEvent的默认返回值。即使一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
- onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
然后就是当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法,如下:
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
* 如果定义了这个视图的OnClickListener。执行与单击有关的所有正常操作:报告可访问性事件、播放声音等。
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
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);
return result;
}
通过setClickable和setLongClickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性。另外,setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener则会自动将View的LONG_CLICKABLE设为true,这一点可以从源码中看出来,如下:
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
* 在单击此视图时注册一个回调。如果这个视图不是可点击的,它就会变成可点击的。
*
* @param l The callback that will run
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
OK,点击事件的分发机制的源码实现已经分析完了。
以上是根据我的一些理解,做的总结分享,旨在抛砖引玉,希望有更多的志同道合的朋友一起讨论学习,共同进步!