Расширенные функции стилуса

Android и ChromeOS предоставляют различные API, которые помогают создавать приложения, обеспечивающие пользователям исключительное удобство работы со стилусом. Класс MotionEvent предоставляет информацию о взаимодействии стилуса с экраном, включая силу нажатия, ориентацию, наклон, наведение и распознавание ладони. Библиотеки для прогнозирования движения и графики с низкой задержкой улучшают рендеринг стилуса на экране, чтобы обеспечить естественное ощущение, как при письме ручкой на бумаге.

MotionEvent

Класс MotionEvent представляет действия пользователя, например положение и движение указателей на экране. Для ввода стилусом MotionEvent также предоставляет данные о давлении, ориентации, наклоне и наведении.

Сведения о событии

Чтобы получить доступ к объектам MotionEvent стилуса, добавьте модификатор pointerInteropFilter к поверхности рисования. Реализуйте класс ViewModel с методом, который обрабатывает события движения. Передайте метод в качестве лямбда-выражения onTouchEvent модификатора pointerInteropFilter:

@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
    Canvas(
        modifier = modifier
            .clipToBounds()
            .pointerInteropFilter {
                viewModel.processMotionEvent(it)
            }

    ) {
        // Drawing code here.
    }
}

Объект MotionEvent предоставляет данные, связанные со следующими аспектами события пользовательского интерфейса:

  • Действия – физическое взаимодействие с устройством, например касание экрана, перемещение указателя по экрану или наведение указателя на экран.
  • Указатели: идентификаторы объектов, взаимодействующих с экраном (палец, стилус, мышь).
  • Axis: тип данных – координаты x и y, давление, наклон, ориентация и наведение (расстояние).

Действия

Чтобы реализовать поддержку стилуса, вам нужно понять, какое действие выполняет пользователь.

В классе MotionEvent есть множество констант ACTION, которые определяют события движения. Вот основные действия, которые можно выполнять с помощью стилуса:

Действие Описание
ACTION_DOWN
ACTION_POINTER_DOWN
Указатель коснулся экрана.
ACTION_MOVE Указатель перемещается по экрану.
ACTION_UP
ACTION_POINTER_UP
Указатель больше не касается экрана.
ACTION_CANCEL Когда предыдущий или текущий набор движений должен быть отменен.

Приложение может выполнять такие задачи, как начало нового штриха при возникновении ACTION_DOWN, рисование штриха с помощью ACTION_MOVE, и завершение штриха при срабатывании ACTION_UP.

Набор действий MotionEvent от ACTION_DOWN до ACTION_UP для заданного указателя называется набором движений.

Указатели

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

Индексы указателей варьируются от нуля до количества указателей, возвращаемых методом MotionEvent#pointerCount(), минус 1.

Значения осей указателей можно получить с помощью метода getAxisValue(axis, pointerIndex). Если индекс указателя не задан, система возвращает значение для первого указателя (0).

Объекты MotionEvent содержат информацию о типе используемого указателя. Чтобы узнать тип указателя, переберите индексы указателей и вызовите метод getToolType(pointerIndex).

Подробнее об указателях…

Ввод стилусом

Вы можете отфильтровать ввод стилусом с помощью TOOL_TYPE_STYLUS:

val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)

Стилус также может сообщать, что он используется в качестве ластика, с помощью TOOL_TYPE_ERASER:

val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)

Данные об осях стилуса

ACTION_DOWN и ACTION_MOVE предоставляют данные о стилусе, а именно координаты x и y, давление, ориентацию, наклон и наведение.

Чтобы предоставить доступ к этим данным, API MotionEvent предоставляет getAxisValue(int), где параметр – это любой из следующих идентификаторов осей:

Axis Возвращаемое значение getAxisValue()
AXIS_X Координата X события движения.
AXIS_Y Координата Y события движения.
AXIS_PRESSURE Для сенсорного экрана или сенсорной панели – сила нажатия пальцем, стилусом или другим указателем. Для мыши или трекбола: 1, если нажата основная кнопка, и 0, если нет.
AXIS_ORIENTATION Для сенсорного экрана или сенсорной панели – ориентация пальца, стилуса или другого указателя относительно вертикальной плоскости устройства.
AXIS_TILT Угол наклона стилуса в радианах.
AXIS_DISTANCE Расстояние от стилуса до экрана.

Например, MotionEvent.getAxisValue(AXIS_X) возвращает координату X для первого указателя.

Подробнее о том, как обрабатывать жесты с несколькими касаниями…

Позиция

Вы можете получить координаты x и y указателя с помощью следующих вызовов:

