在 performClick 方法中 , 调用了 li.mOnClickListener.onClick(this); , li.mOnClickListener 就是用户设置的点击事件监听器 ;
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { 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; } }
二、触摸事件 与 点击事件 冲突处理
通过分析上述 View 的 dispatchTouchEvent 方法的源码可知 ,
如果触摸事件 返回 true ,
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { public boolean dispatchTouchEvent(MotionEvent event) { //noinspection SimplifiableIfStatement // 设置的 触摸监听器 就是封装在该对象中 ListenerInfo li = mListenerInfo; // 判断该组件是否被用户设置了 触摸监听器 OnTouchListener if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED // 执行被用户设置的 触摸监听器 OnTouchListener && li.mOnTouchListener.onTouch(this, event)) { // 如果用户设置的 触摸监听器 OnTouchListener 触摸方法返回 true // 此时该分发方法的返回值就是 true result = true; } } }
则点击事件就会被屏蔽 ;
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { public boolean dispatchTouchEvent(MotionEvent event) { // 如果上面为 true ( 触摸监听器的触摸事件处理返回 true ) , 就会阻断该分支的命中 , 该分支不执行了 // 也就不会调用 View 组件自己的 onTouchEvent 方法 // 因此 , 如果用户的 触摸监听器 OnTouchListener 返回 true // 则 用户的 点击监听器 OnClickListener 会被屏蔽掉 // 如果同时设置了 点击监听器 OnClickListener 和 触摸监听器 OnTouchListener // 触摸监听器 OnTouchListener 返回 false , 点击监听器 OnClickListener 才能被调用到 if (!result && onTouchEvent(event)) { result = true; } } }
方法一 : 最简单的方法是 让 触摸事件 返回 false , 这样 点击和触摸 事件 都可以共存 ;
方法二 : 如果一定要让触摸事件返回 true , 则只能在触摸事件中 手动调用 View 的 performClick() 方法 , 但是要注意 控制 触摸的 按下 , 移动 , 抬起 事件 , 细粒度的分析与控制每个事件的关系 , 然后模拟出点击事件的调用逻辑 ;
三、View 事件分发相关源码
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { /** * 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; // 判断该组件是否被用户设置了 触摸监听器 OnTouchListener if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED // 执行被用户设置的 触摸监听器 OnTouchListener && li.mOnTouchListener.onTouch(this, event)) { // 如果用户设置的 触摸监听器 OnTouchListener 触摸方法返回 true // 此时该分发方法的返回值就是 true result = true; } // 如果上面为 true ( 触摸监听器的触摸事件处理返回 true ) , 就会阻断该分支的命中 , 该分支不执行了 // 也就不会调用 View 组件自己的 onTouchEvent 方法 // 因此 , 如果用户的 触摸监听器 OnTouchListener 返回 true // 则 用户的 点击监听器 OnClickListener 会被屏蔽掉 // 如果同时设置了 点击监听器 OnClickListener 和 触摸监听器 OnTouchListener // 触摸监听器 OnTouchListener 返回 false , 点击监听器 OnClickListener 才能被调用到 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; } /** * 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; } } if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: // 点击事件 Click 是 按下 + 抬起 事件 // 如果要判定点击 , 需要同时有 MotionEvent.ACTION_DOWN + 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; 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; } /** * Entry point for {@link #performClick()} - other methods on View should call it instead of * {@code performClick()} directly to make sure the autofill manager is notified when * necessary (as subclasses could extend {@code performClick()} without calling the parent's * method). */ private boolean performClickInternal() { // Must notify autofill manager before performing the click actions to avoid scenarios where // the app has a click listener that changes the state of views the autofill service might // be interested on. notifyAutofillManagerOnClick(); return 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. */ // NOTE: other methods on View should not call this method directly, but performClickInternal() // instead, to guarantee that the autofill manager is notified when necessary (as subclasses // could extend this method without calling super.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; } }