Android Touch Event Dispatch: A Deep Dive

举报
yd_223002268 发表于 2026/08/27 08:43:33 2026/08/27
【摘要】 Android Touch Event Dispatch: A Deep Dive IntroductionEvery time you tap a button, swipe a list, or pinch to zoom, a chain of method calls decides which view finally consumes that gesture. Underst...

Android Touch Event Dispatch: A Deep Dive

Introduction

Every time you tap a button, swipe a list, or pinch to zoom, a chain of method calls decides which view finally consumes that gesture. Understanding this chain — the Android touch event dispatch mechanism — is essential for building custom views, nested scrolling, and any UI where gestures compete. Get it wrong and your scrolling container eats your child’s click, or your custom view swallows every touch in the region.

This article unpacks the full dispatch flow, the three core methods, the sequence of calls, and the common patterns and pitfalls you will meet in real apps.


1. The MotionEvent

Touch input arrives as MotionEvent objects. Each event carries an action code and coordinates:

  • ACTION_DOWN — a finger has touched the screen. This is the start of a touch sequence.
  • ACTION_MOVE — the finger has moved. Many of these follow a DOWN.
  • ACTION_UP — the finger has lifted. This ends the sequence.
  • ACTION_CANCEL — the sequence was aborted, typically because a parent decided to intercept.
  • ACTION_POINTER_DOWN / ACTION_POINTER_UP — a secondary finger joined or left (multi-touch).

A single touch gesture is always a sequence beginning with one ACTION_DOWN, followed by zero or more ACTION_MOVEs, and ending with either ACTION_UP or ACTION_CANCEL. This sequence property is the foundation of the whole dispatch model: the view that consumes the DOWN is the one that will receive the rest of the sequence, unless a parent intercepts.


2. The Three Core Methods

The dispatch mechanism is built on three methods, each with a clear role. Every View and ViewGroup participates.

2.1 dispatchTouchEvent(MotionEvent) — boolean

This is the entry point. When a touch event reaches a view or view group, dispatchTouchEvent is called first. It is responsible for deciding what happens next:

  • For a View, it calls onTouchEvent and returns its result.
  • For a ViewGroup, it asks each child (in reverse Z-order) whether it wants the event, and if none does, handles it itself.

The return value signals consumption: true means “I handled this event,” false means “I did not.”

2.2 onInterceptTouchEvent(MotionEvent) — boolean

This exists only on ViewGroup. It is the parent’s chance to steal the event from its children before they see it. Returning true means “I am intercepting — do not dispatch to children, send the rest of this sequence to my own onTouchEvent.”

Most of the time it returns false. The classic use case is a scrolling container that lets children handle taps but takes over once the finger moves far enough to look like a scroll.

Once a parent intercepts, the child currently receiving the sequence gets an ACTION_CANCEL so it can reset its state.

2.3 onTouchEvent(MotionEvent) — boolean

This is where a view actually reacts to the event — updating state, calling listeners, triggering animations. Returning true means the view consumes the event and wants the rest of the sequence. Returning false on ACTION_DOWN tells the parent “I am not interested,” and the parent will not deliver subsequent events to this view.


3. The Full Dispatch Sequence

Here is the precise order of calls when a MotionEvent arrives at the root of a view hierarchy. Assume a ViewGroup containing a child View.

3.1 ACTION_DOWN

ViewGroup.dispatchTouchEvent
  -> ViewGroup.onInterceptTouchEvent?  (returns falselet children try)
  -> child.dispatchTouchEvent
       -> child.onTouchEvent?          (returns true — child consumes)
  <- true
<- true

If the child returns true for the DOWN, it is recorded as the “touch target” for this sequence. All subsequent MOVE and UP events in this sequence will be delivered directly to the child, short-circuiting the child-search loop — but the parent still gets a chance to intercept on every event.

3.2 ACTION_MOVE (no interception)

ViewGroup.dispatchTouchEvent
  -> ViewGroup.onInterceptTouchEvent?  (returns false)
  -> child.dispatchTouchEvent          (delivered directly to the known target)
       -> child.onTouchEvent
  <- true
<- true

3.3 ACTION_MOVE (parent intercepts)

ViewGroup.dispatchTouchEvent
  -> ViewGroup.onInterceptTouchEvent   (returns true — parent steals)
  -> child.dispatchTouchEvent(CANCEL)  (child gets a chance to clean up)
  -> ViewGroup.onTouchEvent(MOVE)      (parent now handles the rest)
<- true

From this point on, the parent’s onTouchEvent receives the remaining MOVEs and the final UP. The child is out of the loop.

3.4 ACTION_UP

Delivered to whoever is the current target — the original child if no interception happened, or the parent if it intercepted. After UP, the touch target is cleared and the next DOWN starts a fresh sequence.


4. The Consumption Rules, Precisely

