OnLongClickListener的判断流程
onLongClick()的返回值
从View.setOnLongClickListener()说起,setOnLongClickListener()会为View设置一个长按的监听,在长按控件时就能收到事件的回调;
mFinishBtn.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return false;
}
});
可以看到回调方法有一个返回值,看下这个在源码的注释写的什么:
true if the callback consumed the long click, false otherwise.
翻译过来就是:如果这个回调消耗长按事件,返回true,否则返回false
这个返回值的意义在于,如果同时设置了onClickListener和onLongClickListener,返回true的时候,长按不会触发onClickListener的回调,返回false的时候两个回调都会触发;
接下来通过源码看看上面的结论是否正确
从setOnClickListener和setOnLongClickListener这两个方法开始,这两个方法设置的回调都会赋值给View内部的一个ListenerInfo对象,之后回调的方法也是从这个对象中拿;
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
//获取ListenerInfo对象
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
通过搜索发现,在View的performLongClickInternal方法中回调了onLongClickListener的onClick()方法
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this); //回调onLongClick方法
}
...
return handled;
}
在View 的performClick()方法中回调了onClick()方法:
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
现在想要搞清楚为什么onLongClick返回true,就不会回调onClick方法;
我们发现在onLongClick中的返回值也跟着performLongClickInternal()方法返回了,于是顺着performLongClickInternal()方法走一下:
performLongClick() 调用 performLongClickInternal(),
performLongClick(float x, float y) 调用 performLongClick(),
在CheckForLongPress类的run方法中,调用了performLongClick(float x, float y),并给boolean类型的变量mHasPerformedLongPress赋值:
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
private boolean mOriginalPressedState;
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
...
}
于是我们就找到了标识长按的标志,mHasPerformedLongPress,接下来再来查找下这个标志用到的地方:
果然,在View的onTouchEvent()方法中判断了这个变量:
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();
}
}
}
如果mHasPerformedLongPress为true的话将不会调用下面的performClick()方法,而正是在performClick()方法中回调了onClick()方法;
于是我们就验证了之前的判断,这个返回值是用来标识是否同时回调onClick()方法的;
判断是否是长按
搞清楚了onLongClick返回值的作用,我们不禁有了疑问,View是如何判断一次点击是不是长按的呢?
在上面的分析中我们发现,onLongClick的回调是在一个CheckForLongPress类的run方法里,这个CheckForLongPress类继承了Runnable接口,也就是说这个任务是有可能被post到任务队列里进行执行的;
查看View类的属性发现View持有了一个CheckForLongPress对象的引用mPendingCheckForLongPress,这个引用是在checkForLongClick()中被赋值的:
//View.java # checkForLongClick(int delayOffset, float x, float 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);
}
}
在创建完这个对象之后又调用了View自己的postDelay()方法,来将这个任务post到了消息队列中,时延是ViewConfiguration.getLongPressTimeout() - delayOffset,这样就完成了延迟一段时间之后,回调onLongClick()方法;
最后确认下这个方法的调用位置:
这个方法一共在View中有4个调用的地方
-
onKeyDown(int keyCode, KeyEvent event)
中 -
onTouchEvent()方法中的ACTION_DOWN判断分支下:
if (!clickable) { checkForLongClick(0, x, y); //调用 break; } ... 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 { setPressed(true, x, y); checkForLongClick(0, x, y); //调用 }
-
CheckForTap的run方法中
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); } }
第一个调用的地方暂时不管,onKeyDown是对按键消息的监听,主要看一下上面的第二条和第三条:
看第二条的第一个调用时发现,代码中clickable为false的时候调用了checkForLongClick()方法,这儿也先暂时不管,看下面;
如果View是在可滚动的容器中,就发送一条延时消息,其中的runnable为CheckForTap的一个对象,在这个对象的run方法中调用了checkForLongClick();
如果是不可滚动的,就直接调用了checkForLongClick();
在ACTION_DOWN事件的时候肯定是调用了checkForLongClick();的;
查看代码发现,在回调onLongClick()方法之前的一系列调用中都没有进行判断,也就是说只要这个callback没有被移除,在指定时间之后肯定要回调onLongClick()方法;
意味着要是触摸的时间不够长,肯定会存在着一个地方将这个callback移除,
再次查看onTouchEvent()中ACTION_UP 下的代码:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback(); //移除长按的callback
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
这个判断中的mHasPerformedLongPress看着有点眼熟,对,在上面分析onLongClick()的返回值的作用的时候出现过,这个mHasPerformedLongPress也是判断点击时长是否足够的关键参数;
再看下checkForLongClick()方法:
//View.java # checkForLongClick(int delayOffset, float x, float y)
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE ||
(mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false; // mark
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
...
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
直接将mHasPerformedLongPress设置为了false,而在上面的分析中CheckForLongPress的run方法中将mHasPerformedLongPress设置为了true;
也就是说mHasPerformedLongPress只有在长按的回调任务被执行,且我们在onLongClick()方法返回true的时候才被赋值为true,如果点击的时间没达到长按的时间,那么在ACTION_UP中判断这个值的时候就肯定为false,就会调用removeLongPressCallback();来将长按的Callback移除,也就不会回调到onLongClick()方法;
总结
在View收到ACTION_DOWN事件的时候,会将mHasPerformedLongPress赋值为false,并将执行长按回调的任务postDelay()到任务队列,delay的时间就是长按的阈值;
而执行长按回调这个任务会在执行回调时根据onLongClick的返回值将mHasPerformedLongPress设置为true;
在View收到ACTION_UP事件时,会根据mHasPerformedLongPress来判断是否执行onClick();如果从ACTION_DOWN开始已经过了长按的时间,长按任务已经执行了,并且mHasPerformedLongPress已经设置为了true,就不会执行onClick;
如果点击很短的时间就松开,View接到ACTION_UP的时候长按任务还没被执行,mHasPerformedLongPress就不可能被设置为true,将会执行onClick,并且在onClick之前,将长按任务从任务队列中移除;
到这里LongClickListener的分析也就结束了,View中利用了发送延时消息来判断时间,节省了定时器的代码量;