在开始之前,我们先写一个小案例,代码如下
Activity中的代码
package com.sparkhuu.testevent;
import android.nfc.Tag;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;
import java.io.Serializable;
public class MainActivity extends AppCompatActivity implements View.OnTouchListener, View.OnClickListener {
private static final String TAG = "MainActivity";
RelativeLayout rl_layout;
Button btn_view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rl_layout = (RelativeLayout) this.findViewById(R.id.rl_layout);
btn_view = (Button) this.findViewById(R.id.btn_view);
btn_view.setOnTouchListener(this);
rl_layout.setOnTouchListener(this);
btn_view.setOnClickListener(this);
rl_layout.setOnClickListener(this);
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.i(TAG, "OnTouchListener----onTouch----action" + motionEvent.getAction() + "-----" + view);
return false;
}
@Override
public void onClick(View view) {
Log.i(TAG, "OnClickListener---onClick---" + view);
}
}
XML中的代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.sparkhuu.testevent.MainActivity"
>
<Button
android:id="@+id/btn_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</RelativeLayout>
运行以后,我们点击Button,这个时候log日志如下
当我们点击Button以后,左右移动一下再抬起,这个时候log日志如下
当我们点击Button以外区域的log信息如下
从日志大概能猜出,我们点击Button先执行了onTouch并且当我们滑动时候会有action的改变,然后再执行onClick方法,我们很容易联想到会不会是Action_down,Action_move,Action_up分别对应action为0,2,1;
眼睑的可以看到onTouch中返回的是false,如果我们改为true呢?结果如下
则onClick不执行了
总结:
1,View的事件触发先出发onTouch,再触发onClick;
2,如果onTouch中返回true,则不会触发onClick,onTouch默认返回false
这是为什么呢?我们通过源码来进行分析
先分析下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 the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
我们重点来看这几个地方,
if (onFilterTouchEventForSecurity(event)) 判断当前view有没有被挡住,然后定义一个 ListenerInfo li = mListenerInfo;
这个ListenerInfo是view的一个静态内部类,如下
static class ListenerInfo {
/**
* Listener used to dispatch focus change events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnFocusChangeListener mOnFocusChangeListener;
/**
* Listeners for layout change events.
*/
private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
protected OnScrollChangeListener mOnScrollChangeListener;
/**
* Listeners for attach events.
*/
private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
/**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
public OnClickListener mOnClickListener;
/**
* Listener used to dispatch long click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnLongClickListener mOnLongClickListener;
/**
* Listener used to dispatch context click events. This field should be made private, so it
* is hidden from the SDK.
* {@hide}
*/
protected OnContextClickListener mOnContextClickListener;
/**
* Listener used to build the context menu.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnCreateContextMenuListener mOnCreateContextMenuListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
private OnHoverListener mOnHoverListener;
private OnGenericMotionListener mOnGenericMotionListener;
private OnDragListener mOnDragListener;
private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
}
接着重点来了
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
首先li不会为空,li.mOnTouchListener呢可以发现在
/**
* 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) {
getListenerInfo().mOnTouchListener = l;
}
可以发现li.mOnTouchListener是否为空取决于当前view是否设置了setOnTouchListener方法,而我们的案例中设置了,自然也不为空,接着通过位与运算判断当前view是不是enabled,默认都是enabled,接着就会判断onTouch,所以我们之前onTouch返回true的时候就直接result=true不往下执行了
当onTouch返回false的时候,这个时候会执行onTouchEvent,从
if (!result && onTouchEvent(event)) {
result = true;
}
可以看出result的值取决于onTouchEventd的返回,
总结:
触摸事件view会先调用dispatchTouchEvent,
在dispatchTouchEvent会先执行onTouch后执行onClick
如果控件的onTouch返回false或者mOnTouchListener为null(没有setOnTouchListener的情况下)或者控件不是enabled的情况下,会调用onTouchEvent,并且dispatchTouchEvent的返回值和onTouchEvent的返回值一致,
如果控件不是enabled,设置了onTouch也不会执行,只能通过重写onTouchEvent处理,dispatchTouchEvent返回值和onTouchEvent返回值一致
如果控件是enable且onTouch返回true,dispatchTouchEvent直接返回true不会调用onTouchEvent
下面我们来看看onClick和onTouchEvent是什么东东
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
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)) {
performClick();
}
}
}
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;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
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;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
代码过多,我们来分析这段代码
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
可以看出,当控件是disabled状态下onTouchEvent的返回值与状态是否是clickable有关,是clickable的返回true,反之,返回false,view的clickable,enable可以通过xml或者java来设置
通过该方法的返回值,可以看出,如果一个控件是enabled并且是disclickabled,则直接返回false,反之,如果控件是enabled并且是clickable的则会进入一个MotionEvent的switch判断中,最终onTouchEvent返回true,ACTION_DOWN和ACTION_MOVE都做了必要的设置和置位,当ACTION_UP时,首先判断是不是被按下,同时是否可以得到焦点,然后判断如果不是longPress则通过post再UI Thread执行一个performClicl的Runnable,也就是performcilck方法,如下
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
public boolean performClick() {
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);
return result;
}
这个方法也是先定义一个ListenerInfo然后赋值,然后判断li.mOnClickListener 是否为null,决定是否执行onClck,而li.mOnClickListener 是否为null是在setOnClick中赋值的,如下:
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
可以看出控件只要设置了setOnClickListener那么 li.mOnClickListener 就不为null,而且如果控件设置了setOnClickListener,那么会自动设置控件是ciclable的,
总结:
onTouchEvent的 MotionEvent.ACTION_UP中触发onCilck的监听,
当dispatchTouchEvent进行事件分发的时候,只有前一个action返回true。才会触发下一个action。
下面通过自定义一个view来测试
package com.sparkhuu.testevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
/**
* author:sparkhuu
* email:sparkhuu@gmail.com
*/
public class TestButton extends Button {
private static final String TAG = "TestButton";
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(TAG, "dispatchTouchEvent -- action" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "onTouchEvent -- action" + event.getAction());
return super.onTouchEvent(event);
}
}
运行以后结果如下
可以看出dispatchTouchEvent先派发down事件,然后调用onTouch然后调用onTouchEvent返回true,同时dispatchTouchEvent返回true,然后dispatchTouchEvent调用move和up事件,调用完up事件以后调用onClick事件,同时dispatchTouchEvent返回true,至此,一次完整的view事件派发流程结束
将onTouchEvnet返回改为true
点击button运行结果如下
可以发现,当自定义控件只返回了true而没调用super.onTouchEvent(event),事件派发机制和上面类似,只是在up事件中没有调用onClick而已,如果稍作修改如下,可想结果和派发一致
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "onTouchEvent -- action" + event.getAction());
super.onTouchEvent(event);
return true;
}
如果对onTouchEvent的返回再做修改,返回false,如下
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "onTouchEvent -- action" + event.getAction());
super.onTouchEvent(event);
return false;
}
可以看出dispatchTouchEvent在派发down事件以后,紧接着调用onTouch再调用onTouchEvnet的down事件,由于onToucnEvent返回false,立即停止派发,至于后面的RelativeLayout的touch与click事件,下篇博客解释,我们可以确定如果onTouchEvent返回false,那么会阻止事件继续派发
继续修改dispatchTouchEvent返回true
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
// return super.dispatchTouchEvent(event);
return true;
}
可以发现dispatchTouchEvent直接返回true且不调用super,任何事件都得不到触发,同类如果调用super,代码修改如下
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
super.dispatchTouchEvent(event);
return true;
}
结果正常
如果修改dispatchTouchEvent返回值为false,代码如下
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
// super.dispatchTouchEvent(event);
return false;
}
可以看出事件不进行任何触发,关于点击Button触发了Relatvie的事件暂不用关注,下篇解释
继续修改调用super,代码如下
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
super.dispatchTouchEvent(event);
return false;
}
可以看出第一次down事件派发完成,返回false不进行下一次事件派发
再对代码进行如下修改
package com.sparkhuu.testevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
/**
* author:sparkhuu
* email:sparkhuu@gmail.com
*/
public class TestButton extends Button {
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
super.dispatchTouchEvent(event);
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "onTouchEvent -- action" + event.getAction());
super.onTouchEvent(event);
return false;
}
}
再对代码作修改
package com.sparkhuu.testevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
/**
* author:sparkhuu
* email:sparkhuu@gmail.com
*/
public class TestButton extends Button {
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
super.dispatchTouchEvent(event);
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(MainActivity.TAG, "onTouchEvent -- action" + event.getAction());
super.onTouchEvent(event);
return true;
}
}
由此,dispatchTouchEvent是负责事件派发的,返回false将停止下次事件派发,返回true将继续下次事件派发,
总结
1触摸控件view首先执行dispatchTouchEvent进行事件派发
2在dispatchTouchEvnet中会首先执行onTouch方法,后执行onClick方法
3如果控件不是enabled设置了onTouch也不会执行,只能通过重写onTouchEvent方法来实现,dispatchTouchEvent的返回值和onTouchEvent的返回值一致
4如果控件的onTouch返回false或者onTouchlistener为null,或者控件不是enabled情况下调用onTouchEvent,dispatchTouchEvent和ontoucheEvent的返回一样
5如果控件是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent
6当dispatchTouchEvent在事件派发的时候,只有前一个action返回true,才会触发下一个action
可以查看ViewGroup分析
ViewGroup分析