Существует несколько терминов и концепций, которые важно понимать при работе над обработкой жестов в приложении. На этой странице объясняются термины указатели, события указателя и жесты, а также представлены различные уровни абстракции жестов. Он также глубже погружается в потребление и распространение событий.
Определения
Чтобы понять различные концепции на этой странице, вам необходимо понимать некоторые используемые термины:
- Указатель : физический объект, который вы можете использовать для взаимодействия с вашим приложением. Для мобильных устройств наиболее распространенным указателем является палец, взаимодействующий с сенсорным экраном. Альтернативно вы можете использовать стилус вместо пальца. На больших экранах вы можете использовать мышь или трекпад для косвенного взаимодействия с дисплеем. Устройство ввода должно иметь возможность «указывать» на координату, чтобы считаться указателем, поэтому клавиатура, например, не может считаться указателем. В Compose тип указателя включается в изменения указателя с помощью
PointerType
. - Событие указателя : описывает низкоуровневое взаимодействие одного или нескольких указателей с приложением в определенный момент времени. Любое взаимодействие с указателем, например, прикосновение пальца к экрану или перетаскивание мыши, вызовет событие. В Compose вся необходимая информация для такого события содержится в классе
PointerEvent
. - Жест : последовательность событий указателя, которую можно интерпретировать как одно действие. Например, жест касания можно рассматривать как последовательность событий «вниз», за которыми следует событие «вверх». Существуют общие жесты, используемые во многих приложениях, такие как касание, перетаскивание или преобразование, но при необходимости вы также можете создать свой собственный жест.
Различные уровни абстракции
Jetpack Compose предоставляет различные уровни абстракции для обработки жестов. На верхнем уровне находится поддержка компонентов . Составные элементы, такие как Button
автоматически включают поддержку жестов. Чтобы добавить поддержку жестов в пользовательские компоненты, вы можете добавить модификаторы жестов , такие как clickable
к произвольным составным объектам. Наконец, если вам нужен собственный жест, вы можете использовать модификатор pointerInput
.
Как правило, используйте самый высокий уровень абстракции, который предлагает необходимую вам функциональность. Таким образом, вы сможете воспользоваться лучшими практиками, включенными в этот слой. Например, Button
содержит больше семантической информации, используемой для доступности, чем clickable
, который содержит больше информации, чем необработанная реализация pointerInput
.
Поддержка компонентов
Многие готовые компоненты Compose включают в себя своего рода внутреннюю обработку жестов. Например, LazyColumn
реагирует на жесты перетаскивания, прокручивая свое содержимое, Button
показывает пульсацию при нажатии на него, а компонент SwipeToDismiss
содержит логику смахивания для закрытия элемента. Этот тип обработки жестов работает автоматически.
Помимо внутренней обработки жестов, многие компоненты также требуют, чтобы вызывающая сторона обрабатывала жест. Например, Button
автоматически обнаруживает касания и запускает событие щелчка. Вы передаете лямбду onClick
Button
чтобы отреагировать на жест. Аналогичным образом вы добавляете лямбда-выражение onValueChange
к Slider
, чтобы реагировать на то, что пользователь перетаскивает дескриптор ползунка.
Если это соответствует вашему варианту использования, отдавайте предпочтение жестам, включенным в компоненты, поскольку они включают готовую поддержку фокусировки и доступности и хорошо протестированы. Например, Button
помечается особым образом, чтобы службы специальных возможностей правильно описывали ее как кнопку, а не как любой кликабельный элемент:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
Дополнительные сведения о специальных возможностях в Compose см. в разделе Специальные возможности в Compose .
Добавляйте определенные жесты к произвольным составным объектам с помощью модификаторов.
Вы можете применить модификаторы жестов к любому произвольному составному объекту, чтобы он прослушивал жесты. Например, вы можете позволить обычному Box
обрабатывать жесты касания, сделав его clickable
, или позволить Column
обрабатывать вертикальную прокрутку, verticalScroll
.
Существует множество модификаторов для обработки различных типов жестов:
- Обрабатывайте касания и нажатия с помощью модификаторов
clickable
,combinedClickable
,selectable
,toggleable
иtriStateToggleable
. - Управляйте прокруткой с помощью
horizontalScroll
,verticalScroll
и более общих модификаторовscrollable
. - Управляйте перетаскиванием с помощью модификатора
draggable
иswipeable
. - Управляйте мультитач-жестами, такими как панорамирование, вращение и масштабирование, с помощью
transformable
модификатора.
Как правило, отдавайте предпочтение готовым модификаторам жестов, а не пользовательской обработке жестов. Модификаторы добавляют больше функциональности помимо обработки событий чистого указателя. Например, clickable
модификатор не только добавляет обнаружение нажатий и касаний, но также добавляет семантическую информацию, визуальные индикаторы взаимодействия, наведения, фокуса и поддержку клавиатуры. Вы можете проверить исходный код clickable
, чтобы увидеть, как добавляется функциональность.
Добавьте собственный жест к произвольным составным объектам с помощью модификатора pointerInput
Не каждый жест реализован с помощью готовых модификаторов жестов. Например, вы не можете использовать модификатор для реакции на перетаскивание после длительного нажатия, щелчка с нажатой клавишей Control или касания тремя пальцами. Вместо этого вы можете написать собственный обработчик жестов для идентификации этих пользовательских жестов. Вы можете создать обработчик жестов с помощью модификатора pointerInput
, который предоставит вам доступ к необработанным событиям указателя.
Следующий код прослушивает события необработанного указателя:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
Если разобрать этот фрагмент, то основными компонентами будут:
- Модификатор
pointerInput
. Вы передаете ему один или несколько ключей . Когда значение одного из этих ключей изменяется, лямбда-выражение содержимого модификатора выполняется повторно. Образец передает дополнительный фильтр в составной элемент. Если значение этого фильтра изменится, обработчик событий указателя должен быть выполнен повторно, чтобы убедиться, что регистрируются правильные события. -
awaitPointerEventScope
создает область сопрограммы, которую можно использовать для ожидания событий указателя. -
awaitPointerEvent
приостанавливает выполнение сопрограммы до тех пор, пока не произойдет следующее событие указателя.
Хотя прослушивание необработанных входных событий является мощным инструментом, также сложно написать собственный жест на основе этих необработанных данных. Для упрощения создания пользовательских жестов доступно множество служебных методов.
Обнаружение полных жестов
Вместо обработки необработанных событий указателя вы можете прослушивать определенные жесты и реагировать соответствующим образом. AwaitPointerEventScope
предоставляет методы для прослушивания:
- Нажмите, коснитесь, двойное касание и долгое нажатие:
detectTapGestures
- Перетаскивание:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
иdetectDragGesturesAfterLongPress
- Преобразования:
detectTransformGestures
Это детекторы верхнего уровня, поэтому вы не можете добавить несколько детекторов в один модификатор pointerInput
. Следующий фрагмент обнаруживает только касания, а не перетаскивания:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
Внутри метод detectTapGestures
блокирует сопрограмму, и второй детектор никогда не достигается. Если вам нужно добавить более одного прослушивателя жестов в компонуемый объект, вместо этого используйте отдельные экземпляры модификатора pointerInput
:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
Обработка событий за жест
По определению, жесты начинаются с события перемещения указателя вниз. Вы можете использовать вспомогательный метод awaitEachGesture
вместо цикла while(true)
, который проходит через каждое необработанное событие. Метод awaitEachGesture
перезапускает содержащий блок, когда все указатели подняты, указывая, что жест завершен:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
На практике вы почти всегда захотите использовать awaitEachGesture
если только вы не отвечаете на события указателя, не распознавая жесты. Примером этого является hoverable
, который не реагирует на события перемещения указателя вниз или вверх — ему просто нужно знать, когда указатель входит или выходит за свои границы.
Дождитесь определенного события или поджеста
Существует набор методов, которые помогают идентифицировать общие части жестов:
- Приостановите работу до тех пор, пока указатель не опустится с помощью
awaitFirstDown
, или подождите, пока все указатели поднимутся с помощьюwaitForUpOrCancellation
. - Создайте низкоуровневый прослушиватель перетаскивания, используя
awaitTouchSlopOrCancellation
иawaitDragOrCancellation
. Обработчик жестов сначала приостанавливается до тех пор, пока указатель не достигнет точки касания, а затем приостанавливается до тех пор, пока не произойдет первое событие перетаскивания. Если вас интересует перетаскивание только по одной оси, используйте вместо этогоawaitHorizontalTouchSlopOrCancellation
плюсawaitHorizontalDragOrCancellation
илиawaitVerticalTouchSlopOrCancellation
плюсawaitVerticalDragOrCancellation
. - Приостановите действие до тех пор, пока не произойдет долгое нажатие с помощью
awaitLongPressOrCancellation
. - Используйте метод
drag
для непрерывного прослушивания событий перетаскивания илиhorizontalDrag
илиverticalDrag
для прослушивания событий перетаскивания по одной оси.
Применяйте вычисления для событий мультитач
Когда пользователь выполняет мультитач-жест, используя более одного указателя, сложно понять необходимое преобразование на основе необработанных значений. Если модификатор transformable
или методы detectTransformGestures
не обеспечивают достаточно детального управления для вашего варианта использования, вы можете прослушивать необработанные события и применять к ним вычисления. Этими вспомогательными методами являются calculateCentroid
, calculateCentroidSize
, calculatePan
, calculateRotation
и calculateZoom
.
Диспетчеризация событий и хит-тестирование
Не каждое событие указателя отправляется каждому модификатору pointerInput
. Диспетчеризация событий работает следующим образом:
- События указателя отправляются в составную иерархию . В тот момент, когда новый указатель запускает свое первое событие указателя, система начинает проверку «подходящих» компонуемых объектов. Составной объект считается подходящим, если он имеет возможности обработки ввода указателя. Хит-тестирование проходит от верхней части дерева пользовательского интерфейса к нижней. Составной объект считается «попадающим», когда событие указателя произошло в пределах этого составного объекта. В результате этого процесса образуется цепочка компонуемых объектов , которая проходит проверку на попадание положительно.
- По умолчанию, когда на одном уровне дерева имеется несколько подходящих компонуемых объектов, «попаданием» считается только компонуемый объект с самым высоким z-индексом. Например, когда вы добавляете два перекрывающихся составных объекта
Button
вBox
, только тот, который нарисован сверху, получает любые события указателя. Теоретически это поведение можно переопределить, создав собственную реализациюPointerInputModifierNode
и установив дляsharePointerInputWithSiblings
значение true. - Дальнейшие события для того же указателя отправляются в ту же самую цепочку компонуемых объектов и выполняются в соответствии с логикой распространения событий . Система больше не выполняет проверку попадания для этого указателя. Это означает, что каждый составной объект в цепочке получает все события для этого указателя, даже если они происходят за пределами этого составного объекта. Составные объекты, не входящие в цепочку, никогда не получают события указателя, даже если указатель находится внутри их границ.
События наведения, вызванные наведением мыши или стилуса, являются исключением из определенных здесь правил. События наведения передаются любому составному объекту, к которому они попадают. Таким образом, когда пользователь наводит указатель от границ одного составного объекта к другому, вместо отправки событий в этот первый составной объект события отправляются в новый составной объект.
Потребление событий
Если более чем одному составному объекту назначен обработчик жестов, эти обработчики не должны конфликтовать. Например, взгляните на этот пользовательский интерфейс:
Когда пользователь нажимает кнопку закладки, лямбда-выражение onClick
кнопки обрабатывает этот жест. Когда пользователь нажимает на любую другую часть элемента списка, ListItem
обрабатывает этот жест и переходит к статье. С точки зрения ввода указателя, кнопка должна использовать это событие, чтобы ее родительский элемент знал, что на него больше нельзя реагировать. Жесты, включенные в готовые компоненты, и общие модификаторы жестов включают такое поведение потребления, но если вы пишете свой собственный жест, вам придется обрабатывать события вручную. Вы делаете это с помощью метода PointerInputChange.consume
:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Потребление события не останавливает распространение события на другие компонуемые объекты. Вместо этого компонуемый объект должен явно игнорировать потребляемые события. При написании пользовательских жестов следует проверить, не было ли событие уже использовано другим элементом:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
Распространение событий
Как упоминалось ранее, изменения указателя передаются каждому составному объекту, к которому он попадает. Но если существует более одного такого составного объекта, в каком порядке распространяются события? Если вы возьмете пример из последнего раздела, этот пользовательский интерфейс преобразуется в следующее дерево пользовательского интерфейса, где только ListItem
и Button
реагируют на события указателя:
События указателя проходят через каждый из этих компонуемых объектов три раза в течение трех «проходов»:
- На начальном проходе событие течет от верхней части дерева пользовательского интерфейса к нижней. Этот поток позволяет родителю перехватить событие до того, как дочерний элемент сможет его использовать. Например, всплывающие подсказки должны перехватывать длительное нажатие , а не передавать их своим детям. В нашем примере
ListItem
получает событие передButton
. - На главном проходе событие течет от конечных узлов дерева пользовательского интерфейса до корня дерева пользовательского интерфейса. На этом этапе вы обычно используете жесты и является этапом по умолчанию при прослушивании событий. Обработка жестов на этом проходе означает, что конечные узлы имеют приоритет над своими родительскими узлами, что является наиболее логичным поведением для большинства жестов. В нашем примере
Button
получает событие передListItem
. - На последнем проходе событие еще раз передается от вершины дерева пользовательского интерфейса к конечным узлам. Этот поток позволяет элементам, расположенным выше в стеке, реагировать на событие, полученное их родителем. Например, кнопка удаляет индикацию пульсации, когда нажатие превращается в перетаскивание ее прокручиваемого родительского элемента.
Визуально поток событий можно представить следующим образом:
Как только входное изменение будет использовано, эта информация передается с этой точки потока дальше:
В коде вы можете указать интересующий вас проход:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
В этом фрагменте кода каждое из этих вызовов метода await возвращает одно и то же идентичное событие, хотя данные о потреблении могли измениться.
Тестовые жесты
В ваших методах тестирования вы можете вручную отправлять события указателя с помощью метода performTouchInput
. Это позволяет выполнять либо полные жесты более высокого уровня (например, сведение пальцем или долгое нажатие), либо жесты низкого уровня (например, перемещение курсора на определенное количество пикселей):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Дополнительные примеры см. в документации performTouchInput
.
Узнать больше
Вы можете узнать больше о жестах в Jetpack Compose из следующих ресурсов:
{% дословно %}Рекомендуется для вас
- Примечание. Текст ссылки отображается, когда JavaScript отключен.
- Доступность в Compose
- Прокрутка
- Коснитесь и нажмите