这是个困惑我好久的问题,搞了好久才明白一些,看了很多大牛的技术文章,也写了一些小demo测试了一番,菜鸟在这里和大家分享一下了!
首先,这里涉及到View和ViewGroup,但是View只有事件的响应与否,我的理解是应为View不是容器型的控件,但是ViewGroup是容器型的控件,所以其涉及到事件的传递与回传,所谓回传就是响应!
好了,我们切入主题,
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
当我们触碰到任何一个控件的时候,都会调用该控件的dispatchTouchEvent方法,这里我们查看程序的运行结果如下所示
onTouch execute,action0
onTouch execute,aciton1
onClick execute
可以看到,onTouch是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你还可能会有多次ACTION_MOVE的执行,如果你手抖了一下)。因此事件传递的顺序是先经过onTouch,再传递到onClick。
但是如果我们把onTouch方法的返回值返回true的话,我们会发现onClick竟然没有执行
追究其原因,我们就要仔细的去看一下View的dispatchTouchEvent源码:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
进入if语句首先我们要知道mOnTouchListener 到底是谁,才能进而判断出其是不是为null,按住ctrl+k我们发现了一个惊天大秘密。
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
与那里这里的mOnTouchListener,就是我们写的内部类
new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
我们已经赋值了,它是绝对不会为空的。再看第二个参数(mViewFlags & ENABLED_MASK) == ENABLED,它的意思就是该控件是否可用的enabled,所有控件默认都是可用的,所以这里我们只需要看第三个条件即可,mOnTouchListener.onTouch(this, event),其实这里就是回调啦,这里正是我们自己写的onTouch函数呀,当该函数返回true的时候,disPatcheTouchEvent的返回值就是true,这时代表着onTouch事件是被消耗掉的,所以onclick函数是不会被执行的。
由此可知,onclick事件的执行一定是在onTouchEvent(event)中调用的!所以当onTouch函数返回false的时候,就会调用onTouchEvent函数,好了接下来我们继续看源码
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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 & PREPRESSED) != 0;
if ((mPrivateFlags & 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 (!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) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
这里源码很多,但是我们只需要几行就可以啦,从第三个大的if语句看起
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))
也就是从这里看起,这就话的意思是目前该控件是否是可点击的,我们知道这里的button肯定是可点击的,即使是不可点击的ImageView,我们要是给其注册setOnClickListener()的话,它也就变得为可点击的啦,这里可以查看一下setOnClickListener()的源码
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
该if语句的意思就是如果控件是不可以点击的话,注册该监听事件后,就变得可以点击啦。
所以这时onTouchEvent函数是一定会返回true的,所以onTouch函数中的事件是完全都被执行的,但是此时的onclick也是被执行的,关键是我们我知道它是在哪里被调用,其实就是在onTouchEvent函数中的
if (!post(mPerformClick)) {
performClick();
}
这里我们就要再去查看一下performClick()到底是怎么定义的啦,继续追根溯源,原来秘密在这里
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
首先mOnClickListener 是什么呀,按住ctrl+k,我们去找一找
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}
突然有种豁然开朗的感觉,这里其实回调机制,OnClickListener不就是我们定义的内部类嘛,所以肯定不会为空呀,所以这时就开始执行performClick()的 mOnClickListener.onClick(this)我们自己写的onClick的函数体中内容。
所以明白了上述的原理之后,自定义Button控件,代码如下所示
package com.example.demo2;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Button;
public class MyButton extends Button {
public MyButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context) {
super(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case 0:
System.out.println("000000000000000");
break;
case 1:
System.out.println("111111111111111");
break;
case 2:
System.out.println("222222222222222");
break;
default:
break;
}
return true;
}
}
在布局文件中使用该自定义MyButton 控件在MainActivity中定义其相应的是事件
package com.example.demo2;
import java.security.PublicKey;
import android.os.Bundle;
import android.app.Activity;
import android.support.v4.view.ViewPager;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn = (Button) findViewById(R.id.btn);
btn.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("onTouch------ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
System.out.println("onTouch------ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
System.out.println("onTouch------ACTION_MOVE");
break;
}
return false;
}
});
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("哈哈哈");
}
});
}
}
点击该按钮运行的结果如下所示
05-10 12:04:28.737: I/System.out(9319): onTouch------ACTION_DOWN
05-10 12:04:28.737: I/System.out(9319): 000000000000000
05-10 12:04:29.067: I/System.out(9319): onTouch------ACTION_MOVE
05-10 12:04:29.067: I/System.out(9319): 222222222222222
05-10 12:04:29.067: I/System.out(9319): onTouch------ACTION_UP
05-10 12:04:29.067: I/System.out(9319): 111111111111111
这里是因为当onTouch返回false的时候,(根据源码)会调用onTouchEvent函数,但是此时我们重写了父类的该函数,所以会调用自己的onTouch函数,这里就是多态的思想啦,onTouch返回的为true,所以会把所有事件消耗完,又因为我们在源码中是先判断的 mOnTouchListener.onTouch(this, event)) ,所以该函数中相应的up、down、move都要比onTouchEvent中的要早,由于子类重写了onTouchEvent函数,所以onclick是永远都不会执行到了的!