Стилус рисует на экране с координатами x и y.
Рисунок 1. Координаты X и Y указателя стилуса на экране.

Давление

Силу нажатия можно получить с помощью метода MotionEvent#getAxisValue(AXIS_PRESSURE) или, для первого указателя, MotionEvent#getPressure().

Значение силы нажатия для сенсорных экранов и панелей находится в диапазоне от 0 (нет нажатия) до 1, но в зависимости от калибровки экрана могут возвращаться и более высокие значения.

Штрих стилуса, представляющий континуум от низкого до высокого давления. Слева штрих узкий и бледный, что указывает на низкое давление. Штрих становится шире и темнее слева направо, пока не станет самым широким и темным справа, что указывает на самое высокое давление.
Рисунок 2. Представление давления: низкое давление слева, высокое давление справа.

Ориентация

Ориентация указывает, в каком направлении указывает стилус.

Ориентацию указателя можно получить с помощью getAxisValue(AXIS_ORIENTATION) или getOrientation() (для первого указателя).

Для стилуса ориентация возвращается в виде значения в радианах от 0 до pi (𝛑) по часовой стрелке или от 0 до -pi против часовой стрелки.

Ориентация позволяет использовать настоящую кисть. Например, если стилус представляет собой плоскую кисть, ширина плоской кисти зависит от ориентации стилуса.

Рисунок 3. Стилус указывает влево примерно на минус 0,57 радиана.

Наклон

Наклон – это угол между стилусом и экраном.

Наклон возвращает положительный угол стилуса в радианах, где ноль – перпендикулярно экрану, а 𝛑/2 – плоско на поверхности.

Угол наклона можно получить с помощью getAxisValue(AXIS_TILT) (для первого указателя сочетания клавиш нет).

Наклон позволяет максимально точно воспроизвести реальные инструменты, например имитировать штриховку наклоненным карандашом.

Стилус наклонен примерно на 40 градусов относительно поверхности экрана.
Рисунок 4. Стилус наклонен примерно на 0,785 радиана, или 45 градусов от перпендикуляра.

Наведите указатель

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

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

Рисунок 5. Стилус над экраном. Приложение реагирует, даже если стилус не касается поверхности экрана.

Примечание. Compose предоставляет модификаторы, которые влияют на интерактивное состояние элементов пользовательского интерфейса:

  • hoverable – настроить компонент так, чтобы он реагировал на наведение указателя.
  • indication – рисует визуальные эффекты для этого компонента при взаимодействии.

Игнорирование касаний ладонью, навигация и случайные вводы

Иногда мультисенсорные экраны могут регистрировать нежелательные касания, например, когда пользователь кладет руку на экран для поддержки во время рукописного ввода. Функция игнорирования касаний ладонью обнаруживает такие действия и сообщает, что последний набор MotionEvent нужно отменить.

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

ACTION_CANCEL и FLAG_CANCELED

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

ACTION_CANCEL

Добавлено в Android 1.0 (уровень API 1)

ACTION_CANCEL указывает, что предыдущий набор событий движения должен быть отменен.

ACTION_CANCEL срабатывает, когда обнаруживается любое из следующих событий:

  • Жесты для навигации
  • Игнорирование касаний ладонью

При активации ACTION_CANCEL необходимо определить активный указатель с помощью getPointerId(getActionIndex()). Затем удалите штрих, созданный этим указателем, из истории ввода и повторно отрисуйте сцену.

FLAG_CANCELED

Добавлено в Android 13 (уровень API 33)

FLAG_CANCELED Указывает, что указатель был поднят в результате непреднамеренного касания. Обычно этот флаг устанавливается, когда пользователь случайно касается экрана, например, держа устройство или кладя ладонь на экран.

Чтобы получить доступ к значению параметра, выполните следующие действия:

val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED

Если флаг установлен, вам нужно отменить последний набор MotionEvent из последнего ACTION_DOWN от этого указателя.

Как и в случае с ACTION_CANCEL, указатель можно найти с помощью getPointerId(actionIndex).

Рисунок 6. Штрих стилуса и касание ладонью создают наборы MotionEvent. Касание ладонью отменяется, и экран обновляется.

Полноэкранный режим, отображение на весь экран и навигация жестами

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

Рисунок 7. Жест, позволяющий перевести приложение в фоновый режим.

Чтобы жесты не вызывали нежелательные нажатия в приложении, вы можете использовать отступы и ACTION_CANCEL.

Также ознакомьтесь с разделом Игнорирование касаний ладонью, навигация и нежелательные действия.

Чтобы жесты навигации не вызывали нежелательные события касания, используйте метод setSystemBarsBehavior() и свойство BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE объекта WindowInsetsController.

// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

Дополнительную информацию о вставках и управлении жестами можно найти в следующих статьях:

Малая задержка

Задержка – это время, которое требуется оборудованию, системе и приложению, чтобы обработать и отобразить действия пользователя.

Задержка = обработка ввода аппаратным обеспечением и ОС + обработка приложением + компоновка системой

  • аппаратная отрисовка
Задержка приводит к тому, что отрисованный штрих отстает от положения стилуса. Промежуток между нарисованным штрихом и положением стилуса представляет собой задержку.
Рисунок 8. Задержка приводит к тому, что отрисованный штрих отстает от положения стилуса.

Источник задержки

  • Регистрация стилуса на сенсорном экране (аппаратное обеспечение). Первоначальное беспроводное подключение, при котором стилус и ОС обмениваются данными для регистрации и синхронизации.
  • Частота дискретизации касаний (аппаратная) – количество проверок в секунду, выполняемых сенсорным экраном, чтобы определить, касается ли указатель поверхности. Диапазон значений: от 60 до 1000 Гц.
  • Обработка входных данных (приложение): применение цвета, графических эффектов и преобразования к входным данным пользователя.
  • Отрисовка графики (ОС + аппаратное обеспечение): переключение буферов, аппаратная обработка.

Графика с низкой задержкой

Библиотека Jetpack для графики с низкой задержкой сокращает время обработки между вводом пользователя и отрисовкой на экране.

Библиотека сокращает время обработки, избегая многобуферного рендеринга и используя технику рендеринга переднего буфера, что означает запись непосредственно на экран.

Отрисовка переднего буфера

Передний буфер – это память, которую экран использует для рендеринга. Это самый близкий к экрану уровень, на котором могут рисовать приложения. Библиотека с низкой задержкой позволяет приложениям выполнять рендеринг непосредственно в передний буфер. Это повышает производительность, предотвращая замену буфера, которая может происходить при обычном рендеринге с несколькими буферами или рендеринге с двойным буфером (наиболее распространенный случай).

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

Отрисовка переднего буфера – отличный способ обновить небольшую область экрана, но не весь экран. При отрисовке в переднем буфере приложение отрисовывает контент в буфер, из которого дисплей считывает данные. В результате могут возникать артефакты рендеринга или разрывы (см. ниже).

Библиотека с низкой задержкой доступна на устройствах Android 10 (уровень API 29) и более поздних версий, а также на устройствах с ChromeOS, работающих на Android 10 (уровень API 29) и более поздних версий.

Связанные запросы

Библиотека с низкой задержкой предоставляет компоненты для реализации отрисовки переднего буфера. Библиотека добавляется в качестве зависимости в файл build.gradle модуля приложения:

dependencies {
    implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}

Обратные вызовы GLFrontBufferRenderer

Библиотека с низкой задержкой включает интерфейс GLFrontBufferRenderer.Callback, который определяет следующие методы:

Библиотека с низкой задержкой не имеет ограничений по типу данных, которые вы используете с GLFrontBufferRenderer.

Однако библиотека обрабатывает данные как поток из сотен элементов, поэтому оптимизируйте использование и распределение памяти.

Обратные вызовы

Чтобы включить обратные вызовы отрисовки, реализуйте GLFrontBufferedRenderer.Callback и переопределите onDrawFrontBufferedLayer() и onDrawDoubleBufferedLayer(). GLFrontBufferedRenderer использует обратные вызовы, чтобы отображать данные наиболее оптимальным образом.

val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> {
   override fun onDrawFrontBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       param: DATA_TYPE
   ) {
       // OpenGL for front buffer, short, affecting small area of the screen.
   }
   override fun onDrawMultiDoubleBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       params: Collection<DATA_TYPE>
   ) {
       // OpenGL full scene rendering.
   }
}
Объявите экземпляр GLFrontBufferedRenderer

Подготовьте GLFrontBufferedRenderer, указав SurfaceView и обратные вызовы, созданные ранее. GLFrontBufferedRenderer оптимизирует отрисовку в переднем и двойном буферах, используя ваши обратные вызовы:

var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Отрисовка

Отрисовка переднего буфера начинается при вызове метода renderFrontBufferedLayer(), который запускает обратный вызов onDrawFrontBufferedLayer().

Отрисовка с двойной буферизацией возобновляется при вызове функции commit(), которая запускает обратный вызов onDrawMultiDoubleBufferedLayer().

В примере ниже процесс выполняет рендеринг в передний буфер (быстрый рендеринг), когда пользователь начинает рисовать на экране (ACTION_DOWN) и перемещает указатель (ACTION_MOVE). Процесс выполняет рендеринг в двойной буфер, когда указатель покидает поверхность экрана (ACTION_UP).

