Motion events in android

По следам бага и немного о событиях MotionEvent в Android

Думаю, многие из нас писали код вида:

Но, думаю, не многие задумывались о том, какой путь проходит каждый объект MotionEvent прежде чем попасть в этот метод. В большинстве случае в этом нет необходимости, но все же случаются ситуации, когда незнание особенностей MotionEvent и обработки касаний приводит к печальным результатам.

Год назад я с друзьями разрабатывал приложение, где очень многое упиралось в обработку касаний. Однажды, загрузив новые исходники из репозитория и собрав приложение, я обнаружил, что вертикальная координата касания определяется неверно. Просматривая последние коммиты команды, я наткнулся на интересную строку, где внезапно от y-координаты отнималось 100. То есть, что-то вроде «y -= 100;», причем, это число не было вынесено как константа и вообще было непонятно почему именно 100. На мой очевидный вопрос я получил ответ «Ну, мы опытным путем определили, что в этом месте y-координата всегда на 100 (пикселей) больше, чем должна быть». Здесь, конечно, стоило бы перечитать документацию по обработке касаний и, просмотрев код проекта, найти ошибку, но я решил пойти более интересным путем – проследить по исходникам Android за MotionEvent от его получения до утилизации.

Если я смог кого-то заинтриговать историей в стиле «По следам полосатого бага» — добро пожаловать под кат.

Мораль

Для начала убедимся, что хранить MotionEvent, который пришел к нам с onTouch – плохо. Я использовал небольшое тестовое приложение со следующим кодом:

Запускаем приложение, несколько раз тапаем в одну точку под ActionBar-ом и смотрим в логи. Лично я получил следующую картину: «32.0», «41.0 41.0», «39.0 39.0 39.0», «39.0 39.0 39.0 39.0». То есть, после первого вызова мы сохранили в истории объект с y=32, но уже после следующего нажатия y этого объекта равен 41, а в историю заносится объект с таким же y. На самом деле это все один и тот же объект, который был использован при первом вызове onTouch и повторно использован при втором его вызове. Поэтому мораль проста: не храните MotionEvent, полученный в onTouch! Используйте этот объект только в рамках метода onTouch, а для остальных нужд извлекайте из него координаты и храните их в PointF, например.

Исходники Android – пул MotionEvent

А теперь предлагаю заглянуть в кроличью нору исходников Android и определить почему MotionEvent ведет себя именно таким образом.

Во-первых, уже по поведению тестового приложения понятно, что объекты MotionEvent не создаются при каждом касании, а повторно используются. Сделано это потому, что касаний может быть много за короткий промежуток времени и создание множества объектов ухудшило бы производительность. Как минимум за счет учащения сборки мусора. Представьте, сколько объектов создавалось бы за минуту игры в Fruit Ninja, ведь события – это не только DOWN, UP и CANCEL, но и MOVE.

Логика работы с пулом объектов MotionEvent находится классе MotionEvent — grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/android/view/MotionEvent.java. С пулом здесь связаны статические методы и переменные. Максимальное количество одновременно хранимых объектов определяет константа MAX_RECYCLED (и равна она 10), счетчик хранимых объектов – gRecyclerUsed, для синхронизации и обеспечения работы в асинхронном режиме используется gRecyclerLock. gRecyclerTop – голова списка объектов, оставленных на утилизацию. И еще есть не статическая переменная mNext, а также mRecycledLocation и mRecycled.

Когда системе нужен объект, вызывается статический метод obtain(). Если пул пуст (gRecyclerTop == null), создается и возвращается новый объект. В противном же случае возвращается последний утилизированный объект (gRecyclerTop), а его место занимает предпоследний (gRecyclerTop = gRecyclerTop.mNext).

Читайте также:  Android with 5mp camera

Для утилизации вызывается recycle() на утилизируемом объекте. Он занимает место «последнего добавленного» (gRecyclerTop), а ссылка на текущий «последний» сохраняется в mNext (mNext = gRecyclerTop). Это все происходит после проверки на переполнение пула.

