Why onTouch Fires Before onClick: Deep Dive into Android Touch Event Dispatch
This article explains Android's touch event dispatch mechanism, detailing MotionEvent actions, the roles of dispatchTouchEvent, onInterceptTouchEvent, and onTouchEvent, and demonstrates through code why onTouch executes before onClick and how returning true from onTouch consumes the click event.
Click Event Transmission Rules
What is MotionEvent
MotionEvent represents the series of low‑level input events generated when a finger contacts the screen. The most important actions are:
ACTION_DOWN – finger just touches the screen.
ACTION_MOVE – finger moves while staying on the screen.
ACTION_UP – finger lifts off the screen.
Dispatch Process
The Android view system distributes a MotionEvent through three core methods: public boolean dispatchTouchEvent(MotionEvent ev) – entry point that decides how the event propagates. public boolean onInterceptTouchEvent(MotionEvent ev) – used by ViewGroup to optionally intercept the event before children receive it. public boolean onTouchEvent(MotionEvent ev) – final handler that processes the event for the view itself; its return value indicates whether the event was consumed.
OnClickListener is the lowest‑priority handler and is invoked only after the touch handling chain finishes.
Registering Listeners
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("TAG", "onClick executed");
}
}); button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("TAG", "onTouch executed, Action=" + event.getAction());
return false; // return true to consume the event and stop further propagation
}
});When the app runs, onTouch() is called before onClick(). If onTouch() returns true, the click listener is never invoked because the event is considered consumed.
Source Code Analysis
View.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// stop nested scroll on down
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
result = true; // OnTouchListener consumed the event
}
if (!result && onTouchEvent(event)) {
result = true; // View's own onTouchEvent consumed the event
}
}
return result;
}The method creates a result flag, runs a security filter, invokes any registered OnTouchListener, and finally calls onTouchEvent. If any step returns true, the event is marked as handled and propagation stops.
onFilterTouchEventForSecurity
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}This method blocks touch events when the view or its window is obscured, providing a security safeguard.
View.onTouchEvent
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 (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
// Handle click release
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
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;
}
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);
}
break;
}
return true;
}
return false;
}The method first determines whether the view is clickable (or a tooltip). It then switches on the action type to perform the appropriate logic: start long‑press detection on ACTION_DOWN, schedule a click on ACTION_UP, cancel pending callbacks on ACTION_CANCEL, and update visual feedback on ACTION_MOVE.
checkForLongClick
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);
}
}This runnable posts a delayed task that will invoke performLongClick if the press lasts longer than the system long‑press timeout.
performClickInternal & performClick
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
notifyAutofillManagerOnClick();
final ListenerInfo li = mListenerInfo;
boolean result;
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;
}These methods finally trigger the OnClickListener after all touch handling has completed, playing the click sound, sending accessibility events, and notifying autofill services.
Tip: The security filter checks the FILTER_TOUCHES_WHEN_OBSCURED flag. By clearing or setting this flag you can control whether touches are ignored when the window is covered by another UI element.
In summary, the event flow for a clickable view is:
dispatchTouchEvent → (OnTouchListener?.onTouch) → onTouchEvent → (if ACTION_UP) performClickInternal → performClick → OnClickListenerIf onTouch() (either from an OnTouchListener or the view's own onTouchEvent) returns true, the event is considered consumed and the subsequent onClick() will not be invoked.
AI Code to Success
Focused on hardcore practical AI technologies (OpenClaw, ClaudeCode, LLMs, etc.) and HarmonyOS development. No hype—just real-world tips, pitfall chronicles, and productivity tools. Follow to transform workflows with code.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
