I think many of us have written code like:
@Override public boolean onTouch(View view, MotionEvent event) { final float x = event.getX(); final float y = event.getY();
But, I think, not many people thought about what path each MotionEvent object passes before getting into this method. In most cases, this is not necessary, but there are still situations where ignorance of the features of MotionEvent and touch processing leads to sad results.
A year ago, I developed an application with friends, where a lot of things rested on touch processing. Once, downloading new sources from the repository and assembling the application, I found that the vertical coordinate of the touch is not determined correctly. Looking through the last commits of the command, I came across an interesting line, where 100 were suddenly taken from the y-coordinate. That is, something like “y - = 100;”, moreover, this number was not pronounced as a constant and it was not clear why 100. To my obvious question, I received the answer "Well, we empirically determined that at this point the y-coordinate is always 100 (pixels) larger than it should be." Here, of course, it would be worth re-reading the touch processing documentation and, after reviewing the project code, find the error, but I decided to go in a more interesting way - to follow the Android source for MotionEvent from its receipt to disposal.
')
If I could intrigue someone with a story in the style “In the footsteps of the striped bug” - welcome under cat.
Morality
First, make sure that storing a MotionEvent that came to us with onTouch is bad. I used a small test application with the following code:
package com.alcsan.test;
We start the application, several times we tap into one point under the ActionBar and look in the logs. Personally, I received the following picture: "32.0", "41.0 41.0", "39.0 39.0 39.0", "39.0 39.0 39.0 39.0". That is, after the first call we saved an object with y = 32 in the history, but after the next pressing of this object y is equal to 41, and an object with the same y is recorded in the history. In fact, this is all the same object that was used during the first onTouch call and reused during the second call. Therefore, the moral is simple: do not store MotionEvent, obtained in onTouch! Use this object only within the onTouch method, and for other needs, retrieve the coordinates from it and store them in PointF, for example.
Android Sources - MotionEvent Pool
And now I suggest to look into the rabbit hole of the Android source and determine why MotionEvent behaves in this way.
First, already from the behavior of the test application, it is clear that MotionEvent objects are not created at each touch, but reused. This is done because there can be many touches in a short period of time and the creation of many objects would degrade performance. At least due to the increased collection of garbage. Imagine how many objects would be created per minute of the game in Fruit Ninja, because events are not only DOWN, UP and CANCEL, but also MOVE.
The logic of working with the MotionEvent object pool is in the MotionEvent class -
grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/android/view/MotionEvent.java . The static methods and variables are associated with the pool here. The maximum number of simultaneously stored objects is defined by the MAX_RECYCLED constant (and it is equal to 10), the stored objects counter is gRecyclerUsed, and gRecyclerLock is used for synchronization and provision of work in asynchronous mode. gRecyclerTop - the head of the list of objects left for recycling. And there is also a non-static variable mNext, as well as mRecycledLocation and mRecycled.
When an object is needed for the system, the static method is obtained (). If the pool is empty (gRecyclerTop == null), a new object is created and returned. Otherwise, the last reclaimed object (gRecyclerTop) is returned, and the second to last object takes its place (gRecyclerTop = gRecyclerTop.mNext).
For recycling, call recycle () on the recycled object. It takes the place of the “last added” (gRecyclerTop), and the link to the current “last” is stored in mNext (mNext = gRecyclerTop). This all happens after checking for pool overflow.
Android Sources - MotionEvent Processing
We won't dive too deep and start with the handleMessage (Message msg) method -
grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/android/view/ViewRoot. java? av = f # 1712 - ViewRoot class. Here comes the finished MotionEvent (received by the system via MotionEvent.obtain ()) wrapped in a Message. The method, by the way, serves to handle not only touches, but also other events. Therefore, the body of the method is a big switch, in which we are interested in lines from 1744 to 1847. Here the event is pre-processed, then mView.dispatchTouchEvent (event), then the event is added to the pool: event.recycle (). The dispatchTouchEvent (...) method raises a listener event, if any, and tries to delegate event handling to the internal View.
Bug tracks
And now briefly about what the bug was.
First, a little about what exactly did with MotionEvent in that project. After receiving the object, the application saved it into a variable, waited a certain number of milliseconds and processed it. This behavior was associated with gestures: roughly speaking, if the user touched the screen and held up a finger for a second, show him a certain dialogue. The application received an ACTION_DOWN event and, without receiving ACTION_UP or ACTION_CANCEL events for a second, it responded. Moreover, it reacted on the basis of the initiating MotionEvent. Thus, the reference to him lived for some time, during which several other touch events could occur.
The following happened consistently:
1. The user touched the screen.
2. The system received a new object using the MotionEvent.obtain () method and filled it with touch data.
3. The event object got into handleMessage (...), there it was preprocessed and, several methods later, got into the listener's onTouch () method.
4. The onTouch () method saved an object reference. This is where the timer starts.
5. In the handleMessage (...) method, the object was placed in the pool - event.recycle (). That is, the system now considers this object free for reuse.
6. While the timer is ticking, the user has touched the screen a few more times, while using the same object to process these touches.
7. The timer has finished counting, a method is called that refers by reference to the MotionEvent object obtained at the first touch. The object is the same, but x and y have already changed.
In the test example, everything was also simple:
1. First touch. The MotionEvent object is requested. Since the first call - the object is created.
2. The object is filled with information about the touch.
3. The object comes in onTouch () and we save the link to it in the history list.
4. The object is recycled.
5. The second touch. The MotionEvent object is requested. Since there is already one in the pool, it returns.
6. The coordinates obtained from the object pool are changed.
7. The object comes to onTouch (), we add it to the history, but this is the same object that is already in the history, and the coordinates of the first touch are lost - they were replaced by the coordinates of the second touch.
findings
Yes, it would be simpler and more correct to read the documentation and see that it is impossible to store MotionEvent objects in this way. It would be faster to look at the solution to the problem on StackOverflow. But, it was interesting and informative to go the way of MotionEvent through the sources from creation to recycling.