Исходники Android – обработка MotionEvent

Нырять слишком глубоко не будем и начнем с метода handleMessage(Message msg) — grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/android/view/ViewRoot.java?av=f#1712 – класса ViewRoot. Сюда приходит уже готовый MotionEvent (полученный системой через MotionEvent.obtain()), обернутый в Message. Метод, кстати, служит для обработки не только касаний, но и других событий. Поэтому тело метода – большой switch, в котором нас интересуют строки с 1744 по 1847. Здесь происходит предварительная обработка события, затем mView.dispatchTouchEvent(event), затем же событие добавляется в пул: event.recycle(). Метод dispatchTouchEvent(…) вызывает событие слушателя, если таковой имеется, и пытается делегировать обработку события внутренним View.

Следы бага

И теперь вкратце о том, в чем заключался баг.

Для начала немного о том, что конкретно делали с MotionEvent в том проекте. Получив объект, приложение сохраняло его в переменную, ждало некоторое количество миллисекунд и обрабатывало его. Связано такое поведение было с жестами: грубо говоря, если пользователь коснулся экрана и задержал палец на секунду – показать ему определенный диалог. Приложение получало событие ACTION_DOWN и, не получив в течение секунды событий ACTION_UP или ACTION_CANCEL, реагировало. Причем, реагировало исходя из инициирующего MotionEvent. Таким образом, ссылка на него жила некоторое время, за которое могло произойти несколько других событий касания.

Последовательно происходило следующее:
1. Пользователь касался экрана.
2. Система получала новый объект методом MotionEvent.obtain() и наполняла его данными о касании.
3. Объект события попадал в handleMessage(…), там он предобрабатывался и, несколько методов спустя, попадал в метод onTouch() слушателя.
4. Метод onTouch() сохранял ссылку на объект. Здесь же запускается таймер.
5. В методе handleMessage(…) объект помещался в пул — event.recycle(). То есть, система теперь считает этот объект свободным для повторного использования.
6. Пока таймер тикает, пользователь коснулся экрана еще несколько раз, при этом для обработки этих касаний использовался один и тот же объект.
7. Таймер завершил отсчет, вызывается некий метод, который обращается по ссылке к объекту MotionEvent, полученному при первом касании. Объект тот же, а вот x и y уже успели поменяться.

В тестовом же примере все тоже было просто:
1. Первое касание. Запрашивается объект MotionEvent. Поскольку вызов первый – объект создается.
2. Объект наполняется информацией о касании.
3. Объект приходит в onTouch() и мы сохраняем ссылку на него в списке-истории.
4. Объект утилизируется.
5. Второе касание. Запрашивается объект MotionEvent. Поскольку в пуле уже есть один – он и возвращается.
6. У полученного из пула объекта меняются координаты.
7. Объект приходит в onTouch(), мы добавляем его в историю, но это тот же объект, что и уже есть в истории, а координаты первого касания утеряны – их заменили координаты второго касания.

Источник

Motion events in android

Объект класса MotionEvent используется для сообщения о событиях перемещения (мыши, пера, пальца, трекбола). Motion events (события перемещения) могут содержать либо абсолютные, либо относительные перемещения и другие данные, в зависимости от типа устройства.

[Обзор класса MotionEvent]

События перемещения описывают перемещения в виде кода действий (action code) и набора значений осей координат. Action code указывает изменение состояния, которое происходит, когда указатель опускается или поднимается. Значения координат осей описывают позицию и другие свойства перемещения.

Читайте также:  Android emulator visual studio xamarin

Например, когда пользователь первый раз касается экрана, система передает событие касания (touch event) к соответствующему элементу визуального интерфейса View с кодом действия ACTION_DOWN (это и есть action code), с набором координат X и Y точки касания, и с информацией о давлении, размере и ориентации области касания.

