对于View的事件分发,涉及的有dispatchTouchEvent、onTouchEvent、onTouch、onClick
为了更好的查看View的事件转发,我们先来看个demo
首先定义一个自定义View
public class CustomView extends AppCompatButton {
private static final String TAG = "CustomView";
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
LogUtils.e(TAG,"dispatchTouchEvent=ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
LogUtils.e(TAG,"dispatchTouchEvent=ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
LogUtils.e(TAG,"dispatchTouchEvent=ACTION_UP");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
LogUtils.e(TAG,"onTouchEvent=ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
LogUtils.e(TAG,"onTouchEvent=ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
LogUtils.e(TAG,"onTouchEvent=ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
}
把自定义的按钮添加到布局中:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.xiaoma.restudy.customviews.CustomView
android:id="@+id/mbt_cmb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Custom View" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
最后看CustomActivity代码
public class CustomActivity extends BaseActivity {
private CustomView mMbtCmb;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_custom);
initView();
}
@SuppressLint("ClickableViewAccessibility")
private void initView() {
mMbtCmb = (CustomView) findViewById(R.id.mbt_cmb);
mMbtCmb.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
LogUtils.e(TAG, "onTouch=ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
LogUtils.e(TAG, "onTouch=ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
LogUtils.e(TAG, "onTouch=ACTION_UP");
break;
}
return false;
}
});
mMbtCmb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.e(TAG, "onClick");
}
});
mMbtCmb.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
LogUtils.e(TAG, "onLongClick");
return false;
}
});
}
}
点击按钮后,运行的结果如下
E/CustomView: dispatchTouchEvent=ACTION_DOWN
E/.customviews.CustomActivity: onTouch=ACTION_DOWN
E/CustomView: onTouchEvent=ACTION_DOWN
E/CustomView: dispatchTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onTouch=ACTION_MOVE
E/CustomView: onTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onLongClick
E/CustomView: dispatchTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onTouch=ACTION_MOVE
E/CustomView: onTouchEvent=ACTION_MOVE
E/CustomView: dispatchTouchEvent=ACTION_UP
E/.customviews.CustomActivity: onTouch=ACTION_UP
E/CustomView: onTouchEvent=ACTION_UP
E/.customviews.CustomActivity: onClick
点击下就可以出现上面日志,否则出现一系列ACTION_MOVE事件;但总结来看是ACTION_DOWN、ACTION_MOVE、ACTION_UP三个事件的传递是dispatchTouchEvent-->onTouch-->onTouchEvent-->onLongClick-->onClick
查看源码从View的dispatchTouchEvent方法开始,摘抄代码如下
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//上面没有没有消费,在onTouchEvent中返回为true进行消费。
if (!result && onTouchEvent(event)) {
result = true;
}
1、如果onTouchListener不等于null,并且是enabled,且onTouch返回true,则消费掉事件
2、上面如果没有消费,则执行onTouchEvent,如果返回true,则消费掉事件
总结:也就是说如果onTouch方法返回ture,则不执行onTouchEvent方法
View的onTouchEvent方法如下,摘要主要功能如下
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
return true;
}
1.clickable变量标识如果view是可点击或者长按的状态就是true;
2.如果view是disabled状态,不处理事件,返回clickable变量;意味着一个disabled状态的view,如果可点击或者长按,还是消费事件,但是不响应
3.如果mTouchDlegate不为空,则当前view不消费事件,交给触摸代理消费事件;
4.如果clickable返回true或者view可以在悬停或者长按时显示工具,则view消耗此事件;
查看MotionEvent.ACTION_DOWN事件,源码如下
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
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();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
- mHasPerformedLongPress = false; 长按事件还未触发
- boolean isInScrollingContainer = isInScrollingContainer(); 判断是否是个滑动容器
- 如果是滑动容器,设置 mPrivateFlags |= PFLAG_PREPRESSED; 设置为预按状态;发送一个延迟为ViewConfiguration.getTapTimeout()=100的消息;如果非滑动容器,则直接设置为按压状态,且发送延迟500后执行CheckForLongPress()
- 到达延迟时间后,执行CheckForTap()里的run()方法,取消预按状态,且在setPressed(true, x, y)方法里设置按压状态 mPrivateFlags |= PFLAG_PRESSED;且检查是否有长按事件,如果有再发送一个延迟为ViewConfiguration.getLongPressTimeout()=500的消息(减去上面的延迟时间),到达延迟时间后,执行CheckForLongPress()
总结:100用来检测是否按压了,500时长用来检测是否长按
- 如果设置了长按的回调;如果长按的返回值为true,则设置 mHasPerformedLongPress = true;如果没有设置长按回调或者返回false,则mHasPerformedLongPress依旧等于false;
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}
##################################
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
#################
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
private boolean mOriginalPressedState;
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
查看MotionEvent.ACTION_MOVE事件,源码如下:
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
判断当前坐标是否移除了当前的view,pointInView(x, y, mTouchSlop),如果移出,则
- 执行removeTapCallback(),移出是滑动容易内,则设置 mPrivateFlags &= ~PFLAG_PREPRESSED且不到100移除回调
- 执行 removeLongPressCallback(),移除长按回调
- 如果是按压状态,则设置 mPrivateFlags &= ~PFLAG_PRESSED
总结:只要用户移出了我们的控件:则将mPrivateFlags取出PRESSED标识,且移除所有在DOWN中设置的检测,长按等;
查看MotionEvent.ACTION_UP事件,源码如下:
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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)) {
performClickInternal();
}
}
}
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();
}
mIgnoreNextUpEvent = false;
break;
- 如果mPrivateFlags包含PFLAG_PREPRESSED或者PFLAG_PRESSED则进入if执行语句,也就是无论100ms内或者之后抬起都会进入if语句执行
- 如果mHasPerformedLongPress是false,即长按没有执行,则进入方法体,移除长按回调
- 如果mPerformClick为null,则初始化一个实例;然后立即通过Handler添加到消息队列尾部,如果添加失败,直接执行performClick()
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
- 如果mOnClickListener!=null,则回调onClick()方法
- 如果prepressed为true,则执行mUnsetPressedState.run()方法。我们的mPrivateFlags中的PRESSED取消,如果mPendingCheckForTap不为null,移除;
总结
1.整个view的事件分发流程是:
View.dispatchTouchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中会进行onTouchListener的判断,如果不为null且返回ture,则整个事件被消费,onTouchEvent不会执行,否则执行onTouchEvent
- onTouchEvent中的DOWN、MOVE、UP
DOWN时
1、首先设置mHasPerformedLongPress=false,代表长按是false;
2、如果在滑动容器内,首先设置mPrivateFlags=PFLAG_PREPRESSED且发送一个延迟100ms的mPendingCheckForTap,则将标识位mPrivateFlags=PFLAG_PRESSED,清除PFLAG_PREPRESSED标识,同时发送一个延迟500-100ms的mPendingCheckForLongPress,检测长按任务消息
3、如果不在滑动容器内容,则直接设置mPrivateFlags=PFLAG_PRESSED且发送一个延迟500ms的mPendingCheckForLongPress的检测长按任务消息
4、如果时间超过500ms,则触发mOnLongClickListener;如果mOnLongClickListener不为null,且mOnLongClickListener.onLongClick返回true,则mHasPerformedLongPress=true;否则mHasPerformedLongPress还是false
MOVE时
主要就是检测是否划出了View控件,如果划出了
直接移除mPendingCheckForTap,mPendingCheckForLongPress;如果100ms后mPrivateFlags==PFLAG_PRESSED,则清除mPrivateFlags值
UP时
- 如果mPrivateFlags=PFLAG_PREPRESSED,即100ms内触发up,则依次执行 setPressed(true, x, y)、mUnsetPressedState
2、如果mHasPerformedLongPress==false,代表长按未触发,移除长按检测,执行onClick回调
3.500ms后,如果设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick无法触发;如果没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick依旧可以触发 - 所以onLongClickListener是在move触发,onClick是在up触发