Android studio координаты касания

Полный список

Раньше мы для View-компонентов использовали OnClickListener и ловили короткие нажатия. Теперь попробуем ловить касания и перемещения пальца по компоненту. Они состоят из трех типов событий:

— нажатие (палец прикоснулся к экрану)
— движение (палец движется по экрану)
— отпускание (палец оторвался от экрана)

Все эти события мы сможем ловить в обработчике OnTouchListener, который присвоим для View-компонента. Этот обработчик дает нам объект MotionEvent, из которого мы извлекаем тип события и координаты.

На этом уроке рассмотрим только одиночные касания. А мультитач – на следующем уроке.

Project name: P1021_Touch
Build Target: Android 2.3.3
Application name: Touch
Package name: ru.startandroid.develop.p1021touch
Create Activity: MainActivity

strings.xml и main.xml нам не понадобятся, их не трогаем.

MainActivity реализует интерфейс OnTouchListener для того, чтобы выступить обработчиком касаний.

В onCreate мы создаем новый TextView, сообщаем ему, что обработчиком касаний будет Activity, и помещаем на экран.

Интерфейс OnTouchListener предполагает, что Activity реализует его метод onTouch. На вход методу идет View для которого было событие касания и объект MotionEvent с информацией о событии.

Методы getX и getY дают нам X и Y координаты касания. Метод getAction дает тип события касания:

ACTION_DOWN – нажатие
ACTION_MOVE – движение
ACTION_UP – отпускание
ACTION_CANCEL – практически никогда не случается. Насколько я понял, возникает в случае каких-либо внутренних сбоев, и следует трактовать это как ACTION_UP.

В случае ACTION_DOWN мы пишем в sDown координаты нажатия.

В случае ACTION_MOVE пишем в sMove координаты точки текущего положения пальца. Если мы будем перемещать палец по экрану – этот текст будет постоянно меняться.

В случае ACTION_UP или ACTION_CANCEL пишем в sUp координаты точки, в которой отпустили палец.

Все это в конце события выводим в TextView. И возвращаем true – мы сами обработали событие.

Теперь мы будем водить пальцем по экрану (курсором по эмулятору) в приложении, и на экране увидим координаты начала движения, текущие координаты и координаты окончания движения.

Все сохраним и запустим приложение.

Ставим палец (курсор) на экран

Если вчерашний вечер не удался, голова не болит, рука тверда и не дрожит :), то появились координаты нажатия.

Если же рука дрогнула, то появится еще и координаты перемещения.

Продолжаем перемещать палец и видим, как меняются координаты Move.

Теперь отрываем палец от экрана и видим координаты точки, в которой это произошло

В целом все несложно. При мультитаче процесс немного усложнится, там уже будем отслеживать до 10 касаний.

Если вы уже знакомы с техникой рисования в Android, то вполне можете создать приложение выводящее на экран геометрическую фигуру, которую можно пальцем перемещать. Простейший пример реализации можно посмотреть тут: http://forum.startandroid.ru/viewtopic.php?f=28&t=535.

На следующем уроке:

— обрабатываем множественные касания

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Источник

Распознавание пользовательских жестов

Недавно, при разработке игры под Android, я столкнулся с проблемой реализации работы с пользовательскими жестами. В стандартной комплектации Android SDK имеется класс GestureDetector (тут демонстрация работы с этим классом), однако в нём реализованы не все жесты, что мне были нужны, а также некоторые из них работали не так, как мне надо (onLongPress, например, срабатывал не только по длительному касанию, но и по длительному касанию с ведением пальца по экрану). Кроме игр жесты могут использоваться и в обычных приложениях. Они могут заменить некоторые элементы интерфейса, тем самым сделав его проще. Жесты уже используются в очень многих приложениях для устройств с сенсорным вводом и это даёт нам право предполагать, что пользователь уже знаком с ними. Сегодня мы реализуем в нашем приложении распознавание long press, double touch, pinch open, pinch close и других.

Читайте также:  Озвучивание входящих сообщений андроид

Препродакшн

Для примера работы с сенсорным экраном, multitouch и жестами я решил реализовать простенький графический редактор. Что должен представлять из себя наш графический редактор? Небольшой таскающийся холст, расстояние до которого можно менять, разводя и сводя пальцы. Также, чтобы не путать перетаскивание холста и рисование по нему, мы должны реализовать смену режима по длительному касанию. По двойному касанию можно отображать окошко выбора цвета.

Основа приложения

Для начала создаём Android Application Project в Eclipse. Наш проект не будет использовать каких-либо функций из новых SDK, поэтому в качестве Minimum Required SDK можно поставить API 8 (Android 2.2), например. MainActivity (если его нету, то нужно будет создать) приведём к такому виду:

Соответственно, мы должны создать класс ApplicationView, который будет наследовать от View:

Теперь нам надо разобраться, как мы будем отображать холст. Мы создаём Canvas, передав ему в параметры Bitmap. В данном случае при рисовании чего-либо на нашем Canvas, оно отображается на Bitmap. Так же нам нужна переменная позиции нашего холста и расстояния до него. Добавляем в класс ApplicationView несколько переменных:

В конструкторе класса инициализируем холст и его содержимое, а так же закрашиваем его белым цветом:

В методе onDraw отображаем его:

Если мы сейчас запустим наше приложение, мы увидим белый квадрат на сером фоне.

Можно улучшить приложение, сделав его полноэкранным и добавив ландшафтную ориентацию. Для этого надо изменить параметры нашего Activity:

А также, для полноэкранности, изменить style-файлы. Для API 10 и ниже (values/styles.xml):

