MotionEvent.ACTION_UP 可以当做点击事件的触发条件吗?
答案是不行。
我许多人为了给自定义View添加点击事件也是想破了脑袋。
如何让自定义View既允许外部设置OnClickListener,又能保证在自定义View被点击时也执行一些其他的代码呢?
首先View里根本没有onClick之类的方法可以方便地重写,于是我很多人就很自然地把视线转移到了onTouchEvent上来:
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { clicked(); } return super.onTouchEvent(event); }
把手拿起来的时候就是点击结束的时候嘛,我真是太机智了。
这段代码看起来一点问题都没有对不对?
对个大头鬼。
按下按钮之后你突然发现不能反悔了。就算把手指从按钮上移开,代码还是会跑到 ACTION_UP 下面去。
怎么回事,这跟说好的不一样啊?不是按下去之后移走就取消操作的么?我写了两个小时文档点叉之后不小心点到不保存的时候就是这招救了我的命的啊?
不说别的,如果点击事件真这么容易处理,Google程序员把
1 public boolean onTouchEvent(MotionEvent event) { 2 final float x = event.getX(); 3 final float y = event.getY(); 4 final int viewFlags = mViewFlags; 5 final int action = event.getAction(); 6 7 if ((viewFlags & ENABLED_MASK) == DISABLED) { 8 if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { 9 setPressed(false); 10 } 11 // A disabled view that is clickable still consumes the touch 12 // events, it just doesn't respond to them. 13 return (((viewFlags & CLICKABLE) == CLICKABLE 14 || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) 15 || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); 16 } 17 18 if (mTouchDelegate != null) { 19 if (mTouchDelegate.onTouchEvent(event)) { 20 return true; 21 } 22 } 23 24 if (((viewFlags & CLICKABLE) == CLICKABLE || 25 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || 26 (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { 27 switch (action) { 28 case MotionEvent.ACTION_UP: 29 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; 30 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { 31 // take focus if we don't have it already and we should in 32 // touch mode. 33 boolean focusTaken = false; 34 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { 35 focusTaken = requestFocus(); 36 } 37 38 if (prepressed) { 39 // The button is being released before we actually 40 // showed it as pressed. Make it show the pressed 41 // state now (before scheduling the click) to ensure 42 // the user sees it. 43 setPressed(true, x, y); 44 } 45 46 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { 47 // This is a tap, so remove the longpress check 48 removeLongPressCallback(); 49 50 // Only perform take click actions if we were in the pressed state 51 if (!focusTaken) { 52 // Use a Runnable and post this rather than calling 53 // performClick directly. This lets other visual state 54 // of the view update before click actions start. 55 if (mPerformClick == null) { 56 mPerformClick = new PerformClick(); 57 } 58 if (!post(mPerformClick)) { 59 performClick(); 60 } 61 } 62 } 63 64 if (mUnsetPressedState == null) { 65 mUnsetPressedState = new UnsetPressedState(); 66 } 67 68 if (prepressed) { 69 postDelayed(mUnsetPressedState, 70 ViewConfiguration.getPressedStateDuration()); 71 } else if (!post(mUnsetPressedState)) { 72 // If the post failed, unpress right now 73 mUnsetPressedState.run(); 74 } 75 76 removeTapCallback(); 77 } 78 mIgnoreNextUpEvent = false; 79 break; 80 81 case MotionEvent.ACTION_DOWN: 82 mHasPerformedLongPress = false; 83 84 if (performButtonActionOnTouchDown(event)) { 85 break; 86 } 87 88 // Walk up the hierarchy to determine if we're inside a scrolling container. 89 boolean isInScrollingContainer = isInScrollingContainer(); 90 91 // For views inside a scrolling container, delay the pressed feedback for 92 // a short period in case this is a scroll. 93 if (isInScrollingContainer) { 94 mPrivateFlags |= PFLAG_PREPRESSED; 95 if (mPendingCheckForTap == null) { 96 mPendingCheckForTap = new CheckForTap(); 97 } 98 mPendingCheckForTap.x = event.getX(); 99 mPendingCheckForTap.y = event.getY(); 100 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 101 } else { 102 // Not inside a scrolling container, so show the feedback right away 103 setPressed(true, x, y); 104 checkForLongClick(0); 105 } 106 break; 107 108 case MotionEvent.ACTION_CANCEL: 109 setPressed(false); 110 removeTapCallback(); 111 removeLongPressCallback(); 112 mInContextButtonPress = false; 113 mHasPerformedLongPress = false; 114 mIgnoreNextUpEvent = false; 115 break; 116 117 case MotionEvent.ACTION_MOVE: 118 drawableHotspotChanged(x, y); 119 120 // Be lenient about moving outside of buttons 121 if (!pointInView(x, y, mTouchSlop)) { 122 // Outside button 123 removeTapCallback(); 124 if ((mPrivateFlags & PFLAG_PRESSED) != 0) { 125 // Remove any future long press/tap checks 126 removeLongPressCallback(); 127 128 setPressed(false); 129 } 130 } 131 break; 132 } 133 134 return true; 135 } 136 137 return false; 138 }
写这么长是为了好玩吗。
悟空,回想一下SeekBar死前三天的样子吧。
拖动SeekBar的滑块时手指如果离开了SeekBar的范围会怎么样?
滑块还是会左右动的。
所以单纯把离开范围的所有事件直接扔掉显然是不可以的。
那么这个问题怎么处理呢?
聪(sàng)明(xīn)伶(bìng)俐(kuáng)的爆栈网网友想出了许多波谲云诡的技巧,比如:
1 switch (event.getAction()) { 2 case MotionEvent.ACTION_DOWN: 3 rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 4 return true; 5 case MotionEvent.ACTION_UP: 6 if (rect != null 7 && !rect.contains(v.getLeft() + (int) event.getX(), 8 v.getTop() + (int) event.getY())) { 9 // The motion event was outside of the view, handle this as a non-click event 10 11 return true; 12 } 13 // The view was clicked. 14 // TODO: do stuff 15 return true; 16 default: 17 return true;
实在是太有想法了,让人想上金正恩拍手图。
但是这个方法太不优雅了,高贵如我怎能用这种庶民级的work-around。
于是我花了一下午时间思考,终于得出了答案。
1 public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { 2 ...... 3 super.setOnClickListener(this); 4 } 5 6 @Override 7 public void setOnClickListener(final OnClickListener l) { 8 super.setOnClickListener(new OnClickListener() { 9 @Override 10 public void onClick(View v) { 11 CustomView.this.onClick(v); 12 13 if (l != null) { 14 l.onClick(v); 15 } 16 } 17 }); 18 } 19 20 @Override 21 public void onClick(View v) { 22 // Do something 23 }
……………………………………真TM想扇自己两巴掌……