The behavior follows a few rules that, once internalized, make the whole system predictable:

  1. Only the view that consumes ACTION_DOWN receives the rest of the sequence. If a view returns false from onTouchEvent for DOWN, it will not see MOVE or UP for this gesture.
  2. A parent can intercept mid-sequence. onInterceptTouchEvent is called for every event after DOWN, not just the first one. This is how a scroll container waits for movement before deciding to take over.
  3. Interception sends ACTION_CANCEL to the child. The child receives exactly one CANCEL and then no more events from this sequence. Always handle CANCEL symmetrically with UP — release resources, reset pressed state, end animations.
  4. A child can request its parent not to intercept. Calling parent.requestDisallowInterceptTouchEvent(true) sets a flag that causes the parent to skip onInterceptTouchEvent for the rest of the sequence. This is how a child that is certain it wants the gesture (e.g., a button mid-press) can prevent a scroll parent from stealing it.
  5. Z-order matters. When multiple children overlap, the child drawn last (highest in the Z-order, last in the child list) is asked first. The first child to consume wins; siblings underneath never see the event.

5. Worked Example: A Scroll Container with Tappable Children

This is the scenario every Android developer eventually debugs. You have a vertical scroll container (ScrollView-like) holding buttons. You want taps to reach the buttons, but drags to scroll the container.

The container implements onInterceptTouchEvent roughly like this:

class VerticalScrollContainer @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {

    private var touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    private var downY = 0f
    private var isDragging = false

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        when (ev.actionMasked) {
            ACTION_DOWN -> {
                downY = ev.y
                isDragging = false
            }
            ACTION_MOVE -> {
                if (abs(ev.y - downY) > touchSlop) {
                    isDragging = true
                    return true // steal the sequence from the child
                }
            }
            ACTION_CANCEL, ACTION_UP -> {
                isDragging = false
            }
        }
        return false
    }

    override fun onTouchEvent(ev: MotionEvent): Boolean {
        // Handle actual scrolling here once isDragging is true
        return true
    }
}

The flow in practice:

  1. DOWN arrives. The container’s onInterceptTouchEvent records the start Y and returns false, so the event goes to the child button. The button consumes it (shows pressed state).
  2. MOVE events arrive. As long as the finger has not moved beyond touchSlop, the container keeps returning false and the button keeps receiving the moves — it still thinks it is being tapped.
  3. The finger moves past touchSlop. The container’s onInterceptTouchEvent returns true. The button receives an ACTION_CANCEL (it clears its pressed state). The container’s onTouchEvent takes over and scrolls.

This touchSlop threshold is critical. It is the platform’s way of distinguishing a tap from a drag, and it is tuned per device to match the touchscreen’s noise floor. Never hardcode a pixel value for this — always use ViewConfiguration.get(context).scaledTouchSlop.


6. requestDisallowInterceptTouchEvent

Sometimes the child knows best. A horizontally swipeable child inside a vertical scroll container wants to keep the gesture once it has started a horizontal drag, even though the vertical container might also want it.

The child calls:

parent.requestDisallowInterceptTouchEvent(true)

This sets a flag on the parent that causes onInterceptTouchEvent to be skipped for the remainder of the sequence. The flag is automatically cleared on the next ACTION_DOWN, so you typically call it from the child’s onTouchEvent when it detects the start of its own gesture.

This is the mechanism behind NestedScrollingChild and most nested-scrolling support: the inner view coordinates with the outer view rather than simply fighting over interception.


7. Nested Scrolling

The intercept/cancel model is blunt: when a parent intercepts, the child is fully cut off. This works for simple cases but breaks down for nested scrolling, where a parent and child need to cooperate — the child scrolls until it hits its edge, then the parent takes the overflow.

Android provides the nested scrolling APIs to solve this without fighting over interception:

  • NestedScrollingChild3 / NestedScrollingParent3 — the modern interfaces.
  • NestedScrollView — a ready-made implementation.

The child dispatches scroll deltas to the parent before consuming them itself, letting the parent consume the portion it wants. This cooperative model is how a vertically scrolling list inside a vertically scrolling parent (like a NestedScrollView containing a RecyclerView) produces the smooth handoff users expect.

If you are building a custom scrolling container that should nest inside others, implement NestedScrollingParent3 and delegate to a NestedScrollingParentHelper. If you are building a scrollable child, implement NestedScrollingChild3 and use a NestedScrollingChildHelper. The helpers handle the bookkeeping; you implement the actual scrolling.


8. GestureDetector and ScaleGestureDetector

For common gestures — tap, double tap, long press, fling, scroll, scale — do not roll your own detection in onTouchEvent. Use the platform detectors:

  • GestureDetector — detects taps, double taps, long presses, scrolls, and flings.
  • ScaleGestureDetector — detects pinch-to-scale.

These detectors consume MotionEvents and call back to listener methods with interpreted gestures. They handle the fiddly timing and threshold math correctly across devices.

