Tuesday, May 5, 2015

Order of Touch Event Dispatch in Android

Until very recently, I couldn’t say that I well understand Android’s UI event handling. Chances are that one won’t need to understand the very details to meet daily needs.

However, I eventually encountered a case of “one ScrollView inside another ScrollView” which forces me to figure out the arcane relationship between ViewGroup.onInterceptTouchEvent() and ViewGroup.requestDisallowInterceptTouchEvent(). Fortunately, this is a question many other has already asked. Thanks to technical blog post such as this one (and sincerely the Android documentation about this is not bad at all), I don’t need to learn it from trials and falls.

There is one nuisance though, that has confused me for a while, the answer of which isn’t in a readily available post from StackOverflow. That is what is the order in which Android dispatch touch events, when there are overlapping views. Overlapping is the key word here and this is the topic of today’s post.

This Simple Case

Throughout our discussion, let’s ignore the onInterceptTouchEvent() calls and focus only on onTouchEvent() calls, i.e. focusing on the event bubbling phase in web term.

When there is no overlapping views, touch dispatching is straightforward. Suppose we have a view hierarchy as follows:
then the order that onTouchEvent() is called is: C => B => A. That is, the target View (i.e. the innermost View that the touch lands on) received the call to onTouchEvent() first, followed by its parent, so on and so forth.

The Overlapping Case

Now consider the simplest case of View overlapping. Suppose there are two leaf Views C and D which has an overlapping region and that the touch event lands on that region (indicated by red circle) :
It’s not hard to guess that the order of onTouchEvent() call is: (C, D) => B => A. I am using the notion (C, D) to indicate that the order between C and D could vary. For all framework ViewGroup, such as FrameLayout, the order is determined by the child order. For instance, if C is the first child View of B and D is the second child View of B, then D’s onTouchEvent() is called first. This matches the drawing behavior that child View with lower index is drawn later, thus on top of, child View with higher index. Therefore, the child View that is drawn on top is also the one that handle the touch event first. One can redefine the drawing order via ViewGroup.getChildDrawingOrder(), which will affect the touch event dispatching order as well (if dispatchTouchEvent() is not overridden).

The General Case

To generalize, rather than giving result first, I’ll resort to another example, as shown below:
The View hierarchy is provide below for clarity:
In the View hierarchy above, child View to the left has lower index.

The touch location, which is indicated by the red circle again, is the overlapping region of all the child Views C, D, F and G. To verify the order of onTouchEvent() calls, I made a sample app with the above layout and attached a OnTouchListener to each of the Views that output some log. The app looks like the following:
Screenshot_2015-05-06-00-27-41.png

Here is the log I got from touching the region indicated by the red circle:
V/OVERLAPPING_VIEW_TEST﹕ ----View G.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ ----View F.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ --ViewGroup E.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ ----View D.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ ----View C.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ --ViewGroup B.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ ViewGroup A.onTouchEvent()

Here is the result of touching the region indicated by the green circle:
V/OVERLAPPING_VIEW_TEST﹕ ----View D.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ ----View C.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ --ViewGroup B.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ ViewGroup A.onTouchEvent()

And here is the result for the blue circle:
V/OVERLAPPING_VIEW_TEST﹕ ----View F.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ --ViewGroup E.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ ----View C.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ --ViewGroup B.onTouchEvent()
V/OVERLAPPING_VIEW_TEST﹕ ViewGroup A.onTouchEvent()

To summarize, here are the steps to determine the order of onTouchEvent() calls:
  1. Draw the View hierarchy tree.
  2. Remove from the tree any node that the touch location is not on.
  3. Do a post-order traversal where child View with higher index takes precedence.

Of course, any participate can return true from onTouchEvent() to stop the dispatching.

No comments:

Post a Comment