Zygote
Input & windowsintermediate

Touch dispatch in the View tree

DOWN goes down the tree, everything else goes up. onInterceptTouchEvent, onTouchEvent, the touch target, and the cancel that fires when an ancestor changes its mind.

12 minAPI 1–37 · 17interactivequizverified 2026-10-27

The view dispatch rules are three methods and one rule. The methods are dispatchTouchEvent, onInterceptTouchEvent and onTouchEvent; the rule is that a gesture has exactly one owner, and it is chosen on the first event and kept until the gesture ends.

TL;DR#

  • ACTION_DOWN is offered down the tree. A ViewGroup can intercept it, or pass it to children in reverse draw order.
  • The first view that returns true from onTouchEvent becomes the touch target for the rest of the gesture.
  • Subsequent ACTION_MOVE and ACTION_UP go straight to the target — they do not re-traverse the tree.
  • If an ancestor starts intercepting mid-gesture, the target receives ACTION_CANCEL and loses the gesture.
  • performClick() fires only if the view consumed ACTION_DOWN and the pointer is still inside its bounds on ACTION_UP.

Mental model#

Imagine a company where a customer request arrives at the front desk.

The request goes down the org chart: the front desk asks each department whether it wants to handle it. A department can take it immediately (intercept) or forward it to its own team.

The first team that says "we'll handle it" owns the request for the rest of its life. Later updates go directly to that team, not back through the org chart.

If a manager later decides to take over, the team gets a message saying "stop working on this" — that is the cancel.

Where the analogy breaks#

An org chart is hierarchical and stable. A view tree changes while a gesture is in progress: views can be removed, added, or moved, and a view that was the target can be detached. The framework handles this with the cancel mechanism and with mFirstTouchTarget, a linked list that survives re-layout. The mental model also hides that a ViewGroup can intercept after the gesture starts, which has no clean corporate equivalent.

The big picture#

Loading diagram…
Text description of this diagram

An interactive view tree: a DecorView containing a Content group and an Overlay, with Title and Button inside Content. Each node has an 'intercept' toggle for ViewGroups and a 'consume' toggle for any node. Pressing the toggles and reading the trace shows the dispatch order: ViewGroups are asked onInterceptTouchEvent first, children are offered the event in reverse draw order, and the first node to consume becomes the touch target. Toggling all consumes off results in no click; toggling the parent's intercept on routes the event to the parent instead.

The simulator is the rules made executable. Try three configurations and read the trace each time:

  1. Only the Button consumes. The Button becomes the target and the click fires.
  2. The Content group intercepts and consumes. The Button never sees the event; Content becomes the target.
  3. Nothing consumes. The trace shows every node returning false, and the gesture is dropped.

Deep dive#

The three methods#

// ViewGroup
override fun dispatchTouchEvent(ev: MotionEvent): Boolean
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean
 
// View (and ViewGroup, which can also consume)
override fun onTouchEvent(event: MotionEvent): Boolean

dispatchTouchEvent is the entry point. onInterceptTouchEvent exists only on ViewGroup. onTouchEvent exists on everything. The default View.onTouchEvent returns true only if the view is clickable; a plain view with no click listener ignores touches.

The DOWN descent#

On ACTION_DOWN, ViewGroup.dispatchTouchEvent does this:1

  1. Ask itself: should I intercept? (onInterceptTouchEvent)
  2. If not, iterate children in reverse draw order — topmost first — and call dispatchTouchEvent on each.
  3. The first child that returns true becomes mFirstTouchTarget.
  4. If no child takes it, call the group's own onTouchEvent.

Reverse draw order is the key detail. The view drawn last is the one visually on top, so it must get the first chance to handle the touch. This is why an overlay sibling above a button steals its touches.

The MOVE and UP ascent#

Once a target exists, subsequent events for the gesture go directly to it. ViewGroup.dispatchTouchEvent checks mFirstTouchTarget and, if set, calls dispatchTransformedTouchEvent on that target rather than re-traversing.

This is why changing the tree mid-gesture is dangerous: the target is a specific view, and if it is removed, the gesture has nowhere to go. The framework sends ACTION_CANCEL when the target is detached.

Intercepting mid-gesture#

A ViewGroup may decide to intercept after the gesture has started — a ScrollView that detects a vertical drag, for example. When it does:

  1. The current target receives ACTION_CANCEL.
  2. The ViewGroup becomes the target for the rest of the gesture.
  3. Any state the child had built up for the gesture should be reset.

This is the mechanism behind every "the parent steals the scroll" behaviour, and the reason child views must handle ACTION_CANCEL correctly. A child that only resets on ACTION_UP will be left in a stuck state.

performClick and the click contract#

View.performClick() is called from View.onTouchEvent on ACTION_UP if:

  • The view is clickable.
  • The view received and consumed ACTION_DOWN.
  • The pointer was still within the view's bounds at ACTION_UP (or the view has isPressed state consistent with that).

If you override onTouchEvent and consume events yourself, you must call performClick() yourself or the click listener and accessibility click action will not fire. This is why custom views often override performClick() and call super.performClick().

override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> return true
        MotionEvent.ACTION_UP -> {
            if (isInside(event)) performClick()
            return true
        }
    }
    return super.onTouchEvent(event)
}
 
override fun performClick(): Boolean {
    super.performClick()
    // your action
    return true
}

Multi-touch and pointer IDs#

A gesture is not necessarily one pointer. Each finger gets a pointer ID, and the event stream includes ACTION_POINTER_DOWN and ACTION_POINTER_UP for additional fingers. getActionMasked() strips the pointer index from the action, and getActionIndex() tells you which pointer changed.

The dispatch rules do not change: the target is chosen on the first ACTION_DOWN, and every subsequent event for that gesture — including additional pointers — goes to the same target. A view that wants per-pointer behaviour must track pointer IDs itself.

