Понимание жестов

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

Определения

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

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

Это детекторы верхнего уровня, поэтому вы не можете добавить несколько детекторов в один модификатор 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, второй слой — изображение, столбец и кнопка, а столбец разделяется на два текста. 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 из следующих ресурсов:

{% дословно %} {% дословно %} {% дословно %} {% дословно %}