Для API 11 и выше (values-v11/styles.xml):

Для API 14 (values/styles-v14.xml):

Реализация работы с сенсорным экраном

Нам нужно начать начать взаимодействовать с сенсорным экраном с помощью переопределения метода onTouchEvent (подробнее о работе с сенсорным экраном тут):

Мы видим, что при касании пальца, в список fingers мы добавляем объект типа Finger. Когда палец поднимаем, мы удаляем этот объект, а при перемещении пальцев мы обновляем координаты всех объектов. В итоге список fingers содержит в себе все касания с актуальными и предыдущими координатами. Класс Finger:

Мы можем нехитрой манипуляций отобразить все касания. Для этого в методе onDraw вписываем это:

Перемещение холста

Для реализации перемещения холста нам нужно в методе onTouchEvent, в блоке перемещения объекта, вызывать метод checkGestures, который будет работать с касаниями. Вызов этого метода там уже есть, однако под комментарием. Раскомментируем его и пишем сам метод:

Можно запустить и поводить пальцем и если всё сделано правильно, то холст должен таскаться за ним.

Изменение расстояния до холста

Для реализации данного жеста нужно разделить всё содержимое метода на multitouch (когда пальцев более одного) и обычное касание. Если это multitouch, то мы будем постоянно проверять нынешнее расстояние между двумя пальцами и прошлое. Изменим содержимое метода checkGestures:

В этом участке кода был использован метод checkDistance, который нужно добавить в ApplicationView. Вот его код:

Теперь при касании двух пальцев и сведении\разведении их будет изменятся расстояние до холста. Если у вас эмулятор, то ничего не получится.

Изменение режима

Нам нужно создать переменную, которая будет отвечать за режим. Я назвал её drawingMode типа boolean. Для реализации долгого касания нам придётся использовать метод, который будет вызываться через время. Тут есть несколько вариантов развития событий:

  1. мы пишем наш код в методе onDraw;
  2. мы пишем наш код в onTouchEvent;
  3. мы создаём таймер и пишем код в него;
  4. мы создаём Handler и с помощью метода postDelayed вызываем наш Runnable;

В методе onDraw, по моему мнению, должно выполняться только отображение графики. В onTouchEvent писать можно только с учётом того, что палец будет перемещаться, тем самым постоянно вызывая этот метод. Таймер работает постоянно и это немного напрягает, поэтому мы будем использовать четвёртый вариант. В классе ApplicationView мы создаём переменную handler типа Handler, а так же добавляем в блок «if(action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN)» метода onTouchEvent строку:

Теперь надо создать Runnable с именем longPress:

Теперь нам в классе Finger надо создать переменную wasDown типа long, которая будет содержать в себе время нажатия (пригодится для двойного касания). В конструкторе этой переменной надо задать значение System.currentTimeMillis(). Ещё нам надо добавить переменную startPoint типа Point, которая будет содержать в себе стартовую позицию пальца. Она должна содержать в себе то значение, что было передано в конструктор, или при первом вызове setNow. Так же нам надо создать переменную enabledLongTouch типа boolean, отображающую пригодность данного касания для реализуемого нами события. Нам надо постоянно проверять, не отошёл ли палец слишком далеко от старта. Этот функционал можно реализовать в setNow. В итоге должно получиться примерно так:

Читайте также:  Как узнать свой порт андроид

Теперь в методе run нашего Runnable мы можем проверять, длительное ли это касание:

Чтобы сделать длительное касание лучше — надо включить лёгкую вибрацию, когда оно активировалось. Для этого надо просто создать переменную vibrator типа Vibrator и в конструкторе установить ей значение следующим образом:

Важно: для работы с вибрацией в manifest’е должна быть следующая строка:

Тогда в методе run, нашего таймера в конце проверки на длительное касание можно вписать:

Рисование

Сейчас мы реализуем рисование на холсте и изменение размера кисти. Для этого мы каждую часть метода checkGestures разделим ещё на две части: режим рисования и обычный режим. В режиме рисования при касании мы просто будем вести линию, а в режиме рисования при multitouch мы будем изменять размер кисти. Вот так станет выглядеть метод checkGestures:

В последней строке я задаю значение некому cursor. Это переменная типа Point, содержащая координаты курсора. Курсор нужен лишь для того, чтобы ориентироваться в размере кисти. Для отображения курсора в методе onDraw добавляем:

Теперь мы можем перемещать холст, приближать, отдалять его, переходить в режим рисования, рисовать, изменять размер кисти. Осталось лишь реализовать выбор цвета.

Выбор цвета

Выбор цвета происходит после двойного касания по экрану. Для этого в ApplicationView нужно создать переменную, сохраняющую прошлое касание по экрану и переменную, сохраняющую координаты этого касания. Первая пусть будет называться lastTapTime типа long, а вторая — lastTapPosition типа Point. Тогда изменим метод onTouchEvent:

Нам осталось лишь реализовать диалог выбора цвета. Там, где происходит касание (помечено комментарием), пишем:

Если вы запустите приложение, то увидите что по двойному касанию появляется окошко выбора цвета.

Заключение

Распознавание пользовательских жестов оказалось не такой уж и сложной задачей. Мы разобрали эту на примере реализации графического редактора. Похожей реализацией, естественно, могут обладать не только приложения, но и игры.

Источник

По следам бага и немного о событиях 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, например.

Читайте также:  Смена id веб камеры андроид

Исходники 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).

Для утилизации вызывается 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(), мы добавляем его в историю, но это тот же объект, что и уже есть в истории, а координаты первого касания утеряны – их заменили координаты второго касания.

Источник

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