在我们自定义view或者嵌套view时,经常需要处理滑动事件,点击事件等各种交互事件。在处理过程中,我们可能会遇到事件不响应,滑动和点击事件冲突等问题,这时候,如果我们理解Android触摸事件的分发和处理,处理起来就会得心应手。
本系列分析前两篇,建议先看看:
Android 触摸事件分发和处理机制解析(一)Activity篇
Android 触摸事件分发和处理机制解析(二)ViewGroup篇
这一篇,我们就来看下View的触摸事件分发和处理。具体是 dispatchTouchEvent(),onTouchEvent(),和OnTouchListener的onTouch方法 。
强烈建议打开源码对照进行查看,便于熟悉代码。本文用的是Android 8.0 SDK的源码
先看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 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;
}
接下来,我们重点分析中间那一段:
//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;
}
可以看到,这几行代码中出现了我们所关注的其它两个触摸事件处理方法:一个是OnTouchListener.onTouch(),一个是onTouchEvent()。
而且我们可以先得出一个结论:
结论一:
onTouch()是先于onTouchEvent()被调用的。
如果onTouch()返回false的话,onTouchEvent()才会被调用。
而如果onTouch返回true的话,onTouchEvent()是否就不会被调用了呢?这还得看其它几个判断条件:
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
在源码里找下这个mListenerInfo以及mOnTouchListener是在什么时候被赋值的,找到了唯一的一处:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
那么这个getListenerInfo() 又会在哪儿被调用呢?我们会感兴趣的是这个:
/**
* 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;
}
源码里getListenerInfo()有多处调用,但是mOnTouchListener赋值的地方也只有这一处。
也就是说,在我们每次写代码时,如果调用了setOnTouchListener()方法,并传入了一个OnTouchListener,则View类的mListenerInfo肯定就不会为null了,而且它的mOnTouchListener变量也会被赋值为我们传入的OnTouchListener。
看到这,我们再来看下刚才需要分析的代码:
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
也就是说,如果我们给View设置了onTouchListener的话,li != null && li.mOnTouchListener != null为true。
那么(mViewFlags & ENABLED_MASK) == ENABLED表示判断这个View是不是enable的状态。如果我们调用了setEnable(false),则这儿为false。默认都是true。
因此把这段代码再串起来总结下,就是
结论二:如果我们没有给View设置OnTouchListener,也就不会有onTouch()。View的onTouchEvent()会直接被调用,其返回值就作为dispatchTouchEvent()的返回值。
结论三:如果我们给ViewView设置了OnTouchListener,则onTouch()会先执行。又分两种情况:
如果onTouch()返回true,则onTouchEvent()就不会执行了,dispatchTouchEvent()也返回true。
如果onTouch()返回false,则onTouchEvent()会执行,其返回值就作为dispatchTouchEvent()返回值。
那View的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();
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;
}
}
如果进了下面这个流程,则最后都返回true。否则返回false。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
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)) {
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:
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;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
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;
}
return true;
}
return false;
}
代码比较长,我们只关注重点部分。上面标识出来的那部分代码,总结一下也就一行:
performClick();
看下这个方法干了什么:
/**
* 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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
看到这个方法里的判断,似曾相识对吧,mOnClickListener被赋值的逻辑和onTouchListener那段是一样的:如果我们给View设置了OnClickListener的话,mOnClickListener就是我们设置的这个对象。
区别在于,setOnClickListener()方法里,view会被自动地设置为clickable。
/**
* 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;
}
因此这一段串起来,结论就是:
结论四:onClick()方法是在onTouchEvent()中被调用的。如果我们给View设置了OnClickListener,则当触摸事件进行到抬起动作(ACTION_UP)时,onClick()方法会执行。
最后再列一下本篇的结论:
结论一:View的onTouch()是先于onTouchEvent()被调用的。两者都在View的dispatchTouchEvent()中被调用。
结论二:如果我们没有给View设置OnTouchListener(也就不会有onTouch()),View的onTouchEvent()会直接被调用,其返回值就作为dispatchTouchEvent()的返回值。
结论三:如果我们给ViewView设置了OnTouchListener,则onTouch()会先执行。又分两种情况:
如果onTouch()返回true,则onTouchEvent()就不会执行了,dispatchTouchEvent()也返回true。
如果onTouch()返回false,则onTouchEvent()会执行,其返回值就作为dispatchTouchEvent()返回值。
结论四:onClick()方法是在onTouchEvent()中被调用的。如果我们给View设置了OnClickListener,则当触摸事件进行到抬起动作(ACTION_UP)时,onClick()方法会执行。