Following the trail of the bug and a bit about the MotionEvent events in Android

    I think many of us wrote a code of the form:

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            // использование x и y...
            return false;
        }
    

    But, I think, not many people thought about what path each MotionEvent object goes through before getting into this method. In most cases, this is not necessary, but still there are situations where ignorance of the features of MotionEvent and touch processing leads to sad results.

    A year ago, I was developing an application with my friends, where a lot rested on touch processing. Once, downloading new sources from the repository and compiling the application, I found that the vertical coordinate of the touch is not detected correctly. Looking through the last commits of the command, I came across an interesting line, where suddenly 100 points were subtracted from the y-coordinate. That is, something like “y - = 100;”, moreover, this number was not taken out as a constant and in general it was not clear why 100. To my obvious question, I received the answer "Well, we experimentally determined that at this point the y-coordinate is always 100 (pixels) more than it should be." Here, of course, it would be worth re-reading the documentation for processing touches and, having looked at the project code, find an error,

    If I was able to intrigue someone with a story in the style of "Following the Striped Bug" - welcome to 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;
    // imports…
    public class MainActivity extends Activity implements OnTouchListener {
        private List mEventsHistory = new ArrayList();
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            View parentLayout = findViewById(R.id.parent_layout);
            parentLayout.setOnTouchListener(this);
        }
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            logEventsHistory();
            mEventsHistory.add(event);
            return false;
        }
        protected void logEventsHistory() {
            StringBuilder message = new StringBuilder();
            for (MotionEvent event : mEventsHistory) {
                message.append(event.getY());
                message.append(" ");
            }
            Log.i("Events", message.toString());
        }
    }
    


    We launch the application, tap several times at one point under the ActionBar and look at the logs. Personally, I got 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 press of y this object is 41, and an object with the same y is recorded in the history. In fact, this is all the same object that was used the first time onTouch was called and reused the second time it was called. Therefore, the moral is simple: do not store the MotionEvent received in onTouch! Use this object only within the onTouch method, and for other needs, extract coordinates from it and store them in PointF, for example.

    Android sources - MotionEvent pool


    And now I propose to look into the rabbit hole of the Android sources and determine why MotionEvent behaves in this way.

    First, it’s clear from the behavior of the test application that MotionEvent objects are not created every time they are touched, but are 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 in a minute of playing Fruit Ninja, because events are not only DOWN, UP and CANCEL, but also MOVE.

    The logic for working with the MotionEvent object pool is located in the MotionEvent class - grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/android/view/MotionEvent.java. Static methods and variables are associated with the pool here. The maximum number of simultaneously stored objects is determined by the MAX_RECYCLED constant (and is equal to 10), the counter of stored objects is determined by gRecyclerUsed, gRecyclerLock is used to synchronize and ensure operation 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 the system needs an object, the static obtain () method is called. If the pool is empty (gRecyclerTop == null), a new object is created and returned. Otherwise, the last recycled object is returned (gRecyclerTop), and the last but one (gRecyclerTop = gRecyclerTop.mNext) takes its place.

    For disposal, recycle () is called on the object to be disposed. 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 will not dive too deeply 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 through MotionEvent.obtain ()), wrapped in Message. The method, by the way, serves to handle not only touches, but also other events. Therefore, the body of the method is a large 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 attempts to delegate the processing of the event to an internal View.

    Traces of a bug


    And now briefly about what the bug was.

    First, a little about what exactly they did with MotionEvent in that project. Having received the object, the application saved it into a variable, waited for a certain number of milliseconds and processed it. Such behavior was associated with gestures: roughly speaking, if the user touched the screen and held his finger for a second - show him a certain dialogue. The application received the ACTION_DOWN event and, without receiving ACTION_UP or ACTION_CANCEL events within a second, responded. Moreover, it reacted based on the initiating MotionEvent. Thus, the link to him lived for some time, during which several other touch events could occur.

    Sequentially, the following occurred:
    1. The user touched the screen.
    2. The system received a new object using the MotionEvent.obtain () method and populated it with touch data.
    3. The event object got into handleMessage (...), there it was processed and, several methods later, it got into the listener's onTouch () method.
    4. The onTouch () method kept a reference to the object. Here 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 touched the screen several more times, while the same object was used to process these touches.
    7. The timer has completed the countdown, a certain method is called, which 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. The first touch. The MotionEvent object is requested. Since the first call - the object is created.
    2. The subject is filled with touch information.
    3. The object comes in onTouch () and we save the link to it in the history list.
    4. The object is disposed of.
    5. The second touch. The MotionEvent object is requested. Since there is already one in the pool, he returns.
    6. The object received from the pool changes its coordinates.
    7. The object comes in onTouch (), we add it to the story, but this is the same object that is already in the story, and the coordinates of the first touch are lost - they were replaced by the coordinates of the second touch.

    conclusions


    Yes, it would be easier and more correct to read the documentation and see there that it is impossible to store MotionEvent objects in this way. It would be faster to see the solution to the problem on StackOverflow. But, to go the MotionEvent path from source to creation from utilization was interesting and informative.

    Also popular now: