目录
前言
View的事件分发(二)源码分析中,我们提到 OnLongClickListener 和 OnClickListener 的回调都是在 onTouchEvent 中执行的。
那么下面我们就来分析具体的执行流程:
源码分析
以下所有源码都是基于版本 27。为方便阅读,有所删减。
setOnClickListener 源码
//View 类
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {//是否已经设置了点击,没有就设置
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
-----------------------------------------------------------------------
public void setClickable(boolean clickable) {
//设置点击标识
setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
}
setOnLongClickListener 源码
//View 类
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {//是否已经设置了长按点击,没有就设置
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
-----------------------------------------------------------------------
public void setLongClickable(boolean longClickable) {
//设置长按标识
setFlags(longClickable ? LONG_CLICKABLE : 0, LONG_CLICKABLE);
}
首先来看 onTouchEvent 中对 DOWN 事件的处理:
//View 类
public boolean onTouchEvent(MotionEvent event) {
switch (action) {
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();
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;
}
}
在 DOWN 事件来临的时候,首先在 代码① 处将 mHasPerformedLongPress 赋值为 false,表示长按事件还未触发。然后在 代码② 处为 mPrivateFlags 添加 PFLAG_PREPRESSED 标识,又发送了一个实现了 Runnable 接口(CheckForTap)的延迟消息,延迟时长 ViewConfiguration.getTapTimeout() 固定返回 100。
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}
------------------------------------------------------------------
public void setPressed(boolean pressed) {
......
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
......
}
100ms 后,将 mPrivateFlags 取消 PFLAG_PREPRESSED 标识,然后添加了 PFLAG_PRESSED 标识。刷新按下状态的背景。然后检测长按事件。
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);
}
}
如果设置了长按事件,就发送一个实现了 Runnable 接口(CheckForLongPress)的延迟消息。延时时长为 ViewConfiguration.getLongPressTimeout() - delayOffset
,其中 ViewConfiguration.getLongPressTimeout() 固定返回 500ms,delayOffset 为 100ms,延迟时长为 400ms。
private final class CheckForLongPress implements Runnable {
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
}
---------------------------------------------------------------
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
---------------------------------------------------------------
public boolean performLongClick() {
return performLongClickInternal(mLongClickX, mLongClickY);
}
---------------------------------------------------------------
private boolean performLongClickInternal(float x, float y) {
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
//-------代码省略-------
return handled;
}
400ms后,执行了 CheckForLongPress 中的 run 方法。run 方法中,执行了 mOnLongClickListener 的 onLongClick 方法。并且 onLongClick 返回为 true 时( performLongClickInternal 返回为 true ----> performLongClick 返回 true ),将 mHasPerformedLongPress 设置为 true。
所以在按下的时候,如果超过100ms,就设置 View 为按下状态,500ms后如果设置了长按点击事件,就回调长按点击的监听。
继续看 onTouchEvent 中对 MOVE 的处理
public boolean onTouchEvent(MotionEvent event) {
switch (action) {
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
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;
}
}
通过 pointInView 来判断手指是否移出了这个 View 。如果移出了这个 View,就调用 removeTapCallback 和 removeLongPressCallback 方法。并且如果 mPrivateFlags 设置了 PFLAG_PRESSED 标识(按下时长超过 100ms)就执行 setPressed(false),取消按下状态,刷新背景。
private void removeTapCallback() {
if (mPendingCheckForTap != null) {
mPrivateFlags &= ~PFLAG_PREPRESSED;
removeCallbacks(mPendingCheckForTap);
}
}
---------------------------------------------------------------
private void removeLongPressCallback() {
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}
mPrivateFlags 移除 PFLAG_PREPRESSED 标识,移除按下检测(mPendingCheckForTap)和长按检测(mPendingCheckForLongPress)。
所以用户在按下后,手指移动,如果移除了 View 的范围,就取消 View 的按下状态,移除了 DOWN 事件中设置的 检测 和 长按。
查看 onTouchEvent 中对 UP 的处理
public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
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;
}
代码①--------
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
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) {
setPressed(true, x, y);
}
代码②--------
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) {
代码③--------
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;
}
}
首先在 代码① 判断 mPrivateFlags 是否包含了 PFLAG_PRESSED 或 PFLAG_PREPRESSED。如果包含,就执行到了 代码② ,判断 mHasPerformedLongPress 和 mIgnoreNextUpEvent 是否为 false。都为 false
就调用 removeLongPressCallback() 方法,移除长按检测。执行到 代码③,添加一个实现 Runnable 接口的消息(mPerformClick),如果添加失败,则直接执行 mPerformClick 的 performClick() 方法。最后执行 removeTapCallback 方法,将按下检测移除。(如果从按下到抬起时间间隔不到100ms,不会执行按下状态刷新背景,但是会执行 onClickListener.onClick 回调。)
通过前面的分析可知,mHasPerformedLongPress 为 true 的条件就是 mOnLongClickListener.onLongClick 返回 true。所以如果没有设置长按监听,或者设置了长按事件且 mOnLongClickListener.onLongClick 返回 false,或者按下时长不够 500ms(没有达到 mOnLongClickListener.onLongClick 触发条件),则继续往下执行。
private final class PerformClick implements Runnable {
@Override
public void run() {
performClick();
}
}
---------------------------------------------------------------
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
可以看到 performClick 方法中执行了 mOnClickListener.onClick。也就是我们平时熟知的控件点击回调。
最后,代码继续执行到 代码④,如果 prepressed 为 true(mPrivateFlags 包含了 PFLAG_PREPRESSED),则发送一个延迟消息,ViewConfiguration.getPressedStateDuration() 发回固定值 64,延迟时长为 64ms。
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
setPressed(false);
}
}
取消了按下状态,刷新背景。
所以,在 UP 事件的时候,如果长按监听的 onLongClick 返回 false(意味着 mHasPerformedLongPress 不为 true),就执行 mOnClickListener 的 onClick 回调。最后再取消 View 的按下状态。
总结:
-
DOWN 事件:
将 mHasPerformedLongPress 设置为 false。mPrivateFlags 添加 PFLAG_PREPRESSED 标识,延迟 100ms 发送按下检测(CheckForTap)
超过 100ms : 就设置 View 为按下状态。mPrivateFlags 取消 PFLAG_PREPRESSED 标识,添加 PFLAG_PRESSED 标识。延迟 400ms 发送长按检测(CheckForLongPress)
超过 500ms : 如果设置了长按点击事件,就回调长按点击的监听。如果 mOnLongClickListener.onLongClick 返回为 true,就将 mHasPerformedLongPress 设置为 true。
-
MOVE 事件:
用户的触摸点移出了 View 的范围后,就将 按下检测和 长按检测移除。如果已经设置了按下状态(已经过了 100ms) ,就取消按下状态,刷新背景。mPrivateFlags 取消 PFLAG_PREPRESSED 标识和 PFLAG_PRESSED 标识。
-
UP 事件:
按下时长不超过100ms:执行按下操作—> 移除长按检测 —>执行 mOnCLickListener.onClick —> 取消按下操作(延迟 64ms)—>移除按下检测
按下时长不超过500ms:移除长按检测 —> 执行 mOnCLickListener.onClick —> 取消按下操作—>移除按下检测
按下时长超过500ms:mOnLongClickListener.onLongClick 返回 true:移除长按检测 —> 取消按下操作—>移除按下检测
mOnLongClickListener.onLongClick 返回 false:移除长按检测 —> 执行 mOnCLickListener.onClick —> 取消按下操作—>移除按下检测
本篇文章用于记录学习过程中的理解和笔记,如有错误,请批评指正,万分感谢!