在分析分发机制之前,我们来了解一些预备知识。
MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间。
MotionEvent.ACTION_MOVE:手指在屏幕上移动
MotionEvent.ACTION_UP:手指离开屏幕瞬间
MotionEvent.ACTION_CANCEL:取消手势
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
案例
public class MyButton extends Button {
private static String TAG = MyButton.class.getSimpleName();
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.dispatchevent.MyButton
android:id="@+id/my_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="click me"/>
LinearLayout>
public class MainActivity extends AppCompatActivity {
private static String TAG = "MyButton";
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = findViewById(R.id.my_button);
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
int action = motionEvent.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouch ACTION_UP");
break;
default:
break;
}
return false;
}
});
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "onclick");
}
});
mButton.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.e(TAG, "longclick");
return false;
}
});
}
}
好了,跟View事件相关一般就三个地方,一个onTouchEvent,一个dispatchTouchEvent,一个setOnTouchListener;
我们来看一下日志输出:
01-07 22:20:38.992 24590-24590/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_DOWN
01-07 22:20:38.992 24590-24590/com.example.dispatchevent E/MyButton: onTouch ACTION_DOWN
01-07 22:20:38.992 24590-24590/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_DOWN
01-07 22:20:39.016 24590-24590/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
01-07 22:20:39.016 24590-24590/com.example.dispatchevent E/MyButton: onTouch ACTION_MOVE
01-07 22:20:39.016 24590-24590/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_MOVE
01-07 22:20:39.019 24590-24590/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_UP
01-07 22:20:39.020 24590-24590/com.example.dispatchevent E/MyButton: onTouch ACTION_UP
01-07 22:20:39.020 24590-24590/com.example.dispatchevent E/MyButton: onTouchEvent ACTION_UP
01-07 22:20:39.029 24590-24590/com.example.dispatchevent E/MyButton: onclick
好了,可以看到,不管是DOWN,MOVE,UP都会按照下面的顺序执行:
源码分析
首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法,由于Button中没有会一直向上寻找,最终发现View.dispatchTouchEvent。
View.dispatchTouchEvent
/**
* 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 (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
首先判断mOnTouchListener不为null,并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接return true,也就是下面的onTouchEvent(event)不会被执行了;
我们在看一个简单的示例:
mButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
int action = motionEvent.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouch ACTION_UP");
break;
default:
break;
}
return true;
//return false;
}
});
01-07 04:23:35.132 14251-14251/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_DOWN
01-07 04:23:35.134 14251-14251/com.example.dispatchevent E/MyButton: onTouch ACTION_DOWN
01-07 04:23:35.154 14251-14251/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
01-07 04:23:35.156 14251-14251/com.example.dispatchevent E/MyButton: onTouch ACTION_MOVE
01-07 04:23:35.163 14251-14251/com.example.dispatchevent E/MyButton: dispatchTouchEvent ACTION_UP
01-07 04:23:35.164 14251-14251/com.example.dispatchevent E/MyButton: onTouch ACTION_UP
从这个示例中我们看到,如果onTouch中返回true,从而使onTouchEvent就不会执行并且onClick也没有执行,从而得出结论onTouch要优先于onClick之前被执行,并且onClick是在onTouchEvent中被执行的。
View.onTouchEvent
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
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;
}
}
//(1)此处肯定返回true
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
//(4)MotionEvent.ACTION_UP操作
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;
//(2)MotionEvent.ACTION_DOWN操作
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;
//(3)MotionEvent.ACTION_MOVE状态判断
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;
}
分析:
(1)
首先我们需要注意的是在23行,onTouchEvent肯定是返回true的,接下来就是 switch (event.getAction())了,判断事件类型,DOWN,MOVE,UP等;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//...
return true;
}
(2)MotionEvent.ACTION_DOWN
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
private final class CheckForTap implements Runnable {
public void run() {
//取消mPrivateFlags的PREPRESSED,然后设置PRESSED标识
mPrivateFlags &= ~PREPRESSED;
mPrivateFlags |= PRESSED;
//刷新背景
refreshDrawableState();
//如果View支持长按事件,则再发一个延时消息,检测长按
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick(ViewConfiguration.getTapTimeout());
}
}
}
private void postCheckForLongClick(int delayOffset) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
//发送延迟消息,执行CheckForLongPress中run方法,500ms之后触发performLongClick(),之前都算是performOnClick()
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
//调用长按事件,当返回为true时,将mHasPerformedLongPress设置为true,从而使Move_Up中的performOnClick无法执行
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
(3)MotionEvent.ACTION_MOVE
简单来说就是先拿到当前触摸的x,y坐标,然后判断触摸是否移出当前View,如果移出了将清除之前的标识以及removeCallbacks等操作;
(4)MotionEvent.ACTION_UP
//之前如果没有执行performLongClick或者返回值为false,mHasPerformedLongPress将为false;
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();
}
}
}
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
之前也说了,如果没有设置长按回调或者长按回调返回的是false,则mHasPerformedLongPress依然是false,此时将满足条件,从而会执行performClick()。
另外最后还会执行UnsetPressedState.run();
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
setPressed(false);
}
}
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
//可以重新此方法
protected void dispatchSetPressed(boolean pressed) {
}
总结
(1)View的事件转发流程
View.dispatchTouchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行,否则执行onTouchEvent。
(2)Down时
(3)Move时
主要就是检测用户是否划出控件,如果划出将清除标识以及removeCallback等;
(4)Up时
最后一个问题
问:setOnLongClickListener和setOnClickListener是否只能执行一个?
答:不是的,只要setOnLongClickListener中的onLongClick返回false,则两个都会执行;返回true则会屏蔽setOnClickListener。