При работе с обработкой жестов в приложении важно понимать несколько терминов и концепций. На этой странице объясняются термины указатели, события указателя и жесты, а также представлены различные абстракции. уровни для жестов. Также мы подробнее рассмотрим потребление и распространение событий.
Определения
Чтобы понять, о чем идет речь на этой странице, ознакомьтесь с терминологией:
- Указатель. Физический объект, с помощью которого можно взаимодействовать с приложением.
Для мобильных устройств наиболее распространенным указателем является ваш палец, взаимодействующий с сенсорным экраном. Вместо пальца можно использовать стилус.
На больших экранах можно использовать мышь или сенсорную панель для косвенного взаимодействия с дисплеем. Устройство ввода должно иметь возможность указывать на координаты, чтобы считаться указателем. Например, клавиатура не может считаться указателем. В Compose тип указателя включается в изменения указателя с помощью
PointerType. - Событие указателя описывает низкоуровневое взаимодействие одного или нескольких указателей с приложением в определенный момент времени. Любое взаимодействие с указателем, например касание экрана пальцем или перетаскивание мыши, запускает событие. В Compose вся информация о таком событии содержится в классе
PointerEvent. - Жест. Последовательность событий указателя, которые можно интерпретировать как одно действие. Например, нажатие можно рассматривать как последовательность событий нажатия и отпускания. Существуют распространенные жесты, которые используются во многих приложениях, например нажатие, перетаскивание или преобразование, но при необходимости вы также можете создать свой собственный жест.
Разные уровни абстракции
Jetpack Compose предоставляет разные уровни абстракции для обработки жестов.
На верхнем уровне находится поддержка компонентов. Такие composable-функции, как 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…
Добавление определенных жестов к произвольным составным элементам с помощью модификаторов
Вы можете применить модификаторы жестов к любой произвольной composable-функции, чтобы сделать ее восприимчивой к жестам. Например, вы можете разрешить универсальному Box обрабатывать жесты нажатия, сделав его clickable, или разрешить Column обрабатывать вертикальную прокрутку, применив verticalScroll.
Существует множество модификаторов для обработки различных типов жестов:
- Обрабатывайте нажатия с помощью модификаторов
clickable,combinedClickable,selectable,toggleableиtriStateToggleable. - Управляйте прокруткой с помощью модификаторов
horizontalScroll,verticalScrollи более общегоscrollable. - Перетаскивание с помощью модификатора
draggableиswipeable. - Обрабатывайте жесты с несколькими касаниями, такие как панорамирование, поворот и масштабирование, с помощью модификатора
transformable.
Как правило, предпочитайте готовые модификаторы жестов пользовательской обработке жестов.
Модификаторы добавляют больше функциональности поверх чистой обработки событий указателя.
Например, модификатор clickable не только добавляет обнаружение нажатий и касаний, но также добавляет семантическую информацию, визуальные индикаторы взаимодействия, наведение, фокус и поддержку клавиатуры. Вы можете посмотреть исходный код clickable, чтобы узнать, как добавляются функции.
Добавление пользовательского жеста в произвольные composable-функции с помощью модификатора pointerInput
Не каждый жест реализуется с помощью готового модификатора жестов. Например, вы не можете использовать модификатор, чтобы реагировать на перетаскивание после долгого нажатия, нажатие клавиши Ctrl или касание тремя пальцами. Вместо этого вы можете написать собственный обработчик жестов для идентификации этих пользовательских жестов. Вы можете создать обработчик жестов с помощью модификатора 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. Вы передаете ему один или несколько ключей. Когда значение одного из этих ключей меняется, лямбда-выражение содержимого модификатора выполняется повторно. В примере в composable-функцию передается необязательный фильтр. Если значение фильтра изменится, обработчик событий указателя должен быть выполнен повторно, чтобы обеспечить регистрацию правильных событий. 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 блокирует сопрограмму, и второй детектор никогда не достигается. Если вам нужно добавить несколько прослушивателей жестов в composable-функцию, используйте отдельные экземпляры модификатора 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" } } ) }
Обработка событий для каждого жеста
По определению, жесты начинаются с события нажатия указателя. Вместо цикла while(true), который проходит по всем необработанным событиям, можно использовать вспомогательный метод awaitEachGesture. Метод 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. Отправка событий работает следующим образом:
- События указателя отправляются в иерархию composable-функций. В момент, когда новый указатель запускает свое первое событие указателя, система начинает проверку нажатия "подходящих" composable-функций. Composable-функция считается подходящей, если она имеет возможности обработки ввода указателя. Проверка нажатия выполняется сверху вниз по дереву пользовательского интерфейса. Событие указателя происходит в пределах composable-функции. В результате этого процесса создается цепочка composable-функций, которые успешно проходят проверку нажатия.
- По умолчанию, если на одном уровне дерева есть несколько подходящих composable-функций, "попадание" происходит только по функции с самым высоким z-индексом. Например, если вы добавите две перекрывающиеся composable-функции
ButtonвBox, только та, которая нарисована сверху, будет получать события указателя. Теоретически вы можете переопределить это поведение, создав собственную реализациюPointerInputModifierNodeи задав дляsharePointerInputWithSiblingsзначение true. - Дальнейшие события для того же указателя отправляются в ту же цепочку composable-функций и обрабатываются в соответствии с логикой распространения событий. Система больше не выполняет проверку нажатия для этого указателя. Это означает, что каждая компонуемая функция в цепочке получает все события для этого указателя, даже если они происходят за пределами этой компонуемой функции. Composables, которые не входят в цепочку, никогда не получают события указателя, даже если указатель находится в их границах.
События наведения, запускаемые мышью или стилусом, являются исключением из правил, определенных здесь. События наведения отправляются в любую composable-функцию, на которую они попадают. Поэтому, когда пользователь перемещает указатель за пределы одной composable-функции и наводит его на другую, события отправляются не первой, а новой функции.
Использование событий
Если обработчик жестов назначен нескольким composable-функциям, они не должны конфликтовать. Например, посмотрите на этот интерфейс:
Когда пользователь нажимает кнопку закладки, лямбда-выражение onClick кнопки обрабатывает этот жест. Когда пользователь нажимает на любую другую часть элемента списка, ListItem обрабатывает этот жест и переходит к статье. Что касается ввода указателя, кнопка должна поглотить это событие, чтобы ее родительский элемент знал, что больше не нужно на него реагировать. Жесты, включенные в готовые компоненты, и общие модификаторы жестов включают такое поведение потребления, но если вы пишете свой собственный жест, вам необходимо использовать события вручную. Для этого используется метод PointerInputChange.consume:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Обработка события не останавливает его распространение на другие composable-функции. Вместо этого composable-функция должна явно игнорировать обработанные события. При написании пользовательских жестов вы должны проверить, было ли событие уже использовано другим элементом:
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 } } } }
Распространение событий
Как упоминалось ранее, изменения указателя передаются каждой composable-функции, на которую он попадает.
Но если таких composable-функций несколько, в каком порядке распространяются события? Если взять пример из предыдущего раздела, этот пользовательский интерфейс преобразуется в следующее дерево пользовательского интерфейса, где только ListItem и Button реагируют на события указателя:
События указателя проходят через каждую из этих composable-функций три раза за три прохода:
- На начальном этапе событие перемещается от вершины дерева пользовательского интерфейса к его основанию. Это позволяет родительскому объекту перехватывать событие до того, как его обработает дочерний объект. Например, подсказки должны перехватывать долгое нажатие, а не передавать его дочерним элементам. В нашем примере
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
- Прокрутка
- Нажатие