背景:接着上一篇ViewGroup的源码分析,我们知道,父view接收到事件后,会寻找有没有要处理该事件的子view,如果没有就由父view自己处理事件,如果有那么就会把事件分发给子View。
那么就走到View的事件分发里去看看,相比起ViewGroup来说View的事件处理简单的多。我们详细看看View的事件分发处理,OnTouchListener、OnClickListener、以及View.onTouch()。
直接上代码:
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
/** 如果view 包含enable标志,并且事件是拖动滚动条,那么事件被标志为消费 */
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
/**
* 如果TouchListener不为空,view 为enable
* 那么先执行TouchListener的onTouch()方法,
*
* 如果TouchListener的onTouch()方法消费了事件,
* 那么view自己的onTouchEvent(event)方法就不会执行了
*
* */
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
/** 如果事件没有被消费,执行view自己的onTouchEvent(event)方法 */
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
这就是全部的代码,非常少,我们捡干货说:
if (onFilterTouchEventForSecurity(event)) {
主要的代码逻辑在这个条件判断里面,里面主要做了三个判断:
- view是否是enable,且目前事件作用于滚动条上。
- 检查是否设置了OnTouchListener监听。
- 事件是否被消费了,如果以上两步都没有消费掉这个事件,那么最后交给View自己的onTouchEvent(event)方法去处理。
注意:第三步和前面两步是互斥的,如果前两步消费了事件,那么第三步就不会执行。关键判断代码如下:
// 第三步执行判断
if (!result && onTouchEvent(event)) {
result = true;
}
到这里View的这个方法就没有了,是不是很简单。
因为OnTouchListener是应用层自己定义传到View的,没什么好说的,那么我们就看看View自己的onTouchEvent(event)方法:
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;
/** 如果view状态为DISABLED, 直接返回false */
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;
}
}
/**
* 如果clickable = true,则表示view将消费此事件
* */
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
/** 如果mPrivateFlags 包含 PFLAG_PREPRESSED 标识, 那么 prepressed = true */
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
/** 如果view不在滚动容器内,并且手指已经滑出view的区域,那么不执行一下逻辑 */
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
/** 如果已经处理了长按事件,那么mHasPerformedLongPress = true,所以就不会在执行点击事件 */
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.
/**
* 发送一个Runnable消息而不是直接调用performClick().
* 这个会让view的其他状态在点击行为开始之前得到更新,。
* */
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
/**
*
* 如果click消息放入消息队列失败,那么直接调用performClick()方法
*
* */
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
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;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
/** 在【Down】事件下,view是否在一个可以滚动的容器内部,如果在那么加上 PFLAG_PREPRESSED 标识 */
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
/**
* 如果在移动过程中,手指的坐标移出了View的区域,那么将清除掉所有的特征
* 比如长按,点击等
* */
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
往大的方向看,就是:如果clickable = true,那么这个方法就返回true,也就表示view消费这个事件。clickable 为true的条件也很简单 :CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE 只要拥有这三个标识中的任一一个那么就表示这个View是可点击的。也就是会消费掉事件。
在这个方法里面,我们主要关注一下两个事件的处理:
- 点击事件如何处理的。
- 长按事件如何处理的。
- 点击事件的处理:
setPressed(true, x, y);
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
在【Down】事件下给View 加上PFLAG_PRESSED 标识。
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
在【Up】事件下如果长按事件没有被处理,那么就执行 performClickInternal();方法,这个方法最终调用了,OnClickListener.onClick()方法。在进入这个条件时,会先把长按事件 从消息队列里面移除掉。保证不会触发长按逻辑。点击事件就结束了。
还有一个注意的地方,就是在【Move】事件下,如果手指的触摸点移出了View的区域,那么会将点击和长按全部从消息队列移除
/**
* 如果在移动过程中,手指的坐标移出了View的区域,那么将清除掉所有的特征
* 比如长按,点击等
* */
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
- 长按事件的处理:
checkForLongClick(0, x, y);
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
/**
* Defines the default duration in milliseconds before a press turns into
* a long press
*/
private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500;
同样的也是在【Down】事件的时候向消息队列里面发送了一个延迟执行的长按事件消息,时间大概是500ms。如果按下的时间超过了500ms,那么长按消息就会被执行,最终mHasPerformedLongPress = true; 这个字段被赋值 true,然后到【Up】事件
/** 如果已经处理了长按事件,那么mHasPerformedLongPress = true,所以就不会在执行点击事件 */
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
【Up】中这个条件就不满足,也就是说不会去执行点击相关的代码了。
结尾
综上:View事件分发在View里面只有简单3步,然后View的里面OnTouchListener.onTouchEvent() > View.onTouchEvent() > OnClickListener.onClick()