Understanding Android Input Touch Events System Framework (dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent, OnTouchListener.onTouch)

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.

Compilation

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:

What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.

… 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.

Activity.dispatchTouchEvent()
  • Always first to be called.
  • Sends event to the root view attached to the window.

Now if Activity.dispatchTouchEvent() returns 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.

Before we move on, note that dispatchTouchEvent() is defined on Activity, View and ViewGroup. It is responsible for routing the touch events to where they should go (down the hierarchy).

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 ViewGroup.dispatchEvent() (not 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 dispatchTouchEvent().

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:

ViewGroup.dispatchTouchEvent()
  1. onInterceptTouchEvent()

    • 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.
  2. 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.dispatchTouchEvent() or even previous_child.onInterceptTouchEvent()) then pass on to the next child view.
  3. 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:

Android Touch System Intercept Example

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) A.dispatchTouchEvent() to B.dispatchTouchEvent() (ViewGroup to ViewGroup) we already know what happens from B.dispatchTouchEvent() to C.dispatchTouchEvent() (ViewGroup to View).

So, it’s time to look at how C.dispatchTouchEvent() works:

View.dispatchTouchEvent()

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 (OnTouchListener.onTouch()) or onTouchEvent() at each level.

So if C.onTouchEvent() returns false, then it goes back up to B.onTouchEvent(). If that returns false then it goes back up to A.onTouchEvent().

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 false or 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).

Android Touch System Interested View Example

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.

Android Touch System Ignorant Example

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 true.

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 super.dispatchTouchEvent() or 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 B.dispatchTouchEvent() returns true then the value for super.dispatchTouchEvent() in 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!

References

Author: Rishabh

Rishabh is a full stack web and mobile developer from India. Follow me on Twitter.

1 thought on “Understanding Android Input Touch Events System Framework (dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent, OnTouchListener.onTouch)”

  1. hey,when I learn Android Touch System ,I have a question about this,I find in activity’s dispatchTouchEvent,if it call super.dispatchTouchEvent(ev),whatever what it is return,everything is not change?this is my test code:


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    textview = (TextView) findViewById(R.id.test);

    textview.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_MOVE) {
    Log.i("TAG", "MOVE");
    }else if(event.getAction() == MotionEvent.ACTION_DOWN){
    Log.i("TAG","DOWN");
    }
    return false;
    }
    });

    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.i("TAG", "Activity's dispatchTouchEvent");
    super.dispatchTouchEvent(ev);
    return false;
    // return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    Log.i("TAG", "Activity's onTouchEvent");
    return super.onTouchEvent(event);
    }

    Can you explain why? thank you

Leave a Reply

Your email address will not be published. Required fields are marked *

*