onTouch,onClick冲突详解
ONE Goal,ONE Passion!
概述:
ViewGroup和View的分发.
http://blog.youkuaiyun.com/fengltxx/article/details/49330343
介绍一个OnTouchEvent中事件流程
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
System.out.println("---ACTION_DOWN--");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("---ACTION_MOVE--");
break;
case MotionEvent.ACTION_UP:
upX = (int) event.getX();
System.out.println("---ACTION_UP--");
if (Math.abs(downX - upX) > 30) {
return true;
}
break;
}
return super.onTouchEvent(event);
}
咦…….为什么要返回一个super.onTouchEvent(event).
注意:系统想接收到此事件.
- 1.return super.onTouchEvent(event);
- 2.调用super.onTouchEvent(event)即:(将这句代码放在第onTouchEvent(MotionEvent event)方法);
开始我们的测试
在ACTION_DOWN中return true.
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
System.out.println("---ACTION_DOWN--");
return true;
当我们点击(down)view并且抬起(up)时.onclick就不能被回调.为什么呢?因为我们没有将down事件传递给系统.仅仅把up事件传递了. 为什么不传递down事件就不能执行onclick呢?going..
来看看super.onTouchEvent(event)到底做了什么?
public boolean onTouchEvent(MotionEvent event) {
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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
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) {
// 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);
}
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();
}
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();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = 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();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
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;
}
我们先找到onClick在哪个地方被回调.嘎嘎 …找到了在UP事件中: performClick();
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); // 是不是很熟悉.这里就是onClick的回调
return true;
}
return false;
}
哎呦!想要执行performClick();方法好像要经过2层判断.首先我们先看第一层判断.
第一层判断
找到第28行.
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed)
系统判断 mPrivateFlags & PFLAG_PRESSED 和 prepressed的值是否有一个为true.这两个值和什么有关呢?
看DOWN中的第90行:
mPrivateFlags |= PFLAG_PREPRESSED;
做完这个处理第28行判断就为true了.
噢.原来没有DOWN事件.根本没办法执行UP中performClick()中代码.所以我们的onClick事件不能被回调.
如果我们在自定义的View中DOWN中返回Super.onTouchEvent()就可以接收onClick事件了吗?.NO !
别忘了我们还有第二层判断:
找到第44行
if (!mHasPerformedLongPress) //判断是否已经执行长按
哎呦喂!这个mHasPerformedLongPress是什么鬼? 想一下:如果这个值为false那么不就可以突破第二层判断了嘛!答案是–YES. 可是会不会在什么时候系统将mHasPerformedLongPress设置为True呢? 所以我们一路Ctrl+F.(mHasPerformedLongPress = true;)下一步.终于找到了.
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
系统在CheckForLongPress代码块中将mHasPerformedLongPress = true.谁执行了CheckForLongPress呢?
继续找:
private void checkForLongClick(int delayOffset) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
bingo!checkForLongClick方法中执行了CheckForLongPress.又是谁执行了checkForLongClick呢?可以看到有很多地方都执行了这个方法.我们找到在CheckForTap方法中执行了checkForLongClick.
//检查是否是点击事件
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true);
//在点击事件中检查是否是长按事件
checkForLongClick(ViewConfiguration.getTapTimeout());
}
}
继续找CheckForTap.看看到底在哪个地方执行.哇哦!
找到第91-94.在DOWN中执行有这段代码:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
// 延迟 180ms后执行mPendingCheckForTap代码块.目的就是检查事件是点击DOWN还是长按事件
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
最后我们看到.如果代码执行到了CheckForLongPress方法中在将 mHasPerformedLongPress = true;之前还有一个判断if (performLongClick()).只有当performLongClick() = true时.才会将mHasPerformedLongPress赋值为true.
等一下: performLongClick()又是什么鬼从字面意思可以看出.就是执行长点击.我们可以重写这个方法.通过return true还是false来控制mHasPerformedLongPress是否可以被赋值为true.
我去…..终于搞完了.
总结下来就是:
- 1.当手指点下,在onTouchEvent中如果调用了super.onTouchEvent将事件传递给了系统.系统会先执行Action_DOWN.
- 2.在DOWN中,首先将mHasPerformedLongPress = false.然后调用postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());去检查是否是敲击事件.
- 3.在mPendingCheckForTap代码块中会去延时调用checkForLongClick().去检查是否是长点击事件.
- 4.在checkForLongClick()中再去延时调用CheckForLongPress(),去检查是否是长按事件
- 5.在CheckForLongPress()中会根据performLongClick()的返回值来决定是否将mHasPerformedLongPress = true.
每次检查都是延时操作.如果在还没有检查完,就抬起的话.由于在down是已经将mHasPerformedLongPress= false.所以onclick事件是可以执行回调的.
可以看出:
- 1.长按事件是在DOWN事件中判断的.
- 2.点击事件也要经过DOWN事件,而且和长按事件返回值有关.如果返回true(可以理解为将事件消费了)—执行长按就不能执行onclick.返回false同样可以执行onClick
注: ViewGroup和View的分发机制详解.
http://blog.youkuaiyun.com/fengltxx/article/details/49330343