ViewGroup
的dispatchTouchEvent()
方法中,有时会调用super.dispatchTouchEvent()
,而ViewGroup
的父类就是View
。
从View
的dispatchTouchEvent()
方法说起
/**
* 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) {
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();
}
// 判断该View是否被其它View遮盖住。
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// 1.设置了OnTouchListener
// 2.ENABLED状态
// 3.onTouch()方法返回true
result = true;
}
if (!result && onTouchEvent(event)) {
// 执行onTouchEvent()
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;
}
上面的代码中提到了li.mOnTouchListener
,那么mOnTouchListener
是什么呢?
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
mOnTouchListener
其实就是我们在Activity
中设置的OnTouchListener
。
如果设置了OnTouchListener
,View
是ENABLED
状态并且onTouch()
方法返回true
,则View
的OnTouchEvent()
方法就不会被执行。
View
的onTouchEvent()
方法
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
// 关于TouchDelegate,文档中是这样说的
// The delegate to handle touch events that are physically in this view
// but should be handled by another view.
// 就是说如果两个View, View 2在View 1中,View 1比较大,如果我们想点击
// View1的时候,让View 2去响应点击事件,这时候就需要使用TouchDelegate来设置。
// 如果希望View的Touch范围扩大,可以尝试使用TouchDelegate。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {// 这个View可点击
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
// PFLAG_PREPRESSED 表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动。
// PFLAG_PRESSED 表示不是在一个可滚动的容器中,已经可以确定按下这一操作。
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// mPrivateFlags为PFLAG_PREPRESSED或PFLAG_PRESSED时执行下面的代码。
// 处理点击或长按事件。
// 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);
}
if (!mHasPerformedLongPress) {
// 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();
}
// PerformClick就是个Runnable,里面执行performClick()方法。
// post()未执行成功,在直接立即执行performClick()方法。
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
// `ACTION_DOWN`事件发生后,
// 如果在ViewConfiguration.getTapTimeout()时间内触发`ACTION_UP`事件,
// mPrivateFlag = PFLAG_PREPRESSED,即prepressed == true
if (prepressed) {
// 取消按下状态,UnsetPressedState也是个Runnable,里面执行setPressed(false)。
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
// 如果mPendingCheckForTap != null,做移除操作。
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
// 长按事件还未发生
mHasPerformedLongPress = false;
// performButtonActionOnTouchDown()处理鼠标右键菜单,有些View显示右键菜单就直接弹菜单。
// 一般设备用不到鼠标,所以返回false。
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.
if (isInScrollingContainer) {
// 因为用户可能是点击或者是滚动,所以我们不能立马判断,先给用户设置一个预点击的标识。
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
// 发送一个延时的操作,用于判断用户到底是点击还是滚动。
// 其实就是在tapTimeout中如果用户没有滚动,那就是点击了。
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
// 检查是否是长按,就是过一段时间后如果还在按住,那就是长按了。
// 长按的时间是ViewConfiguration.getLongPressTimeout(),也就是500毫秒
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
// 取消按下状态,移除点击检查,移除长按检查。
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// 检查是否移动到View外面了。
if (!pointInView(x, y, mTouchSlop)) {// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
ACTION_DOWN
事件的处理
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
发送一个延迟时间为ViewConfiguration.getTapTimeout()
的消息,到达延时时间后会执行CheckForTap()
里面的run()
方法。
ViewConfiguration.getTapTimeout()
为115ms。mPendingCheckForTap
是一个CheckForTap()
。
private final class CheckForTap implements Runnable {
public void run() {
// 取消mPrivateFlags的PREPRESSED,然后设置PRESSED标识
mPrivateFlags &= ~PREPRESSED;
mPrivateFlags |= PRESSED;
refreshDrawableState();// 刷新背景
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
// 如果设置了长点击,再发送一个长按检测
postCheckForLongClick(ViewConfiguration.getTapTimeout());
}
}
}
如果115ms后,没有触发ACTION_UP
事件,将会执行上面的run()
方法。
private void postCheckForLongClick(int delayOffset) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
// 然后发出一个检测长按的延迟任务,
// 延时为:ViewConfiguration.getLongPressTimeout() - delayOffset(500ms -115ms)
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
从用户ACTION_DOWN
触发开始算起,如果500ms内没有触发ACTION_UP
事件,则认为是长按事件。
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
// performLongClick()方法返回true,才会将mHasPerformedLongPress设为true,
// 即认为发生了长按点击事件。
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
}
ACTION_UP
事件的处理
在ACTION_UP
事件中,调用了performClick()
方法.。
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @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);// 执行onClick()方法。
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
如果设置了OnClickListener()
,则执行onClick()
方法。
同时也看看UnsetPressedState()
的实现,
private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
}
public void setPressed(boolean pressed) {
if (pressed) {
mPrivateFlags |= PRESSED;
} else {
mPrivateFlags &= ~PRESSED;
}
refreshDrawableState();
dispatchSetPressed(pressed);
}
把mPrivateFlags
中的PRESSED
取消,然后刷新背景,分发SetPressed()
。
onTouchEvent()
方法脉络整理
从源码角度将MotionEvent
的各种情况分析了一遍,需要从整体角度整理该方法
ACTION_DOWN
事件设置
mPrivateFlags
为PREPRESSED
,设置mHasPerformedLongPress = false
,然后发出一个延迟为115ms的CheckForTap
(实际为Runnable()
)。如果115ms内没有触发
ACTION_UP
,则将mPrivateFlags
置为PRESSED
,清除PREPRESSED
标识,同时发出一个延时为500ms-115ms的CheckForLongPress()
,检测长按。如果500ms内没有触发
ACTION_UP
,则会调用performLongClick()
方法。此时如果LongClickListener != null
,则会执行LongClickListener.onClick()
,同时如果LongClickListener.onClick()
返回值为true,则mHasPerformedLongPress = true
;否则mHasPerformedLongPress == false
。
ACTION_MOVE
事件115ms内移出
View
,removeTapCallback()
。115ms后移除
View
,则将mPrivateFlags
中的PRESSED
去除,同时removeLongPressCallback()
。
ACTION_UP
事件115ms内,此时
mPrivateFlags == PREPRESSED
,移除长按检测removeLongPressCallback()
,执行点击事件performClick()
,延迟执行UnsetPressedState()
。115ms - 500ms内,此时
mPrivateFlags == PRESSED
,移除长按检测removeLongPressCallback()
,执行点击事件performClick()
。如果是500ms以后,那么有两种情况:
设置了
onLongClickListener()
,且onLongClickListener.onClick()
返回true
,则点击事件OnClick()
方法无法触发。没有设置
onLongClickListener()
或者onLongClickListener.onClick()
返回false
,则点击事件OnClick()
方法依然可以触发。
执行
UnsetPressedState()
。如果
mPendingCheckForTap != null
,做移除操作。