Android事件分发机制
Activity对点击事件的分发
在点击事件产生了,首先最先接触到点击事件的就是Activity,Activity通过 dispatchTouchEvent 来进行事件的分发。源码如下:
<b>Activity # dispatchTouchEvent</b>
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
由上面的源码可以得知,从getWindow().superDispatchTouchEvent(ev) 中得知, Activity事件的分发是由内部的Window来完成的。如果他返回的是true,那个事件循环就此结束了。如果返回false ,那么意味着所有View 的onTouchEvent都返回了false,都不处理事件,那么最后 return onTouchEvent(ev); 就会执行,既Activity中onTouchEvent就会执行。
从上面可以知道,getWindow().superDispatchTouchEvent(ev)的执行就是用来把事件分发给子View的。
<b>Window # superDispatchTouchEvent</b>
public abstract boolean superDispatchTouchEvent(MotionEvent event);
但是这个方法是abstract方法,那么就有继承于Window的类实现了这个方法,从Activity的启动中,我们可以得知 PhoneWindow 就是这个类。
<b>PhoneWindow # superDispatchTouchEvent</b>
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
那么PhoneWindow直接把事件传递给了 DecorView
从PhoneWindow 的构造方法中可以看到
mDecor = (DecorView) preservedWindow.getDecorView();
Window # getDecorView
/**
* Retrieve the top-level window decor view (containing the standard
* window frame/decorations and the client's content inside of that), which
* can be added as a window to the window manager.
*
* <p><em>Note that calling this function for the first time "locks in"
* various window characteristics as described in
* {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}.</em></p>
*
* @return Returns the top-level window decor view.
*/
public abstract View getDecorView();
从Window中getDecorView的注解可以看到DecorView拿到的就是Activity所设置的View,而这个View就是我们在Activity中通过setContentView设置的View。那么目标事件就会传递给了子View。
从这里就知道Activity把事件传递给了顶级View。顶级View也称根View,通常这个View是ViewGroup。
顶级VIew对事件的分发过程
从上述中,事件已经传递给了DecorView,而DecorView是继承于FrameLayout,FrameLayout又继承于ViewGroup。处理事件的分发原理就可以从 ViewGroup 中找到。
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;
}
....
newTouchTarget = addTouchTarget(child, idBitsToAssign);
....
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
从上面可以知道,ViewGroup中 onInterceptTouchEvent 是否被调用,看的是事件的传递是否为MotionEvent.ACTION_DOWN 或者 mFirstTouchTarget != null 。 MotionEvent.ACTION_DOWN 这个很好判断。至于mFirstTouchTarget != null 要从后面的代码分析 。 从后面的代码中可以看到,当事件由ViewGroup的子元素处理成功时,在addTouchTarget方法中,mFirstTouchTarget就指向这个子元素。所以一旦当前ViewGroup拦截了这个事件,不会再调用onInterceptTouchEvent 方法 ,那么这一系列的事件将不再进行分发,全部交给ViewGroup处理。
当然除了上面 if 的两个条件,影响是否 调用 onInterceptTouchEvent 方法的就是 FLAG_DISALLOW_INTERCEPT 标志位。 这个标记位是通过requestDisallowInterceptTouchEvent 方法来设置的,一般用在子View中。 FLAG_DISALLOW_INTERCEPT 标志位 一旦设置后,那么除了ACTION_DOWN事件外,其他事件都被拦截掉了。Why?因为在ViewGroup进行事件分发的时候,如果是ACTION_DOWN事件,那么就会重置 FLAG_DISALLOW_INTERCEPT ,将导致子View中设置这个标志位无效。因此,我们可以利用这个来解决滑动冲突。
当ViewGroup不再拦截的时候,就会继续执行下面的代码:
ViewGroup # dispatchTouchEvent
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);
.....
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 # dispatchTransformedTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
再从上面代码可以清楚的看到,ViewGroup就会遍历所有的子View。拿到子View的时候,源码中做了几个判断(上述省略了),判断这个子View是否符合条件。如果不满足则直接选取下一个子View。 拿到符合条件的子View,那么将调用 dispatchTransformedTouchEvent 这个方法。 在dispatchTransformedTouchEvent方法中就调用了child.dispatchTouchEvent(event)。接下来就把事件分发到了子View身上了。
如果遍历了所有的子View事件都没有被合适的处理,可能有以下两种情况:
- ViewGroup没有子View
- 子View处理了点击事件,但是在 onTouchEvent 中返回了false。
在这两种情况下,那么ViewGroup就会处理自己的点击事件。源码如下:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
从上面的dispatchTransformedTouchEvent中的第三个参数原本为Child,而现在改为null ,那么 将调用 super.dispatchTouchEvent(event) 这个方法,交给自己的View处理。
VIew对事件的处理过程
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
.....
return result;
}
从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.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
上面源码注释写的很清楚了,当View处于不可用的状态下,点击事件照样会消耗点击事件。
继续接下来:
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();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
.....
}
.....
return true;
}
从上面if的条件可以看出来,只用View的CLICKABLE和LONG_CLICKABLE有一个为true,那么就会消耗这个事件,既onTouchEvent返回true。
从case MotionEvent.ACTION_UP : 和 注解 Only perform take click actions if we were in the pressed state
从上面也可以看到,当View接受到了Down 和 Up 事件的时候 ,会触发performClick方法。这个方法内部其实就是调用了onClick方法。既实现了View 的点击事件。
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;
}
可以得出以下的结论:
- View的enable 属性不影响onTouchEvent的默认返回值,哪怕是一个View是disable状态,只要它的clickable或者longClickable有一个为true,那么他的onTouchEvent就返回true。
- onClick会发生前提是View是可以点击的,并且他收到了down 和 up 事件。