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

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

MotionEvent

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

Данные о событии

Чтобы получить доступ к данным MotionEvent , добавьте к компонентам модификатор pointerInput :

@Composable
fun Greeting() {
    Text(
        text = "Hello, Android!", textAlign = TextAlign.Center, style = TextStyle(fontSize = 5.em),
        modifier = Modifier
            .pointerInput(Unit) {
                awaitEachGesture {
                    while (true) {
                        val event = awaitPointerEvent()
                        event.changes.forEach { println(it) }
                    }
                }
            },
    )
}

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

  • Действия: Физическое взаимодействие с устройством — прикосновение к экрану, перемещение указателя по поверхности экрана, наведение указателя на поверхность экрана.
  • Указатели: идентификаторы объектов, взаимодействующих с экраном: палец, стилус, мышь.
  • Ось: тип данных — координаты 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) , где параметром является любой из следующих идентификаторов оси:

Ось Возвращаемое значение 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 до пи (𝛑) по часовой стрелке или от 0 до -пи против часовой стрелки.

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

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

Наклон

Tilt измеряет наклон стилуса относительно экрана.

Наклон возвращает положительный угол стилуса в радианах, где ноль перпендикулярен экрану, а 𝛑/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() ) . Затем удалите обводку, созданную этим указателем, из истории ввода и повторно отрисуйте сцену.

ФЛАГ_ОТМЕНЕН

Добавлено в 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 .

См. также раздел «Отклонение Palm, навигация и нежелательные вводы» .

Используйте метод 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) появилось намерение ACTION_CREATE_NOTE , которое позволяет вашему приложению запускать действия по созданию заметок на экране блокировки.

Распознавание цифровых чернил с помощью ML Kit

Благодаря распознаванию цифровых рукописных знаков ML Kit ваше приложение может распознавать рукописный текст на цифровой поверхности на сотнях языков. Вы также можете классифицировать эскизы.

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

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

Дополнительную информацию см. в разделе Распознавание цифровых рукописей .

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

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

Кодлабы