Некоторые устройства могут сообщать о сразу нескольких путях перемещения, происходящих одновременно. Экраны с поддержкой мультитача (Multi-touch screens, экраны с определением множественных касаний) выдает трассу перемещения для каждого пальца. Отдельные пальцы или другие объекты, которые генерируют трассы перемещения, называются указателями (pointers). События перемещения содержат информацию о всех указателях, которые активны в настоящий момент, даже если некоторые из них не переместились с момента последнего полученного события.

Количество указателей всегда изменяется только на 1, когда отдельные указатели опускаются и поднимаются, за исключением случая отмены жеста.

Каждый указатель имеет уникальный идентификатор, который назначается при первом опускании (показывается кодом ACTION_DOWN или ACTION_POINTER_DOWN ). Идентификатор указателя остается валидными, пока указатель не поднимется (показывается кодом ACTION_UP или ACTION_POINTER_UP ), или когда жест отменен (показывается кодом ACTION_CANCEL ).

Класс MotionEvent предоставляет несколько методов для опроса позиции и других свойств указателей, такие как getX(int) , getY(int) , getAxisValue(int) , getPointerId(int) , getToolType(int) и многие другие. Большинство этих методов принимают индекс указателя в качестве параметра, а не идентификатор указателя. Индекс каждого указателя находится в диапазоне от 0 до getPointerCount() -1.

Заранее не определен порядок, в котором указатели появляются в событии перемещения. Таким образом индекс одного и того же указателя может поменяться от одного события к другому, но идентификатор указателя гарантированно останется неизменным, пока указатель остается активным. Используйте метод getPointerId(int), чтобы получить идентификатор указателя, и отслеживать его через последующие события перемещения в жесте. Тогда для последовательных событий перемещения используйте метод findPointerIndex(int), чтобы получить индекс указателя на предоставленный идентификатор указателя в этом событии перемещения.

Мышь и кнопки стилуса могут быть получены с использованием getButtonState() . Хорошая идея проверить состояние кнопки при обработки ACTION_DOWN как части события касания. Приложение может выполнять разные действия при появлении события касания, если событие касания сопровождается вторым нажатием на кнопку, как например вызов контекстного меню.

[Пакетная обработка (Batching)]

Для эффективности события перемещения с ACTION_MOVE могут группировать вместе несколько выборок перемещения в один объект (batch). Самые актуальные текущие координаты доступны через вызов getX(int) и getY(int). Предыдущие координаты в batch доступны через getHistoricalX(int, int) и getHistoricalY(int, int). Координаты будут «историческими» (historical) только потому, что они старше текущих координат в batch; однако они все равно более актуальные, чем другие координаты, которые были получены в других событиях перемещения. Чтобы обработать все координаты в batch с привязкой по времени, сначала рассмотрите historical-координаты и затем текущие координаты.

Пример, получение всех выборок для всех указателей в порядке по времени:

[Типы устройств]

Интерпретация содержимого MotionEvent значительно варьируется в зависимости от источника класса устройства.

Для устройств указания с исходным классом SOURCE_CLASS_POINTER , таких как тачскрины, координаты указателя показывают абсолютные позиции, такие как координаты X/Y представления. Каждый полный жест представлен последовательностью событий перемещения с действиями, которые описывают переходы состояний указателя и перемещения. Жест начинается с события перемещения ACTION_DOWN, которое предоставляет место первого опускания указателя. Для каждого дополнительного указателя, который опускается вниз или вверх, фреймворк будет генерировать событие перемещения с ACTION_POINTER_DOWN или ACTION_POINTER_UP соответственно. Перемещения указателя описаны в событиях перемещения с ACTION_MOVE. Окончание жеста происходит либо когда поднимается последний указатель (по событию перемещения с ACTION_UP), или когда жест отменен (с событием перемещения ACTION_CANCEL).

Читайте также:  Лидер по продажам android