Вы можете использовать requestUnbufferedDispatch(), чтобы система ввода не объединяла события движения в пакеты, а доставляла их по мере доступности:

when (motionEvent.action) {
   MotionEvent.ACTION_DOWN -> {
       // Deliver input events as soon as they arrive.
       view.requestUnbufferedDispatch(motionEvent)
       // Pointer is in contact with the screen.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_MOVE -> {
       // Pointer is moving.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_UP -> {
       // Pointer is not in contact in the screen.
       glFrontBufferRenderer.commit()
   }
   MotionEvent.CANCEL -> {
       // Cancel front buffer; remove last motion set from the screen.
       glFrontBufferRenderer.cancel()
   }
}

Рекомендации по отрисовке

✓ Рекомендуется

Небольшие части экрана, рукописный ввод, рисование, создание эскизов.

✗ Неправильно

Обновление полноэкранного режима, панорамирование, масштабирование. Может вызвать слезотечение.

Разрыв

Разрывы возникают, когда экран обновляется одновременно с изменением буфера экрана. На одной части экрана показываются новые данные, а на другой – старые.

Верхняя и нижняя части изображения Android не выровнены из-за разрыва при обновлении экрана.
Рисунок 11. Разрывы изображения при обновлении экрана сверху вниз.

Прогнозирование движения

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

Библиотека прогнозирования движения получает реальные пользовательские входные данные в виде объектов MotionEvent. Объекты содержат информацию о координатах x и y, давлении и времени, которая используется прогнозировщиком движения для прогнозирования будущих объектов MotionEvent.

Прогнозируемое количество объектов (MotionEvent) является приблизительным. Прогнозируемые события могут уменьшить воспринимаемую задержку, но прогнозируемые данные должны быть заменены фактическими данными MotionEvent после их получения.

Библиотека прогнозирования движения доступна в Android 4.4 (уровень API 19) и более поздних версиях, а также на устройствах с ChromeOS под управлением Android 9 (уровень API 28) и более поздних версий.

Задержка приводит к тому, что отрисованный штрих отстает от положения стилуса. Промежуток между штрихом и стилусом заполняется точками прогнозирования. Оставшаяся разница – это воспринимаемая задержка.
Рисунок 12. Задержка уменьшена за счет прогнозирования движений.

Связанные запросы

Библиотека прогнозирования движения предоставляет реализацию прогнозирования. Библиотека добавляется в качестве зависимости в файл build.gradle модуля приложения:

dependencies {
    implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}

Реализация

Библиотека прогнозирования движения включает интерфейс MotionEventPredictor, который определяет следующие методы:

  • record() хранит объекты MotionEvent как записи о действиях пользователя.
  • predict(): возвращает прогнозируемое значение MotionEvent.
Объявите экземпляр MotionEventPredictor
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Передайте данные в предиктор
motionEventPredictor.record(motionEvent)
Прогноз

when (motionEvent.action) {
   MotionEvent.ACTION_MOVE -> {
       val predictedMotionEvent = motionEventPredictor?.predict()
       if(predictedMotionEvent != null) {
            // use predicted MotionEvent to inject a new artificial point
       }
   }
}

Рекомендации по прогнозированию движения

✓ Рекомендуется

Удалять точки прогноза при добавлении новой точки прогноза.

✗ Неправильно

Не используйте точки прогнозирования для окончательной отрисовки.

Приложения для заметок

ChromeOS позволяет приложению объявлять некоторые действия по ведению заметок.

Чтобы зарегистрировать приложение для ведения заметок в ChromeOS, ознакомьтесь с разделом Совместимость ввода.

Чтобы зарегистрировать приложение для заметок на Android, ознакомьтесь со статьей Как создать приложение для заметок.

В Android 14 (уровень API 34) появился объект intent ACTION_CREATE_NOTE, который позволяет приложению запускать действие по созданию заметок на заблокированном экране.

Распознавание рукописного ввода с помощью ML Kit

С помощью ML Kit Digital Ink Recognition ваше приложение сможет распознавать рукописный текст на цифровой поверхности на сотнях языков. Вы также можете классифицировать наброски.

В ML Kit есть класс Ink.Stroke.Builder, который позволяет создавать объекты Ink, обрабатываемые моделями машинного обучения для преобразования рукописного ввода в текст.

Помимо распознавания рукописного текста, модель способна распознавать жесты, такие как удаление и обведение.

Подробнее о распознавании рукописного ввода…

Дополнительные ресурсы

Руководства для разработчиков