class PinchZoomView(context: Context) : View(context) {
    private val scaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            scaleFactor *= detector.scaleFactor
            invalidate()
            return true
        }
    })

    override fun onTouchEvent(ev: MotionEvent): Boolean {
        scaleDetector.onTouchEvent(ev)
        return true
    }
}

A subtle point: a detector’s onTouchEvent returns true whenever it has a gesture in progress, but you should generally return true from your view’s onTouchEvent for the whole sequence once you have consumed the DOWN, so that you keep receiving the events the detector needs.


9. Common Pitfalls

9.1 Forgetting to Handle ACTION_CANCEL

If you track pressed state, animations, or any transient touch state in onTouchEvent, you must reset it on both ACTION_UP and ACTION_CANCEL. A child that only resets on UP will stay “pressed” forever after a parent intercepts, because it received CANCEL instead of UP.

override fun onTouchEvent(ev: MotionEvent): Boolean {
    when (ev.actionMasked) {
        ACTION_DOWN -> isPressed = true
        ACTION_UP, ACTION_CANCEL -> isPressed = false
    }
    return true
}

9.2 Consuming DOWN but Not the Rest

If you return true for ACTION_DOWN but false for ACTION_MOVE, you will receive the moves (because you consumed the down) but signal that you do not want them. The parent will then handle the moves itself, leading to split behavior where half the gesture goes to the child and half to the parent. If you consume the DOWN, commit to consuming the whole sequence unless you deliberately want to hand off.

9.3 Intercepting Too Early

If a parent’s onInterceptTouchEvent returns true on ACTION_DOWN, the children never see any event at all — the parent grabs the entire sequence from the start. This is almost never what you want for a container that should pass taps through. Intercept on MOVE, after you have evidence of a drag, not on DOWN.

9.4 Not Using touchSlop

Hardcoding a movement threshold in pixels makes your view feel inconsistent across devices with different densities and touch hardware. Always derive thresholds from ViewConfiguration.

9.5 Breaking the Z-Order Assumption

If you reorder children or change visibility during a touch sequence, the recorded touch target may point at a view that is no longer on top or even attached. Avoid mutating the view hierarchy during an active touch sequence. If you must, be prepared to clear state on CANCEL.

9.6 Returning false from dispatchTouchEvent Unintentionally

If you override dispatchTouchEvent and forget to call super.dispatchTouchEvent in some branch, you can silently drop events. Only override dispatchTouchEvent when you have a specific reason (e.g., forwarding events to a different view), and always defer to super for the cases you do not handle.


10. Debugging Event Dispatch

When gestures misbehave, a few techniques pin down the culprit:

  • Log the three methods. Add temporary logs in dispatchTouchEvent, onInterceptTouchEvent, and onTouchEvent at each level of the hierarchy, printing the action and the return value. The call sequence becomes immediately visible.
  • Check the touch target. In a ViewGroup, the field mFirstTouchTarget (accessible via reflection in debug, or inferred from behavior) tells you which child currently owns the sequence.
  • Use the hierarchy inspector. Overlapping or invisible siblings are a common cause of stolen events. Confirm which view is actually on top at the touch coordinates.
  • Verify requestDisallow flags. A stale disallowIntercept flag can make a parent stop intercepting when it should. The flag resets on DOWN, but if you set it manually outside the normal flow you can leave it in a surprising state.
  • Test on a real device. Emulators sometimes deliver touch events with different timing and precision than real hardware, masking or introducing timing-sensitive bugs.

11. A Mental Model to Keep

Think of the dispatch as a conversation with three phases:

  1. Delivery. The event walks down the hierarchy from the root, with each ViewGroup consulting onInterceptTouchEvent to decide whether to keep going or stop. The deepest view that consumes the DOWN becomes the target.
  2. Steady state. Subsequent events flow to the target, with parents still checking interception on each one. This is where mid-sequence interception happens.
  3. Resolution. Either the target gets UP and the sequence ends cleanly, or a parent intercepts and the target gets CANCEL while the parent takes over.

Every dispatch question reduces to: who is the target, who is trying to intercept, and who has called requestDisallowInterceptTouchEvent. Keep those three facts in mind and the behavior is always explicable.


Conclusion

The Android touch event dispatch mechanism is a deterministic set of rules, not a black box. Three methods — dispatchTouchEvent, onInterceptTouchEvent, and onTouchEvent — cooperate through a small set of conventions: consume the DOWN to own the sequence, intercept to steal mid-sequence, cancel to tell the child to reset, and disallow intercept to protect a gesture you have started.

Master these rules and you can build any gesture interaction with confidence: scroll containers that cooperate with their children, custom views that compete correctly for touches, and nested scrolling that feels native. The key is always to handle CANCEL, respect touchSlop, and let the platform detectors do the gesture math for you.

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。