Without a deep understanding of how Android views handles touches, a lot of touch behaviours seem confusing. Why is this button click not working? Why is that RecyclerView
not scrolling? How do I handle nested ScrollView
s?
This article covers how touch events flow through the view hierarchy, and how a few core functions affect the flow. Part 2: Common Touch Event Scenarios shows concrete, visual examples of how these functions.
Table of Contents
- Background Knowledge
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
- requestDisallowInterceptTouchEvent
Background Knowledge
MotionEvent
Every movement on the touch screen is reported as a MotionEvent object. The
MotionEvent
class has getters for accessing all the information associated with the event. Some commonly-used ones are:
action
(the type of action being performed, more on this later)x
(x-coordinate of touch, relative to the view that handled it)y
(y-coordinate of touch, relative to the view that handled it)rawX
(absolute x-coordinate of touch, relative to the device screen)rawY
(absolute y-coordinate of touch, relative to the device screen)eventTime
(the time the event occurred, in theSystemClock.uptimeMillis()time base)
Screen coordinates
As a reminder, Android screen coordinates are measured in pixels with x = 0 and y = 0 in the top left corner of the screen and the x = maxX and y = maxY in the bottom right corner.
Action
MotionEvent
’s getAction()
returns an Int
constant representing different action types. The full list can be found here, but here are some of the most common ones:
ACTION_DOWN
: When finger or object first comes in contact with the screen. The event contains the initial starting location of a gesture.ACTION_UP
: When finger or object lifts from the screen. Contains the final release location of a gesture.ACTION_MOVE
: Any movements in betweenACTION_DOWN
andACTION_UP
, when the finger’s final release location is different from the initial starting location.ACTION_CANCEL
: Current gesture has been aborted. It occurs when the parent view intercepts the event from its child, for example when the user has dragged enough on a scroll view that it starts scrolling instead of letting you press the buttons inside it.
A gesture is defined as a series of MotionEvent
s, starting with ACTION_DOWN
and ending with either ACTION_UP
or ACTION_CANCEL
. There may be multiple MotionEvent
s fired between the start and end; for example if you put your finger on the screen, make a dragging motion, then lift your finger, you’ll also trigger multiple ACTION_MOVE
events.
How Android handles MotionEvents
When a motion event occurs, it flows top-down through the view hierarchy, starting from the root of the view tree (eg. Activity
) and, if not intercepted, going all the way to the leaf view where the event happened (eg. Button
). On the way down, the views’ dispatchTouchEvent()
s are called. Views can intercept the event by overriding onInterceptTouchEvent()
. If onInterceptTouchEvent()
returns true
, it means the event was consumed and won’t be passed onto descendent views. If it returns false
, it means the view acknowledged the event but continued passing it down.
Then after the event either reaches the leaf view or a view that intercepts and consumes it, it flows back up the chain until it’s consumed. On the way up, onTouchEvent()
is called instead. If a view’s onTouchEvent()
returns true
, the event stops there. Any unconsumed events end at Activity’s onTouchEvent()
. So the Activity is the first to have its onInterceptTouchEvent()
called, and the last to have its onTouchEvent()
called.
Here’s a visualization of what happens when there’s a touch on View B and none of the views handle the event:
Job Offers
Now let’s look at each function in detail, and how they’re implemented in View
, ViewGroup
(subclass of View
), ScrollView
(subclass of ViewGroup
), and Activity
.
dispatchTouchEvent()
This is the first function to be called when a motion event happens. It returns true
if the event was consumed by the view, false
otherwise.
View.dispatchTouchEvent
Views don’t have any children, so the dispatchTouchEvent()
implementation is simple; it calls onTouchEvent()
and any touch listeners set on the view, and returns true
if any of them return true to say that the event was consumed.
Since dispatchTouchEvent()
does some additional state management and calls onTouchEvent()
immediately anyway, it’s recommended that custom views override onTouchEvent()
rather than dispatchTouchEvent()
, to avoid changing the default state management.
ViewGroup.dispatchTouchEvent
On a ViewGroup
, it calls onInterceptTouchEvent()
. If onInterceptTouchEvent()
returns false
, it iterates through its child views in reverse order in which they were added. If the touch is inside the current child view, it calls child.dispatchTouchEvent()
. If the child view returns false
, signifying it didn’t consume it, the view group calls child.dispatchTouchEvent()
on the next child.
It’s recommended that custom ViewGroup
s override onInterceptTouchEvent()
rather than dispatchTouchEvent()
, since onInterceptTouchEvent()
is intended for spying on touch events whereas dispatchTouchEvent()
does some additional state management.
ScrollView.dispatchTouchEvent
Same as ViewGroup
’s; doesn’t override the function.
Activity.dispatchTouchEvent
It calls dispatchTouchEvent()
on its children. Note that Activity
doesn’t provide an onInterceptTouchEvent()
, so overriding it in a custom activity is the only way to ensure it consumes touch events. Otherwise, if any child returns true in onTouchEvent()
, the activity’s onTouchEvent()
won’t be called.
onInterceptTouchEvent()
View.onInterceptTouchEvent
Doesn’t have it 🤷♀️
ViewGroup.onInterceptTouchEvent
For all intents and purposes, the default implementation returns false
. (The only exception is if the device is connected to a mouse as input and the user scrolls the mouse while focused on the ViewGroup
.)
The main purpose of overriding this method is to let the ViewGroup
handle a certain type of touch event while letting the child handle another type. For example, a ScrollView
overrides it to handle scrolling while letting its child handle something like a click.
ScrollView.onInterceptTouchEvent
ScrollView
overrides ViewGroup
’s onInterceptTouchEvent()
. If the event is an ACTION_MOVE
, it checks if the event has enough velocity in a supported direction to be considered a drag. If it does, the view returns true
and its children receive ACTION_CANCELLED
. The function also calls requestDisallowInterceptTouchEvent()
, meaning ancestor views’ onInterceptTouchEvent()
are ignored and the scroll takes precedence over whatever the ScrollView
’s ancestors want to do on touch.
Activity.onInterceptTouchEvent
Doesn’t have it 🤷♀️
onTouchEvent()
View.onTouchEvent
The default implementation returns true
if the view is clickable, but doesn’t do much beyond updating a few flags. The Android documentation recommends calling super.onTouchEvent()
when overriding this in a custom view, since it does some state management. The documentation also recommends overriding performClick()
instead if you only intend to handle click gestures.
ViewGroup.onTouchEvent
Same as View
’s; doesn’t override the function.
ScrollView.onTouchEvent
The onTouchEvent()
uses the event’s information to figure out how much to scroll the view and performs the scroll. It also handles any animations associated with scrolling, for example overscroll effects.
This function also calls requestDisallowInterceptTouchEvent()
.
Activity.onTouchEvent
The default implementation always returns false
.
Summary Table
requestDisallowInterceptTouchEvent()
This is a function on the ViewParent
interface, to be used when a child view does not want its parent and its ancestors to intercept touch events. ViewGroup
implements ViewParent
.
The ViewParent
and its ancestors must obey this request for the duration of the gesture, meaning any interceptors in the view’s ancestors will be disallowed starting from ACTION_DOWN
until ACTION_UP
or ACTION_CANCEL
. You’ll have to call requestDisallowInterceptTouchEvent()
again for each new gesture.
If you don’t want to give the view’s ancestors a chance to handle the event, the view’s onTouchEvent()
must return true
so its ancestors’ onTouchEvent()
don’t get triggered when the event flows back up.
For example, ScrollView
calls this inside is onInterceptTouchEvent()
and onTouchEvent()
if it detects a scroll.
References
The main resources I used for this article are:
- Mastering the Android Touch System talk
- Mastering the Android Touch System article
- The source code for MotionEvent, View, ViewGroup, ScrollView, and Activity
To see concrete examples of how these functions work, please continue to Part 2: Common Touch Event Scenarios.
Thanks to Russell and Kelvin for their valuable editing and feedback ❤️