Практические работы

,

Android и ChromeOS предоставляют различные API, которые помогают создавать приложения, обеспечивающие пользователям исключительное удобство работы со стилусом. Класс MotionEvent предоставляет информацию о взаимодействии стилуса с экраном, включая силу нажатия, ориентацию, наклон, наведение и распознавание ладони. Библиотеки для прогнозирования движения и графики с низкой задержкой улучшают рендеринг стилуса на экране, чтобы обеспечить естественное ощущение, как при письме ручкой на бумаге.

MotionEvent

Класс MotionEvent представляет действия пользователя, например положение и движение указателей на экране. Для ввода стилусом MotionEvent также предоставляет данные о давлении, ориентации, наклоне и наведении.

Сведения о событии

Чтобы получить доступ к объектам MotionEvent стилуса, добавьте модификатор pointerInteropFilter к поверхности рисования. Реализуйте класс ViewModel с методом, который обрабатывает события движения. Передайте метод в качестве лямбда-выражения onTouchEvent модификатора pointerInteropFilter:

@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
    Canvas(
        modifier = modifier
            .clipToBounds()
            .pointerInteropFilter {
                viewModel.processMotionEvent(it)
            }

    ) {
        // Drawing code here.
    }
}

Объект MotionEvent предоставляет данные, связанные со следующими аспектами события пользовательского интерфейса:

  • Действия – физическое взаимодействие с устройством, например касание экрана, перемещение указателя по экрану или наведение указателя на экран.
  • Указатели: идентификаторы объектов, взаимодействующих с экраном (палец, стилус, мышь).
  • Axis: тип данных – координаты x и y, давление, наклон, ориентация и наведение (расстояние).

Действия

Чтобы реализовать поддержку стилуса, вам нужно понять, какое действие выполняет пользователь.

В классе MotionEvent есть множество констант ACTION, которые определяют события движения. Вот основные действия, которые можно выполнять с помощью стилуса:

Действие Описание
ACTION_DOWN
ACTION_POINTER_DOWN
Указатель коснулся экрана.
ACTION_MOVE Указатель перемещается по экрану.
ACTION_UP
ACTION_POINTER_UP
Указатель больше не касается экрана.
ACTION_CANCEL Когда предыдущий или текущий набор движений должен быть отменен.

Приложение может выполнять такие задачи, как начало нового штриха при возникновении ACTION_DOWN, рисование штриха с помощью ACTION_MOVE, и завершение штриха при срабатывании ACTION_UP.

Набор действий MotionEvent от ACTION_DOWN до ACTION_UP для заданного указателя называется набором движений.

Указатели

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

Индексы указателей варьируются от нуля до количества указателей, возвращаемых методом MotionEvent#pointerCount(), минус 1.

Значения осей указателей можно получить с помощью метода getAxisValue(axis, pointerIndex). Если индекс указателя не задан, система возвращает значение для первого указателя (0).

Объекты MotionEvent содержат информацию о типе используемого указателя. Чтобы узнать тип указателя, переберите индексы указателей и вызовите метод getToolType(pointerIndex).

Подробнее об указателях…

Ввод стилусом

Вы можете отфильтровать ввод стилусом с помощью TOOL_TYPE_STYLUS:

val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)

Стилус также может сообщать, что он используется в качестве ластика, с помощью TOOL_TYPE_ERASER:

val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)

Данные об осях стилуса

ACTION_DOWN и ACTION_MOVE предоставляют данные о стилусе, а именно координаты x и y, давление, ориентацию, наклон и наведение.

Чтобы предоставить доступ к этим данным, API MotionEvent предоставляет getAxisValue(int), где параметр – это любой из следующих идентификаторов осей:

Axis Возвращаемое значение getAxisValue()
AXIS_X Координата X события движения.
AXIS_Y Координата Y события движения.
AXIS_PRESSURE Для сенсорного экрана или сенсорной панели – сила нажатия пальцем, стилусом или другим указателем. Для мыши или трекбола: 1, если нажата основная кнопка, и 0, если нет.
AXIS_ORIENTATION Для сенсорного экрана или сенсорной панели – ориентация пальца, стилуса или другого указателя относительно вертикальной плоскости устройства.
AXIS_TILT Угол наклона стилуса в радианах.
AXIS_DISTANCE Расстояние от стилуса до экрана.

Например, MotionEvent.getAxisValue(AXIS_X) возвращает координату X для первого указателя.

Подробнее о том, как обрабатывать жесты с несколькими касаниями…

Позиция

Вы можете получить координаты x и y указателя с помощью следующих вызовов:

