最近发现团队里有些员工在做一些自定义控件的时候感觉比较吃力。尤其是做触摸事件这种东西的时候。很多人对机制并不理解。因为百度出来的东西都太理论化了。确实不好理解。
今天带大家坐几个小demo。帮助理解一下。
先从简单的view 的事件分发机制开始解释。
我们首先自定义一个工程
package com.example.testtouch; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.widget.ImageView; import android.widget.TextView; public class MainActivity extends Activity { private TextView tv; private ImageView iv; @Override protected void onCreate(Bundle savedsInstanceState) { super.onCreate(savedsInstanceState); setContentView(R.layout.activity_main); tv = (TextView) this.findViewById(R.id.tv); iv = (ImageView) this.findViewById(R.id.iv); iv.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.v("test", "iv event down ==" + MotionEvent.ACTION_DOWN); } if (event.getAction() == MotionEvent.ACTION_UP) { Log.v("test", "iv event up ==" + MotionEvent.ACTION_UP); } return false; } }); tv.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.v("test", "tv event down ==" + MotionEvent.ACTION_DOWN); } if (event.getAction() == MotionEvent.ACTION_UP) { Log.v("test", "tv event up ==" + MotionEvent.ACTION_UP); } return false; } }); tv.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Log.v("test", "on click"); } }); } }
然后运行起来以后 点击一下那个textview。
输出日志如下:
10-31 02:54:40.456: V/test(1430): tv event down ==0
10-31 02:54:40.523: V/test(1430): tv event up ==1
10-31 02:54:40.526: V/test(1430): on click
可以看出来 ontouch事件是在onclick事件以前被调用的。
如果我们把 ontouch事件的返回值 更改为true的话。那么就会发现 onclick 事件就不会执行了。
可以理解成 如果ontouch事件 返回的值为true的话 那么剩下的触摸操作全部都被拦截了。
换句话说 只要这个方法返回值 为true 那么剩下的操作就全部都没有了。
我们可以看源代码 去验证一下。首先我们得知道一个前提。 那就是view的事件分发都是由 dispatchTouchEvent
这个方法来控制的,这个方法 在view的类里面定义 我们去看一下源代码。
/** * 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) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }
主要看这个部分
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
如果mOnTouchListener 这个不为空 并且 (mViewFlags & ENABLED_MASK) == ENABLED(代表这个控件可以被点击)
并且mOnTouchListener.onTouch(this, event) 这个也返回true 那么dispatchTouchEvent就返回true了。
否则就执行 onTouchEvent(event) 这个方法 并且返回true。
分析完这个 就比较好理解了,这边是先调用的OnTouchListener.onTouch(this, event) 这个事件,所以我们能看到log输出 ontouch事件
是在onclick时间之前的。并且这个方法 如果返回false 才会走到onTouchEvent 这个事件里面去。如果返回true 那么dispatchTouchEvent
这个函数就直接返回了。onTouchEvent 就永远不会得到执行。这也就解释了为什么我们如果把onTouch事件的值设置成true ,onClick方法
就不会得到执行。
当然也可以看出来 oncLIClick事件是在onTouchEvent里执行的。哪我们继续看源代码onTouchEvent 是怎么做的。
1 /** 2 * Implement this method to handle touch screen motion events. 3 * 4 * @param event The motion event. 5 * @return True if the event was handled, false otherwise. 6 */ 7 public boolean onTouchEvent(MotionEvent event) { 8 final int viewFlags = mViewFlags; 9 10 if ((viewFlags & ENABLED_MASK) == DISABLED) { 11 if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) { 12 mPrivateFlags &= ~PRESSED; 13 refreshDrawableState(); 14 } 15 // A disabled view that is clickable still consumes the touch 16 // events, it just doesn‘t respond to them. 17 return (((viewFlags & CLICKABLE) == CLICKABLE || 18 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); 19 } 20 21 if (mTouchDelegate != null) { 22 if (mTouchDelegate.onTouchEvent(event)) { 23 return true; 24 } 25 } 26 27 if (((viewFlags & CLICKABLE) == CLICKABLE || 28 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { 29 switch (event.getAction()) { 30 case MotionEvent.ACTION_UP: 31 boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; 32 if ((mPrivateFlags & PRESSED) != 0 || prepressed) { 33 // take focus if we don‘t have it already and we should in 34 // touch mode. 35 boolean focusTaken = false; 36 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { 37 focusTaken = requestFocus(); 38 } 39 40 if (prepressed) { 41 // The button is being released before we actually 42 // showed it as pressed. Make it show the pressed 43 // state now (before scheduling the click) to ensure 44 // the user sees it. 45 mPrivateFlags |= PRESSED; 46 refreshDrawableState(); 47 } 48 49 if (!mHasPerformedLongPress) { 50 // This is a tap, so remove the longpress check 51 removeLongPressCallback(); 52 53 // Only perform take click actions if we were in the pressed state 54 if (!focusTaken) { 55 // Use a Runnable and post this rather than calling 56 // performClick directly. This lets other visual state 57 // of the view update before click actions start. 58 if (mPerformClick == null) { 59 mPerformClick = new PerformClick(); 60 } 61 if (!post(mPerformClick)) { 62 performClick(); 63 } 64 } 65 } 66 67 if (mUnsetPressedState == null) { 68 mUnsetPressedState = new UnsetPressedState(); 69 } 70 71 if (prepressed) { 72 postDelayed(mUnsetPressedState, 73 ViewConfiguration.getPressedStateDuration()); 74 } else if (!post(mUnsetPressedState)) { 75 // If the post failed, unpress right now 76 mUnsetPressedState.run(); 77 } 78 removeTapCallback(); 79 } 80 break; 81 82 case MotionEvent.ACTION_DOWN: 83 mHasPerformedLongPress = false; 84 85 if (performButtonActionOnTouchDown(event)) { 86 break; 87 } 88 89 // Walk up the hierarchy to determine if we‘re inside a scrolling container. 90 boolean isInScrollingContainer = isInScrollingContainer(); 91 92 // For views inside a scrolling container, delay the pressed feedback for 93 // a short period in case this is a scroll. 94 if (isInScrollingContainer) { 95 mPrivateFlags |= PREPRESSED; 96 if (mPendingCheckForTap == null) { 97 mPendingCheckForTap = new CheckForTap(); 98 } 99 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 100 } else { 101 // Not inside a scrolling container, so show the feedback right away 102 mPrivateFlags |= PRESSED; 103 refreshDrawableState(); 104 checkForLongClick(0); 105 } 106 break; 107 108 case MotionEvent.ACTION_CANCEL: 109 mPrivateFlags &= ~PRESSED; 110 refreshDrawableState(); 111 removeTapCallback(); 112 break; 113 114 case MotionEvent.ACTION_MOVE: 115 final int x = (int) event.getX(); 116 final int y = (int) event.getY(); 117 118 // Be lenient about moving outside of buttons 119 if (!pointInView(x, y, mTouchSlop)) { 120 // Outside button 121 removeTapCallback(); 122 if ((mPrivateFlags & PRESSED) != 0) { 123 // Remove any future long press/tap checks 124 removeLongPressCallback(); 125 126 // Need to switch from pressed to not pressed 127 mPrivateFlags &= ~PRESSED; 128 refreshDrawableState(); 129 } 130 } 131 break; 132 } 133 return true; 134 } 135 136 return false; 137 }
看62行 有一个 performClick() 我们去看看这个方法。
/** * Call this view‘s OnClickListener, if it is defined. * * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
看到没,我们的点击事件回调就是在这里做的!
回过头来 我们再看看 onTouchEvent 那个switch语句,我们会发现 最终 他们的返回值 都是true!!! 无论是什么事件 都是return true。
然后回过头来 看 dispatchTouchEvent 这个方法。
前面两行代码。
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
大家都知道 触摸事件是由dispatchTouchEvent 这个函数去进行分发的。无论是什么触摸事件,都是手指先点下去 也就是 action down事件。action down的值是0.
所以你看 这边的代码意思就是如果action down的返回事件是true 那就分发。如果action down的返回onTouchEvent都是false的话 哪后面的事件就全部拦截了。
可以理解成。对于dispatchTouchEvent 来说 如果ACTION_DOWN事件返回true,就说明它需要处理这个事件,就让它接收所有的触屏事件,否则,说明它不用处理,也就不让它接收后续的触屏事件了。