Android事件分发完全解析之事件从何而来
Activity的事件分发过程
关于事件是如何而来的,可以参考上面的链接,事件的产生是用户的操作触发了Linux的input子系统。
当一个点击事件产生的时候,事件最先从底层传递给当前的Activity,由Activity的dispatchTouchEvent来进行事件分发。其中具体的工作是由Window来完成的,而我们知道Window是一个抽象类,它的具体实现是PhoneWindow:
// Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
当Window的superDispatchTouchEvent
返回false的时候,标识当前事件没有人处理,所有的View的onTouchEvent都返回的false,这个时候就交给Activity的onTouchEvent
来处理。
而在PhoneWindow中则是将事件传递给了DecorView,而DecorView是什么呢?
DecorView是我们setContentView的父布局,所以事件会进一步传递给我们设置的布局中。
关于DecorView的详细分析:【Android源码】Activity如何加载布局。
ViewGroup的事件分发
一般情况下,我们设置的布局的最外层都是ViewGroup,那么由DecorView传递过来的事件,首先交给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;
}
首先ViewGroup会在事件为ACTION_DOWN
和mFirstTouchTarget != null
的情况下来判断是否需要拦截当前事件,其中ACTION_DOWN
表示当前事件为按下的事件,mFirstTouchTarget
表示成功处理事件的子View,当mFirstTouchTarget != null
的时候说明,事件已经被子元素处理了。
这样的话,如果mFirstTouchTarget != null
成立的时候,ACTION_MOVE
和ACTION_UP
都不会调用onInterceptTouchEvent
查询是否需要拦截事件,这样同一个事件序列都会交给同一个子View来处理。
当然还有一种特殊情况,FLAG_DISALLOW_INTERCEPT
标记,当子View通过requestDisallowInterceptTouchEvent
设置请求父类不要拦截的时候,ViewGroup将不能拦截ACTION_DOWN
以外的其他事件。因为在ACTION_DOWN
的时候,系统会重置FLAG_DISALLOW_INTERCEPT
标记:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
从上面的代码可以看到当ViewGroup需要拦截事件之后,后续的事件都会默认交给它来处理,并且不会再调用onInterceptTouchEvent
。
这个结论说明了一点,onInterceptTouchEvent
并不会每次的事件都会被调用,如果我们需要提前处理所有的事件,那么就需要在dispatchTouchEvent
中来处理。
接下来我们继续看下面的代码,当ViewGroup不拦截事件的时候,事件会继续下发给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);
}
首先遍历ViewGroup的所有子元素
-
判断子元素是否能接收到事件,判断标准是子元素是否在播放动画和事件的坐标是否在子元素的范围内。
private static boolean canViewReceivePointerEvents(@NonNull View child) { return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null; } protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) { final float[] point = getTempPoint(); point[0] = x; point[1] = y; transformPointToViewLocal(point, child); final boolean isInView = child.pointInView(point[0], point[1]); if (isInView && outLocalPoint != null) { outLocalPoint.set(point[0], point[1]); } return isInView; }
-
当有子元素满足条件的时候,事件就会交给子元素来处理。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { 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); } }
-
当子元素的
dispatchTouchEvent
返回true的时候,mFirstTouchTarget
会被赋值并跳出循环。newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break;
当子元素的
dispatchTouchEvent
返回false的时候,ViewGroup会继续遍历。-
当ViewGroup没有子元素或者子元素处理了点击事件但是dispatchTouchEvent返回false的情况下,ViewGroup会自己处理事件,这个时候child为null,则调用
super.dispatchTouchEvent(transformedEvent)
:// 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); }
View的事件分发处理
当ViewGroup不停的向下处理,找到了符合条件的子View之后,事件就交给了View来处理,因为View里面没有子元素,所以处理事件就比较简单了:
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;
}
}
}
- 判断有没有设置
OnTouchListener
,如果设置了OnTouchListener
,并且onTouch
的返回值为true的情况下,onTouchEvent
就不会被调用了。 - 当上述的情况不成立的情况下,会调用
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.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
OnTouchEvent
首先判断当前的View是否处于不可用状态,不可用状态下,事件照样会被处理掉。
接下来就是OnTouchEvent
处理事件的过程,其中DOWM和MOVE没有什么特殊的地方,特殊的是在UP的情况下:
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();
}
}
}
当View的CLICKABLE或者LONG_CLICKABLE有一个为true的时候,会消耗事件。并且此时会触发performClick
方法,此时View的OnClickListener
就会被调用:
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;
}
从上面的分析可以得出一个结论,OnTouchListener
的优先级大于OnTouchEvent
,并且OnClickListener
的优先级最低。当OnTouchListener
的onTouch
返回true的时候,后面的都不会被触发,这也就能解决有时写自定义View的事件分发时的一些奇怪BUG的解释。
到此为止,整个View的事件分发过程就完成了。