Стилус рисует на экране с координатами x и y.
Рисунок 1. Координаты X и Y указателя стилуса на экране.

Давление

Силу нажатия можно получить с помощью метода MotionEvent#getAxisValue(AXIS_PRESSURE) или, для первого указателя, MotionEvent#getPressure().

Значение силы нажатия для сенсорных экранов и панелей находится в диапазоне от 0 (нет нажатия) до 1, но в зависимости от калибровки экрана могут возвращаться и более высокие значения.

Штрих стилуса, представляющий континуум от низкого до высокого давления. Слева штрих узкий и бледный, что указывает на низкое давление. Штрих становится шире и темнее слева направо, пока не станет самым широким и темным справа, что указывает на самое высокое давление.
Рисунок 2. Представление давления: низкое давление слева, высокое давление справа.

Ориентация

Ориентация указывает, в каком направлении указывает стилус.

Ориентацию указателя можно получить с помощью getAxisValue(AXIS_ORIENTATION) или getOrientation() (для первого указателя).

Для стилуса ориентация возвращается в виде значения в радианах от 0 до pi (𝛑) по часовой стрелке или от 0 до -pi против часовой стрелки.

Ориентация позволяет использовать настоящую кисть. Например, если стилус представляет собой плоскую кисть, ширина плоской кисти зависит от ориентации стилуса.

Рисунок 3. Стилус указывает влево примерно на минус 0,57 радиана.

Наклон

Наклон – это угол между стилусом и экраном.

Наклон возвращает положительный угол стилуса в радианах, где ноль – перпендикулярно экрану, а 𝛑/2 – плоско на поверхности.

Угол наклона можно получить с помощью getAxisValue(AXIS_TILT) (для первого указателя сочетания клавиш нет).

Наклон позволяет максимально точно воспроизвести реальные инструменты, например имитировать штриховку наклоненным карандашом.

Стилус наклонен примерно на 40 градусов относительно поверхности экрана.
Рисунок 4. Стилус наклонен примерно на 0,785 радиана, или 45 градусов от перпендикуляра.

Наведите указатель

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

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

Рисунок 5. Стилус над экраном. Приложение реагирует, даже если стилус не касается поверхности экрана.

Примечание. Compose предоставляет модификаторы, которые влияют на интерактивное состояние элементов пользовательского интерфейса:

  • hoverable – настроить компонент так, чтобы он реагировал на наведение указателя.
  • indication – рисует визуальные эффекты для этого компонента при взаимодействии.

Игнорирование касаний ладонью, навигация и случайные вводы

Иногда мультисенсорные экраны могут регистрировать нежелательные касания, например, когда пользователь кладет руку на экран для поддержки во время рукописного ввода. Функция игнорирования касаний ладонью обнаруживает такие действия и сообщает, что последний набор MotionEvent нужно отменить.

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

ACTION_CANCEL и FLAG_CANCELED

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

ACTION_CANCEL

Добавлено в Android 1.0 (уровень API 1)

ACTION_CANCEL указывает, что предыдущий набор событий движения должен быть отменен.

ACTION_CANCEL срабатывает, когда обнаруживается любое из следующих событий:

  • Жесты для навигации
  • Игнорирование касаний ладонью

При активации ACTION_CANCEL необходимо определить активный указатель с помощью getPointerId(getActionIndex()). Затем удалите штрих, созданный этим указателем, из истории ввода и повторно отрисуйте сцену.

FLAG_CANCELED

Добавлено в Android 13 (уровень API 33)

FLAG_CANCELED Указывает, что указатель был поднят в результате непреднамеренного касания. Обычно этот флаг устанавливается, когда пользователь случайно касается экрана, например, держа устройство или кладя ладонь на экран.

Чтобы получить доступ к значению параметра, выполните следующие действия:

val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED

Если флаг установлен, вам нужно отменить последний набор MotionEvent из последнего ACTION_DOWN от этого указателя.

Как и в случае с ACTION_CANCEL, указатель можно найти с помощью getPointerId(actionIndex).

Рисунок 6. Штрих стилуса и касание ладонью создают наборы MotionEvent. Касание ладонью отменяется, и экран обновляется.

Полноэкранный режим, отображение на весь экран и навигация жестами

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

Рисунок 7. Жест, позволяющий перевести приложение в фоновый режим.

Чтобы жесты не вызывали нежелательные нажатия в приложении, вы можете использовать отступы и ACTION_CANCEL.

Также ознакомьтесь с разделом Игнорирование касаний ладонью, навигация и нежелательные действия.

Чтобы жесты навигации не вызывали нежелательные события касания, используйте метод setSystemBarsBehavior() и свойство BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE объекта WindowInsetsController.

// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

Дополнительную информацию о вставках и управлении жестами можно найти в следующих статьях:

Малая задержка

Задержка – это время, которое требуется оборудованию, системе и приложению, чтобы обработать и отобразить действия пользователя.

Задержка = обработка ввода аппаратным обеспечением и ОС + обработка приложением + компоновка системой

  • аппаратная отрисовка
Задержка приводит к тому, что отрисованный штрих отстает от положения стилуса. Промежуток между нарисованным штрихом и положением стилуса представляет собой задержку.
Рисунок 8. Задержка приводит к тому, что отрисованный штрих отстает от положения стилуса.

Источник задержки

  • Регистрация стилуса на сенсорном экране (аппаратное обеспечение). Первоначальное беспроводное подключение, при котором стилус и ОС обмениваются данными для регистрации и синхронизации.
  • Частота дискретизации касаний (аппаратная) – количество проверок в секунду, выполняемых сенсорным экраном, чтобы определить, касается ли указатель поверхности. Диапазон значений: от 60 до 1000 Гц.
  • Обработка входных данных (приложение): применение цвета, графических эффектов и преобразования к входным данным пользователя.
  • Отрисовка графики (ОС + аппаратное обеспечение): переключение буферов, аппаратная обработка.

Графика с низкой задержкой

Библиотека Jetpack для графики с низкой задержкой сокращает время обработки между вводом пользователя и отрисовкой на экране.

Библиотека сокращает время обработки, избегая многобуферного рендеринга и используя технику рендеринга переднего буфера, что означает запись непосредственно на экран.

Отрисовка переднего буфера

Передний буфер – это память, которую экран использует для рендеринга. Это самый близкий к экрану уровень, на котором могут рисовать приложения. Библиотека с низкой задержкой позволяет приложениям выполнять рендеринг непосредственно в передний буфер. Это повышает производительность, предотвращая замену буфера, которая может происходить при обычном рендеринге с несколькими буферами или рендеринге с двойным буфером (наиболее распространенный случай).

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

Отрисовка переднего буфера – отличный способ обновить небольшую область экрана, но не весь экран. При отрисовке в переднем буфере приложение отрисовывает контент в буфер, из которого дисплей считывает данные. В результате могут возникать артефакты рендеринга или разрывы (см. ниже).

Библиотека с низкой задержкой доступна на устройствах Android 10 (уровень API 29) и более поздних версий, а также на устройствах с ChromeOS, работающих на Android 10 (уровень API 29) и более поздних версий.

Связанные запросы

Библиотека с низкой задержкой предоставляет компоненты для реализации отрисовки переднего буфера. Библиотека добавляется в качестве зависимости в файл build.gradle модуля приложения:

dependencies {
    implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}

Обратные вызовы GLFrontBufferRenderer

Библиотека с низкой задержкой включает интерфейс GLFrontBufferRenderer.Callback, который определяет следующие методы:

Библиотека с низкой задержкой не имеет ограничений по типу данных, которые вы используете с GLFrontBufferRenderer.

Однако библиотека обрабатывает данные как поток из сотен элементов, поэтому оптимизируйте использование и распределение памяти.

Обратные вызовы

Чтобы включить обратные вызовы отрисовки, реализуйте GLFrontBufferedRenderer.Callback и переопределите onDrawFrontBufferedLayer() и onDrawDoubleBufferedLayer(). GLFrontBufferedRenderer использует обратные вызовы, чтобы отображать данные наиболее оптимальным образом.

val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> {
   override fun onDrawFrontBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       param: DATA_TYPE
   ) {
       // OpenGL for front buffer, short, affecting small area of the screen.
   }
   override fun onDrawMultiDoubleBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       params: Collection<DATA_TYPE>
   ) {
       // OpenGL full scene rendering.
   }
}
Объявите экземпляр GLFrontBufferedRenderer

Подготовьте GLFrontBufferedRenderer, указав SurfaceView и обратные вызовы, созданные ранее. GLFrontBufferedRenderer оптимизирует отрисовку в переднем и двойном буферах, используя ваши обратные вызовы:

var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Отрисовка

Отрисовка переднего буфера начинается при вызове метода renderFrontBufferedLayer(), который запускает обратный вызов onDrawFrontBufferedLayer().

Отрисовка с двойной буферизацией возобновляется при вызове функции commit(), которая запускает обратный вызов onDrawMultiDoubleBufferedLayer().

В примере ниже процесс выполняет рендеринг в передний буфер (быстрый рендеринг), когда пользователь начинает рисовать на экране (ACTION_DOWN) и перемещает указатель (ACTION_MOVE). Процесс выполняет рендеринг в двойной буфер, когда указатель покидает поверхность экрана (ACTION_UP).