Некоторые устройства указания, такие как мыши, могут поддерживать вертикальную и/или горизонтальную прокрутку (scrolling). Событие прокрутки сообщается как обычное событие перемещений с ACTION_SCROLL , которое включает в себя смещение прокрутки в по осям AXIS_VSCROLL и AXIS_HSCROLL . См. описание getAxisValue(int) для дополнительной информации как получать эти дополнительные оси.

Устройства трекбол с исходным классом SOURCE_CLASS_TRACKBALL получают координаты указателя, которые задают относительное перемещение (изменение) по осям X/Y. Жест трекбола состоит из последовательности перемещений, описанных событиями перемещения с ACTION_MOVE, перемежающимися со случающимися событиями ACTION_DOWN или ACTION_UP, когда кнопки трекбола нажимаются или отпускаются.

Устройства джойстика с исходным классом SOURCE_CLASS_JOYSTICK , получают координаты указателя с абсолютной позицией по осям джойстика. Значения осей джойстика нормализуются к диапазону от -1.0 до 1.0, где 0.0 соответствует центральному положению. Больше информации по набору доступных осей и диапазону перемещений можно получить через getMotionRange(int). Общие оси джойстика: AXIS_X, AXIS_Y, AXIS_HAT_X, AXIS_HAT_Y, AXIS_Z и AXIS_RZ.

Обратитесь к InputDevice для дополнительной информации о различных видах устройств ввода, источниках, предоставляющих координаты указателя.

[Гарантии согласованности]

События перемещения всегда передаются в представления (view) как последовательный поток событий. Что из себя представляет последовательный поток — зависит от типа устройства. Для событий касания последовательность включает в себя информацию по опусканию указателей с привязкой ко времени, групповым перемещением, и затем с поднятием по одному или с отменой жеста.

Хотя фреймворк пытается передать непрерывный поток событий перемещения к элементам интерфейса (View), нет полной гарантии о передаче всех событий. Некоторые события могут быть отброшены или модифицированы элементами интерфейса в приложении до того, как будут доставлены, в результате чего поток событий будет неполным. Элементы интерфейса (View) должны быть всегда готовы к обработке ACTION_CANCEL и должны быть толерантны к аномальным ситуациям, таким как прием нового ACTION_DOWN без предварительного получения ACTION_UP для предыдущего жеста.

Полное описание констант и методов класса см. в [1].

[Пример: определение места касания экрана]

Для того, чтобы определить место касания экрана в программе Android, переопределите метод onTouchEvent для в классе Activity:

[Пример: определение места касания на виджете ImageView]

MotionEvent может использоваться для определения мест касания на изображении, чтобы по ним предпринимать различные действия. Простой пример — двухпозиционный выключатель, на левой картинке он в положении «OFF» (выключено), а на правой в положении «ON» (включено).

Идея состоит в следующем: при нажатии на верхнюю часть картинки выключатель должен «включаться» (т. е. должна показываться картинка, где выключатель в положении ON), а когда нажатие происходит на нижнюю часть картинки, то выключатель должен «выключаться» (должна показываться картинка, где выключатель в положении OFF). Далее процесс по шагам.

1. Создайте 2 картинки для состояний выключателя ON и OFF в виде файлов формата PNG, назовите эти файлы switch_on.png и switch_off.png соответственно. Положите их в папку res\drawable\ проекта. Сделайте Clean для проекта, чтобы в файле R.java сгенерировались идентификаторы для картинок.

2. Бросьте на форму программы виджет ImageView, дайте ему понятный идентификатор, например imageSwitch. В качестве источника картинки для ImageView укажите ресурс switch_off (это наша картинка для выключенного состояния выключателя). В результате в файле activity_main.xml появится определение наподобие следующего:

3. В обработчике в класс Activity программы добавьте переменную ImageView, и в обработчике onCreate сделайте инициализацию этой переменной, привязав её к добавленному виджету imageSwitch. Добавьте также для этой переменной обработчик события OnTouchListener. Вот полный код для onCreate активности:

Источник

Оцените статью