事件分发总览
Activity -> Window -> DecorView (是一个ViewGroup) —> rootView (setContentView设置的)
源码分析
Activity
当一个点击操作发生时,事件首先传递给当前的Activity,由Activity进行事件分分发。
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();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
由上面的源码可知,Activity的分发具体是由 Window 来完成的。这里我们点击进去看到的却是抽象类 Window
Window
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
...
public abstract boolean superDispatchTouchEvent(MotionEvent event);
...
}
由Window 的注释可知,其仅有一个实现类 PhoneWindow,那么我们就直接在 PhoneWindow中看此方法的具体实现即可。
PhoneWindow#superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
那么这个 mDecor 是啥呢?
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
public class DecorView extends FrameLayout {
...
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
...
}
到了这里我们应该都熟悉了,这个 DecorView 是一个继承了 FrameLayout 的ViewGroup,也是我们在 Activity中直接 getDecorView 返回的View。
到了这里我们也看到了事件已经由 Window 分发到 View 了。由于这里DecorView是一个ViewGroup 这里我们先分析ViewGroup 的分发机制,姑且将 View 分为 ViewGroup 与 View (不包含ViewGroup的View)。
ViewGroup
// 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;
}
这里可以看出,ViewGroup 是否拦截当前事件 取决于 事件类型为 ACTION_DOWN 或者 mFirstTouchTarget !=null。mFirstTouchTarget 在后面的源码我们可以看到逻辑,下面会有解释,这里直接看结论就是: 只有在 ViewGroup的子View成功处理 ACTION_DWON 事件(onTouchEvent 返回 true),mFirstTouchTarget才会被赋值。
- ViewGroup 一旦拦截Down事件,那么后续的事件序列都会交由它处理,并且不会再次调用 onInterceptTouchEvent 来询问是否拦截
这里有个特殊情况,就是 FLAG_DISALLOW_INTERCEPT 标记位,这个标记位由requestDisallowInterceptTouchEvent来设置,一般是在子View中调用的。作用就是用来干预 ViewGroup的事件分发。一旦设置之后,ViewGroup将无法拦截除了DOWN事件之外的其他点击事件。因为当事件为ACTION_DWON时,ViewGroup会重置这个标记位。如下源码:
// 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();
}
FLAG_DISALLOW_INTERCEPT 这个标记位会在resetTouchState中被重置。因此,每当面对 DOWN 事件时,ViewGroup都会调用 onInterceptTouchEvent 来询问是否拦截当前DOWN事件。
- ViewGroup 中 onInterceptTouchEvent 不是每次事件都会被调用,只有 dispatchTouchEvent 才会被每次调用;
- FLAG_DISALLOW_INTERCEPT 这个标记位,给我们一个处理滑动冲突的契机。
继续分析源码。
当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;
}
}
首先遍历 ViewGroup 的所有子View,然后判断子View 是否处理点击事件。
子View是否能够处理点击事件,主要由两点来衡量:
- 子元素是否在播放动画
- 点击事件的坐标是都落在子元素的区域内
如果某个子元素满足这个两个条件,那么事件就会传递给它来处理。通过 dispatchTransformedTouchEvent 方法来调用 子元素的 dispatchTouchEvent 方法。而且通过上述分析,我们知道 这里传入的 child !=null。
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
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;
}
...
}
这里我们可以看到,当 child != null 时,直接调用子元素的 dispatchTouchEvent 方法。先不考虑此方法的内部逻辑,如果 dispatchTouchEvent 返回 true,那么 mFirstTouchEvent 就会被赋值同时跳出 for 循环,如下逻辑
newTouchTarget = addTouchTarget(child, idBitsToAssign);
...
break;
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果子元素 dispatchTouchEvent 返回 false,那么就是继续遍历,继续分发事件。
这里我们着重关注 mFirstTouchTarget ,因为 mFirstTouchTarget 是否被赋值,将直接影响 ViewGroup 的对事件的拦截策略。
继续上一段,假如在遍历结束后,没有子元素能够处理这个事件,那么就会由ViewGroup 自己来处理,即调用自身的 onTouchEvent 方法。这里暂且先下这么个结论。其实这里提到的遍历结束,事件仍然没有被处理掉,包含了两种情况:
- ViewGroup 没有子元素
- 子元素处理了点击事件,但是在dispatchTouchEvent 中返回了 false,而这个false 一般也是由于子元素的 onTouchEvent 返回了 false。
在这两种情况下,ViewGroup 中的 mFirstTouchTarget 都不会被赋值,那么在执行下面的逻辑时:
// 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);
}
mFirstTouchTarget == null 判断成立,因为第三个参数 child == null ,就会直接调用 ViewGroup 自身的onTouchEvent,如下:
if (child == null) {
handled = super.dispatchTouchEvent(event);
}
这里调用了父类也就是 View 的dispatchTouchEvent 方法,而View的dispatchTouchEvent 方法中调用 onTouchEvent 方法。
如此分析下来,在 ViewGroup 事件分发过程中的有两种情况 调用了 View 的事件逻辑:
- 遍历子元素时,存在能够接收点击事件的子元素时
- ViewGroup 自身处理点击事件
so,我们进入View 内部来看一下,事件分发的逻辑。
View
首先,我们直接就上 dispatchTouchEvent ,从上述分析中我们知道不管哪种情况,都是首先进入 View 的dispatchTouchEvent 的逻辑。
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
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;
}
}
...
return result;
}
这里可以看到,View 的处理逻辑要比 ViewGroup 的相对简单不少。因为 View 自身没有子元素,所以不需要处理向下传递事件的逻辑。因此,它只能自己处理事件。
这里,首先判断 onTouchListener,如果有设置,会先执行 onTouchListener 的逻辑,根据 onTouchListener 的返回再决定是否执行 onTouchEvent。所以:
- onTouchListener 的优先级要比 onTouchEvent 高
接着进入 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;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
// 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,不管它是不是处于 DISABLED的状态。
然后就是 如果View设置了 OnClickListener ,那么 performClick 就会调用它的的 onClick 方法。
其实,从上面的源码可以看出,如果设置了 TouchDelagate,会优先执行 TouchDelegate 的逻辑,然后根据其返回值,来决定是否执行下面的逻辑。










网友评论