On my journey of learning Android Development, I’ve realised that it is really important to understand the input handling especially how the entire touch events framework work. Wrapping your head around the entire propagation of touch events (including the gesture) is imperative else while coding you’ll find yourself stumped by the behaviour of different ViewGroups (or Views) when attaching touch listeners to them with the hope that your code works as expected when they actually won’t. It gets even tricker when you have ViewGroups like ViewPager or ListView (both scrollable) inside other scrollable ViewGroups like ScrollView.
Instead of explaining the entire system myself right off the bat, I’ll compile a list of kickass resources that does the job much better than I could probably do. Later I’ll try to amalgamate essential pieces from them with my understanding (based on a few tests I did) to sum it up:
- Propagation of Events section of this article (my favourite!) – http://balpha.de/2013/07/android-development-what-i-wish-i-had-known-earlier/
- Android Documentation on Input Events in general – http://developer.android.com/guide/topics/ui/ui-events.html
Excellent StackOverflow thread on how
dispatchTouchEvent()work – http://stackoverflow.com/q/9586032
- Pierr Chen’s flowchat-based explanation with interesting insights – http://pierrchen.blogspot.in/2014/03/pipeline-of-android-touch-event-handling.html
- Newcircle’s Mastering the Android Touch System talk (with slides) – https://thenewcircle.com/s/post/1567/mastering_the_android_touch_system – This one is really insightful. Must watch!
- Managing Touch Events in a ViewGroup Android training resource – http://developer.android.com/training/gestures/viewgroup.html
… and of course the Android source code if you’re daring enough!
Android Touch System Event Propagation
Based on the information gathered from those links and some tests I did by writing a couple of custom Views, I’ll try to describe the core flow (making the entire concept easier to comprehend).
When a touch is triggered on the screen (ACTION_DOWN) by the Android hardware (and of course our fingers) the first place where it is accessible is the
Activity.dispatchTouchEvent() method. It is always first to be called. Then it sends the
MotionEvent object passed to it, to the root view (which is the DecorView I believe) attached to the window.
- Always first to be called.
- Sends event to the root view attached to the window.
true then it’ll consume all the subsequent gesture events (MOVE/UP). Whereas on returning
false it’ll never be called again for subsequent events.
The root view starts dispatching the event down to its children. Let’s presume that we have this hierarchy:
- A – ViewGroup1 (parent of B).
- B – ViewGroup2 (parent of C).
- C – View (child of B) – receives a touch/tap/click.
Now the root view will call
A.dispatchTouchEvent(). Now the job of a
View.dispatchEvent()) is to find out all the child views and view groups whose bounds contain the touch point coordinates (using a hit testing algorithm). When it figures out a list of relevant children, it starts dispatching the events to them by calling their
Here’s an important piece though. Before the
dispatchTouchEvent() is called on the children, the
A.dispatchTouchEvent() will first call
A.onInterceptTouchEvent() to see if the view group is interested in intercepting the event and handling the subsequent gesture by itself (scrolling is a good use case where a fling on B should lead to scrolling on A). The method
onInterceptTouchEvent() is only available on view groups (as they’re the one who can be parents/containers with the requirement to intercept touch events) that can sort of keep an eye on the event and hijack it by returning
true. If it returns
false then dispatching continues as usual, i.e.,
B.dispatchTouchEvent() (child) will be called. But on returning
true, this is what’ll happen:
- ACTION_CANCEL will be dispatched to all the children.
- All the subsequent gesture events (till ACTION_UP/ACTION_CANCEL) will be consumed by the event listeners (
OnTouchListener.onTouch()) if defined, else the event handler
A.onTouchEvent()at A’s level.
A.onInterceptTouchEvent()itself will be never called again.
So here’s the exact flow:
- Check if it should handle the event.
- If yes (returns
true), then ACTION_CANCEL will be dispatched to the children (by
dispatchTouchEvent()) and subsequent events will be consumed by the same ViewGroup.
If onInterceptTouchEvent() doesn’t want to handle the event and returns false, then for each child view, in the reverse order they were added.
- Call the
child.dispatchTouchEvent()if the touch is relevant (inside the view).
- If not handled by previous (by returning true from
previous_child.onInterceptTouchEvent()) then pass on to the next child view.
- Call the
- If no children handles the event, then OnTouchListener.onTouch() gets the chance if defined, else onTouchEvent() does.
Once intercepted, all the children will get ACTION_CANCEL and the event will bubble up from that (intercepted) level itself. Here’s an illustration to explain it better:
onInterceptTouchEvent() for the ViewGroup (ScrollView for example) return true affecting the dispatch chain. Hence,
View.dispatchTouchEvent() (and then from it,
View.onTouchEvent and other listeners if exists) get ACTION_CANCEL events to terminate the dispatching of events to them.
Now that we’ve understood what happens from (or between)
B.dispatchTouchEvent() (ViewGroup to ViewGroup) we already know what happens from
C.dispatchTouchEvent() (ViewGroup to View).
So, it’s time to look at how
- Sends event to
View.OnTouchListener.onTouch()event listener firsts, if exists.
- If not consumed (
truereturned by one of the event listeners), processes the touch itself by calling
Now if the View’s event listeners or handler returns
true, then that becomes the destination for all subsequent events. But if one of them returns
false then the events start bubbling up the hierarchy and calling the event listeners (
onTouchEvent() at each level.
false, then it goes back up to
B.onTouchEvent(). If that returns
false then it goes back up to
If any of the level returns
true then you know what happens. It’ll get all the successive events and
onInterceptTouchEvent() will also not be called if it’s a view group. But if every level returns
false then the events ends up in
Activity.onTouchEvent(). This is super important to remember.
Activity.onTouchEvent() is the last place to catch hold of events in. I don’t think it matters whether you return
true from this method, this will always get the ensuing events if no one else handles (at least what I observe from my tests for now).
So you see how interesting this is. While coming down the chain, the event propagates through
dispatchTouchEvent() at every level. While going back up, it bubbles up through
onTouchEvent() (and/or event listeners if defined) at every level. While coming down, at every level that is a ViewGroup there’s an additional check done at
onInterceptTouchEvent(). Now this check can definitely be prevented if
true is passed to
requestDisallowInterceptTouchEvent() on the view group’s object. This way the child can prevent its parent (the view group) and ancestors to intercept touch events with
onInterceptTouchEvent(). It is suggested that if you really have to, then pass
true on an ACTION_DOWN/ACTION_MOVE but then switch back to default behaviour on ACTION_UP/ACTION_CANCEL by passing
false – else you could end up into weird behaviour (especially with scrollable view groups inside another one that is scrollable) that might consume quite some time to debug.
Another important thing to understand is what
dispatchTouchEvent() returns and how it affects the entire system, especially when coding custom Views. If this method returns
false then that means the event wasn’t/isn’t handled by this view/view group, hence the following events will not be passed to it anymore. So for instance if a ScrollView or ListView (or your custom view extending these classes) wants its scrolling to work properly then there
dispatchTouchEvent() must return
Now when extending a class like ScrollView or ListView to write your own custom View, you’ll most probably delegate the task of dispatching the event to children, scrolling and all sorts of handling involved behind the scenes to
super.dispatchTouchEvent() in your own version of
CustomView.dispatchTouchEvent(). It might be helpful to understand that this
dispatchTouchEvent() in general is meant to return the value bubbled up by the underlying chain. That clearly indicates whether the ViewGroup (or one of its children at any level) actually handles the event or not because if the bubbled up value is
true then the
dispatchTouchEvent() calls will keep on flowing to (and through) this ViewGroup till the level where the event was either intercepted or one of the touch event handlers/listeners returned
true or the
dispatchTouchEvent() at that level simply returned
true for further gestures (ACTION_MOVE/UP).
Hence in our example, if
true then the value for
A.dispatchTouchEvent() will also be true and that is exactly what A should return (with some code like
return super.dispatchTouchEvent()) in order for the further events to go down this chain. Now if
B.dispatchTouchEvent() returns false because it didn’t handle the event then
A.dispatchTouchEvent() will also return the same by
super.dispatchTouchEvent() unless the last call (in the entire event propagation) to
A.onTouchEvent() returns true which’ll make
A.dispatchTouchEvent() also return
true.Hence A will be responsible for consuming further events.
Here’s a stack trace generated by calling
Thread.dumpStack() from the
onTouchEvent of a custom list view I wrote by extending ListView, to help you get a better insight of the events chain to a certain extent.
: java.lang.Throwable: stack dump : at java.lang.Thread.dumpStack(Thread.java:489) : at com.pycitup.pyc.CustomList.DisabledListView.onTouchEvent(DisabledListView.java:60) : at android.view.View.dispatchTouchEvent(View.java:7706) : at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2210) : at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1945) : at com.pycitup.pyc.CustomList.ScrollDisabledListView.dispatchTouchEvent(ScrollDisabledListView.java:34) : at com.pycitup.pyc.CustomList.DisabledListView.dispatchTouchEvent(DisabledListView.java:36) : at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) : at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) : at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) : at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) : at com.pycitup.pyc.CustomList.CustomScrollView.dispatchTouchEvent(CustomScrollView.java:40) : at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) : at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) : at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) : at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) : at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2216) : at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1917) : at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2068) : at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1515) : at android.app.Activity.dispatchTouchEvent(Activity.java:2458) : at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2016) : at android.view.View.dispatchPointerEvent(View.java:7886) : at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:3947) : at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:3826)
I might have been really verbose but I guess that was important to cover all sorts of cases in which touch events propagate in an Android application. Hope this will give you a firm understanding!
- Images have been taken from the deck here.