Вы можете использовать requestUnbufferedDispatch(), чтобы система ввода не объединяла события движения в пакеты, а доставляла их по мере доступности:

when (motionEvent.action) {
   MotionEvent.ACTION_DOWN -> {
       // Deliver input events as soon as they arrive.
       view.requestUnbufferedDispatch(motionEvent)
       // Pointer is in contact with the screen.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_MOVE -> {
       // Pointer is moving.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_UP -> {
       // Pointer is not in contact in the screen.
       glFrontBufferRenderer.commit()
   }
   MotionEvent.CANCEL -> {
       // Cancel front buffer; remove last motion set from the screen.
       glFrontBufferRenderer.cancel()
   }
}

Рекомендации по отрисовке

✓ Рекомендуется

Небольшие части экрана, рукописный ввод, рисование, создание эскизов.

✗ Неправильно

Обновление полноэкранного режима, панорамирование, масштабирование. Может вызвать слезотечение.

Разрыв

Разрывы возникают, когда экран обновляется одновременно с изменением буфера экрана. На одной части экрана показываются новые данные, а на другой – старые.

Верхняя и нижняя части изображения Android не выровнены из-за разрыва при обновлении экрана.
Рисунок 11. Разрывы изображения при обновлении экрана сверху вниз.

Прогнозирование движения

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

Библиотека прогнозирования движения получает реальные пользовательские входные данные в виде объектов MotionEvent. Объекты содержат информацию о координатах x и y, давлении и времени, которая используется прогнозировщиком движения для прогнозирования будущих объектов MotionEvent.

Прогнозируемое количество объектов (MotionEvent) является приблизительным. Прогнозируемые события могут уменьшить воспринимаемую задержку, но прогнозируемые данные должны быть заменены фактическими данными MotionEvent после их получения.

Библиотека прогнозирования движения доступна в Android 4.4 (уровень API 19) и более поздних версиях, а также на устройствах с ChromeOS под управлением Android 9 (уровень API 28) и более поздних версий.

Задержка приводит к тому, что отрисованный штрих отстает от положения стилуса. Промежуток между штрихом и стилусом заполняется точками прогнозирования. Оставшаяся разница – это воспринимаемая задержка.
Рисунок 12. Задержка уменьшена за счет прогнозирования движений.

Связанные запросы

Библиотека прогнозирования движения предоставляет реализацию прогнозирования. Библиотека добавляется в качестве зависимости в файл build.gradle модуля приложения:

dependencies {
    implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}

Реализация

Библиотека прогнозирования движения включает интерфейс MotionEventPredictor, который определяет следующие методы:

  • record() хранит объекты MotionEvent как записи о действиях пользователя.
  • predict(): возвращает прогнозируемое значение MotionEvent.
Объявите экземпляр MotionEventPredictor
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Передайте данные в предиктор
motionEventPredictor.record(motionEvent)
Прогноз

when (motionEvent.action) {
   MotionEvent.ACTION_MOVE -> {
       val predictedMotionEvent = motionEventPredictor?.predict()
       if(predictedMotionEvent != null) {
            // use predicted MotionEvent to inject a new artificial point
       }
   }
}

Рекомендации по прогнозированию движения

✓ Рекомендуется

Удалять точки прогноза при добавлении новой точки прогноза.

✗ Неправильно

Не используйте точки прогнозирования для окончательной отрисовки.

Приложения для заметок

ChromeOS позволяет приложению объявлять некоторые действия по ведению заметок.

Чтобы зарегистрировать приложение для ведения заметок в ChromeOS, ознакомьтесь с разделом Совместимость ввода.

Чтобы зарегистрировать приложение для заметок на Android, ознакомьтесь со статьей Как создать приложение для заметок.

В Android 14 (уровень API 34) появился объект intent ACTION_CREATE_NOTE, который позволяет приложению запускать действие по созданию заметок на заблокированном экране.

Распознавание рукописного ввода с помощью ML Kit

С помощью ML Kit Digital Ink Recognition ваше приложение сможет распознавать рукописный текст на цифровой поверхности на сотнях языков. Вы также можете классифицировать наброски.

В ML Kit есть класс Ink.Stroke.Builder, который позволяет создавать объекты Ink, обрабатываемые моделями машинного обучения для преобразования рукописного ввода в текст.

Помимо распознавания рукописного текста, модель способна распознавать жесты, такие как удаление и обведение.

Подробнее о распознавании рукописного ввода…

Дополнительные ресурсы

Руководства для разработчиков

Практические работы