Touch Event Dispatching Mechanism in Android
The onInterceptTouchEvent() method belongs exclusively to ViewGroup and serves as a pre-processor for touch events. Its core purpose is to intercept onTouchEvent() calls destined for the ViewGroup or its children before they ocurr. This design enables ViewGroups to monitor and potentially redirect touch events centrally—accordingly, controls lacking child views (like TextView) do not expose this method, while containers like LinearLayout do.
Return Value Semantics
Return values fundamentally control event propagation:
- Returning
trueindicates the method consumes the event entirely - Returning
falsesignals incomplete handling, allowing the event to propagate further
The Android SDK documentation elaborates on these behaviors:
- The initial down event always arrives at
onInterceptTouchEvent()first - If no child view handles the down event,
onTouchEvent()receives it; returningtruefromonTouchEvent()ensures subsequent gesture events follow the normal path - Returning
truefromonTouchEvent()blocks furtheronInterceptTouchEvent()calls for the remainder of the gesture - Returning
falsefromonInterceptTouchEvent()allows subsequent events (including the final up) to flow through bothonInterceptTouchEvent()and the target'sonTouchEvent() - Returning
truefromonInterceptTouchEvent()cancels the target view (receivingACTION_CANCEL) and redirects all remaining events to the intercepting view'sonTouchEvent()
Core Dispatching Rules
- Down events always arrive at
onInterceptTouchEvent()before any other handling - Returning
falsefrom a ViewGroup'sonInterceptTouchEvent()on down allows subsequent move/up events to pass through the same interception chain before reaching the target view'sonTouchEvent() - Returning
truefrom a ViewGroup'sonInterceptTouchEvent()on down redirects all subsequent events (including the initial down) directly to that ViewGroup'sonTouchEvent(), preventing any child view from receiving events - If a target view's
onTouchEvent()returnsfalse, the event escalates to its parent view'sonTouchEvent() - If a target view's
onTouchEvent()returnstrue, subsequent events continue routing to that view
Practical Demonstration
Consider a three-layer view hierarchy: ContainerTop and ContainerMiddle (both FrameLayout subclasses), with CustomLabel (a TextView subclass) at the bottom.
layout_structure.xml
<?xml version="1.0" encoding="utf-8"?>
<com.example.ContainerTop
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.ContainerMiddle
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<com.example.CustomLabel
android:id="@+id/display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SampleText"
android:textSize="40sp"
android:textStyle="bold"
android:background="#FFFFFF"
android:textColor="#0000FF"/>
</com.example.ContainerMiddle>
</com.example.ContainerTop>
Scenario 1: No Interception, Target Handles Events
All onInterceptTouchEvent() return false, CustomLabel.onTouchEvent() returns true:
04-11 03:58:42.620: DEBUG/ContainerTop: onInterceptTouchEvent ACTION_DOWN
04-11 03:58:42.620: DEBUG/ContainerMiddle: onInterceptTouchEvent ACTION_DOWN
04-11 03:58:42.620: DEBUG/CustomLabel: onTouchEvent ACTION_DOWN
04-11 03:58:42.800: DEBUG/ContainerTop: onInterceptTouchEvent ACTION_MOVE
04-11 03:58:42.800: DEBUG/ContainerMiddle: onInterceptTouchEvent ACTION_MOVE
04-11 03:58:42.800: DEBUG/CustomLabel: onTouchEvent ACTION_MOVE
04-11 03:58:43.130: DEBUG/ContainerTop: onInterceptTouchEvent ACTION_UP
04-11 03:58:43.130: DEBUG/ContainerMiddle: onInterceptTouchEvent ACTION_UP
04-11 03:58:43.150: DEBUG/CustomLabel: onTouchEvent ACTION_UP
Analysis: This represents standard behavior. Event propagation flows bottom-up (ContainerTop → ContainerMiddle → CustomLabel). Since onInterceptTouchEvent() consistently returns false, intermediate ViewGroups never invoke their own onTouchEvent().
Scenario 2: Upper Container Intercepts Down Event
ContainerTop.onInterceptTouchEvent() returns true on down, CustomLabel.onTouchEvent() returns true:
04-11 03:09:27.589: DEBUG/ContainerTop: onInterceptTouchEvent ACTION_DOWN
04-11 03:09:27.589: DEBUG/ContainerTop: onTouchEvent ACTION_DOWN
04-11 03:09:27.629: DEBUG/ContainerTop: onTouchEvent ACTION_MOVE
04-11 03:09:27.689: DEBUG/ContainerTop: onTouchEvent ACTION_MOVE
04-11 03:09:27.959: DEBUG/ContainerTop: onTouchEvent ACTION_UP
Analysis: Intercepting the down event with true immediately redirects all subsequent events (including the captured down) to ContainerTop's onTouchEvent(). Neither ContainerMiddle nor CustomLabel receives any touch events.
Scenario 3: Target Fails to Handle, Parent Succeeds
Both container onInterceptTouchEvent() return false, CustomLabel.onTouchEvent() returns false, ContainerMiddle.onTouchEvent() returns true:
04-11 09:50:21.147: DEBUG/ContainerTop: onInterceptTouchEvent ACTION_DOWN
04-11 09:50:21.147: DEBUG/ContainerMiddle: onInterceptTouchEvent ACTION_DOWN
04-11 09:50:21.147: DEBUG/CustomLabel: onTouchEvent ACTION_DOWN
04-11 09:50:21.147: DEBUG/ContainerMiddle: onTouchEvent ACTION_DOWN
04-11 09:50:21.176: DEBUG/ContainerTop: onInterceptTouchEvent ACTION_MOVE
04-11 09:50:21.176: DEBUG/ContainerMiddle: onTouchEvent ACTION_MOVE
04-11 09:50:21.206: DEBUG/ContainerTop: onInterceptTouchEvent ACTION_MOVE
04-11 09:50:21.217: DEBUG/ContainerMiddle: onTouchEvent ACTION_MOVE
04-11 09:50:21.486: DEBUG/ContainerTop: onInterceptTouchEvent ACTION_UP
04-11 09:50:21.486: DEBUG/ContainerMiddle: onTouchEvent ACTION_UP
Analysis: Since CustomLabel returns false, the down event bubbles up to ContainerMiddle's onTouchEvent(). After ContainerMiddle returns true, subsequent move/up events bypass CustomLabel entire, routing through the interception chain to ContainerMiddle's onTouchEvent().
Reference Implementation
MainActivity.java
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private TextView displayView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_structure);
}
}
ContainerTop.java
public class ContainerTop extends FrameLayout {
private static final String TAG = "ContainerTop";
public ContainerTop(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int actionType = event.getAction();
Log.d(TAG, "intercept: " + getActionName(actionType));
if (actionType == MotionEvent.ACTION_DOWN) {
return false; // Change to true to intercept
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionType = event.getAction();
Log.d(TAG, "touch: " + getActionName(actionType));
return true; // Change to false for bubble-up testing
}
private String getActionName(int action) {
switch (action) {
case MotionEvent.ACTION_DOWN: return "DOWN";
case MotionEvent.ACTION_MOVE: return "MOVE";
case MotionEvent.ACTION_UP: return "UP";
case MotionEvent.ACTION_CANCEL: return "CANCEL";
default: return "UNKNOWN";
}
}
}
ContainerMiddle.java
public class ContainerMiddle extends FrameLayout {
private static final String TAG = "ContainerMiddle";
public ContainerMiddle(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int actionType = event.getAction();
Log.d(TAG, "intercept: " + getActionName(actionType));
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionType = event.getAction();
Log.d(TAG, "touch: " + getActionName(actionType));
return true;
}
private String getActionName(int action) {
switch (action) {
case MotionEvent.ACTION_DOWN: return "DOWN";
case MotionEvent.ACTION_MOVE: return "MOVE";
case MotionEvent.ACTION_UP: return "UP";
case MotionEvent.ACTION_CANCEL: return "CANCEL";
default: return "UNKNOWN";
}
}
}
CustomLabel.java
public class CustomLabel extends AppCompatTextView {
private static final String TAG = "CustomLabel";
public CustomLabel(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionType = event.getAction();
Log.d(TAG, "touch: " + getActionName(actionType));
return false; // Returns false to allow event bubble-up
}
private String getActionName(int action) {
switch (action) {
case MotionEvent.ACTION_DOWN: return "DOWN";
case MotionEvent.ACTION_MOVE: return "MOVE";
case MotionEvent.ACTION_UP: return "UP";
case MotionEvent.ACTION_CANCEL: return "CANCEL";
default: return "UNKNOWN";
}
}
}