Compose's model#

Compose does not use onInterceptTouchEvent. Its pointer input system is a tree of PointerInputModifierNodes, and "interception" is expressed by consuming a change at a node, which makes it invisible to ancestors. The mental model is inverted — consumption propagates upward rather than an explicit intercept propagating downward — but the observable behaviour is similar: the first consumer at the deepest relevant node wins.

Accessibility is dispatched differently#

An accessibility click does not travel through the touch pipeline at all. It calls performAccessibilityAction on a node, which the view maps to performClick() or its own action handling.2 This is why a custom view that consumes touches but never calls performClick() is also unusable with TalkBack: the same missing call breaks both paths.

Why the tree is traversed in reverse draw order#

Children are stored in draw order: index 0 is drawn first, at the bottom; the last index is drawn last, on top. The dispatch loop runs from the last index down, so the topmost child gets the first chance to handle the event.

This is the same rule that makes a FrameLayout with two overlapping children behave intuitively: the one declared last, drawn on top, receives the touch. An app that wants a transparent overlay to not steal touches must either make it not clickable or set its visibility so it does not participate.

What mFirstTouchTarget actually is#

ViewGroup keeps a linked list of touch targets, normally with one entry. The list exists because a single gesture can have multiple pointers down at once, and in some configurations different children can own different pointers.

For almost all app code, the list has one entry and the behaviour is "the child that consumed DOWN". The name matters because it appears in traces and in dumpsys output when debugging why a touch went to the wrong place.

The pressed state and the click#

View maintains a pressed state derived from the touch. On ACTION_DOWN inside the bounds it becomes pressed, and on ACTION_UP inside the bounds the click fires and the state clears. On ACTION_CANCEL, or on ACTION_UP outside the bounds, the state clears without a click.

That is why a button dragged off and released does not click: the up event is outside the bounds, so the click is suppressed. Custom views that implement their own touch handling must reproduce that behaviour or they will feel different from every other control on the screen.

Nested scrolling, and why it is a separate protocol#

A RecyclerView inside a NestedScrollView is two scrollable views in a parent-child relationship. The touch dispatch rules alone cannot express "the child scrolls until it reaches its end, then the parent takes over" — the parent would have to intercept, which would cancel the child's gesture.

Nested scrolling is a separate protocol layered on top: the child reports its scroll to the parent through NestedScrollingChild, and the parent can consume the unconsumed part through NestedScrollingParent. It is a cooperative extension of the dispatch model rather than a replacement for it.

Testing dispatch#

The dispatch rules are deterministic, which makes them testable without a device. Espresso performs clicks through the accessibility action path rather than raw touch, so it exercises performClick — a useful reminder that a view which is only clickable through touch is not clickable at all for a large class of users.

For raw touch, an instrumentation test can dispatch a MotionEvent sequence directly to the root view and assert which handler fired. That is the fastest way to test a custom gesture recogniser without a physical device.

How it evolved#

API 1 →Basic dispatchthe three-method modelAPI 1
API 8 →Multi-touchpointer IDs and pointer actionsAPI 8
API 28 →Gesture navigationsystem edge gestures compete with app gesturesAPI 28
API 33 →Predictive backback gestures become interactiveAPI 33
Interface changes16 releases
APIVersionWhat changed
11.0
  • Pull-down notification shade
  • Home screen with widgets
31.5 Cupcake
  • Home screen widgets and folders
92.3 Gingerbread
  • New UI with dark theme and green accents
113.0 Honeycomb
  • The Holo design language arrives
123.1 Honeycomb
  • Resizable widgets
164.1 Jelly Bean
  • Project Butter: vsync-driven, smooth UI
  • Expandable notifications
204.4W KitKat (Wear)
  • Voice-driven card UI
215.0 Lollipop
  • Material Design
  • Notifications on the lock screen
247.0 Nougat
  • Quick Settings tiles
  • Direct Reply in notifications
268.0 Oreo
  • Notification channels
289 Pie
  • Dark theme groundwork
2910
  • System-wide dark theme
3011
  • Conversation notifications and bubbles
  • Media controls in Quick Settings
3112
  • Material You dynamic colour
3313 Tiramisu
  • Themed app icons
  • Runtime notification permission
3616 Baklava
  • Material 3 Expressive
  • Live Updates notifications

Under the hood#

Curiosity corner#

  • onInterceptTouchEvent is not called for every event. Once a group has a touch target, it is still asked on subsequent events, but the default implementation returns false and most groups never override it.
  • A click can fire without a touch. performClick() is public, and accessibility services call it directly.
  • The dispatch rules predate Android. They are close to the Java AWT/Swing event model, which is not a coincidence: early Android UI was built by people who had worked on desktop toolkits.

Try it#

Dump the view hierarchy
adb shell dumpsys activity top | grep -A20 "View Hierarchy"
Example output
DecorView
LinearLayout
FrameLayout
TextView (id/title)
Button (id/action)
Trace the dispatch path
adb shell perfetto -o /data/misc/perfetto-traces/touch -t 5s -a com.example.app input view
Example output
viewroot_dispatch → ViewGroup.dispatchTouchEvent → View.onTouchEvent

Check yourself#

Check yourself

1 / 12

Why does the system deliver input to an app through a socket pair rather than a Binder call?

0 answered · 0 correct

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsHandle input eventsdeveloper.android.comaccessed 2026-10-27↩
  2. docsAccessibility on Androiddeveloper.android.comaccessed 2026-10-27↩
  3. docsInput overviewsource.android.comaccessed 2026-10-27↩
  4. docsCreate a view layoutdeveloper.android.comaccessed 2026-10-27↩
  5. docsWinscopesource.android.comaccessed 2026-10-27↩