Há vários termos e conceitos que é importante entender ao trabalhar no processamento de gestos em um aplicativo. Esta página explica os termos ponteiros, eventos de ponteiro e gestos e apresenta os diferentes níveis de abstração para gestos. Ele também detalha o consumo e a propagação de eventos.
Definições
Para entender os vários conceitos nesta página, é preciso entender alguns dos termos usados:
- Ponteiro: um objeto físico que pode ser usado para interagir com seu aplicativo.
Em dispositivos móveis, o ponteiro mais comum é o dedo que interage com
a tela touchscreen. Como alternativa, você pode usar uma stylus para substituir o dedo.
Em telas grandes, você pode usar um mouse ou trackpad para interagir indiretamente com
a tela. Um dispositivo de entrada precisa ser capaz de "apontar" uma coordenada para ser
considerado um ponteiro. Portanto, um teclado, por exemplo, não pode ser considerado um
apontador. No Compose, o tipo de ponteiro é incluído nas mudanças de ponteiro usando
PointerType
. - Evento de ponteiro: descreve uma interação de baixo nível de um ou mais ponteiros
com o aplicativo em um determinado momento. Qualquer interação com o ponteiro, como colocar
um dedo na tela ou arrastar o mouse, aciona um evento. No
Compose, todas as informações relevantes para esse evento estão contidas na classe
PointerEvent
. - Gesto: uma sequência de eventos de ponteiro que pode ser interpretada como uma única ação. Por exemplo, um gesto de toque pode ser considerado uma sequência de um evento para baixo seguido por um evento para cima. Há gestos comuns usados por muitos apps, como tocar, arrastar ou transformar, mas você também pode criar seu próprio gesto personalizado quando necessário.
Diferentes níveis de abstração
O Jetpack Compose oferece diferentes níveis de abstração para processar gestos.
No nível superior está o suporte a componentes. Elementos combináveis, como Button
,
incluem automaticamente suporte a gestos. Para adicionar suporte a gestos a componentes
personalizados, adicione modificadores de gestos, como clickable
, a elementos
combináveis arbitrários. Por fim, se você precisar de um gesto personalizado, use o
modificador pointerInput
.
Como regra, crie no nível de abstração mais alto que ofereça a
funcionalidade de que você precisa. Dessa forma, você se beneficia das práticas recomendadas incluídas
na camada. Por exemplo, Button
contém mais informações semânticas, usadas para
acessibilidade, do que clickable
, que contém mais informações do que uma implementação
pointerInput
bruta.
Suporte a componentes
Muitos componentes prontos para uso no Compose incluem algum tipo de processamento interno de
gestos. Por exemplo, uma LazyColumn
responde a gestos de arrastar
rolando o conteúdo, uma Button
mostra uma ondulação quando você pressiona o botão,
e o componente SwipeToDismiss
contém a lógica de deslizar para dispensar um
elemento. Esse tipo de processamento de gestos funciona automaticamente.
Ao lado do processamento interno de gestos, muitos componentes também exigem que o autor da chamada
processe o gesto. Por exemplo, um Button
detecta toques automaticamente e aciona um evento de clique. Transmita um lambda onClick
ao Button
para
reagir ao gesto. Da mesma forma, adicione um lambda onValueChange
a um
Slider
para reagir ao usuário arrastando a alça do controle deslizante.
Quando for adequado ao seu caso de uso, prefira gestos incluídos nos componentes, já que
eles incluem suporte pronto para foco e acessibilidade e são
bem testados. Por exemplo, uma Button
é marcada de uma forma especial para que
os serviços de acessibilidade a descrevam corretamente como um botão, em vez de apenas qualquer
elemento clicável:
// 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!") }
Para saber mais sobre acessibilidade no Compose, consulte Acessibilidade no Compose.
Adicionar gestos específicos a elementos combináveis arbitrários com modificadores
Você pode aplicar modificadores de gesto a qualquer elemento arbitrário para fazer com que o
elemento combinável detecte gestos. Por exemplo, você pode permitir que um Box
genérico processe gestos de toque tornando-o clickable
ou permitir que um Column
processe a rolagem vertical aplicando verticalScroll
.
Há muitos modificadores para processar diferentes tipos de gestos:
- Processar toques e pressionamentos com os modificadores
clickable
,combinedClickable
,selectable
,toggleable
etriStateToggleable
. - Processar a rolagem com os modificadores
horizontalScroll
,verticalScroll
escrollable
mais genéricos. - Processar a ação de arrastar com o modificador
draggable
eswipeable
. - Processar gestos multitoque, como deslocamento, rotação e zoom, com
o modificador
transformable
.
Como regra geral, prefira modificadores de gestos prontos para uso em vez do processamento de gestos personalizados.
Os modificadores adicionam mais funcionalidades além do processamento de eventos de ponteiro puro.
Por exemplo, o modificador clickable
não apenas adiciona a detecção de pressionamentos e
toques, mas também adiciona informações semânticas, indicações visuais sobre interações,
passagem do cursor, foco e suporte ao teclado. Verifique o código-fonte
de clickable
para ver como a funcionalidade
está sendo adicionada.
Adicionar um gesto personalizado a elementos combináveis arbitrários com o modificador pointerInput
Nem todos os gestos são implementados com um modificador pronto para uso. Por
exemplo, não é possível usar um modificador para reagir a uma ação de arrastar após tocar e manter pressionado, um
clique com o botão "Control" ou toque de três dedos. Em vez disso, crie seu próprio gerenciador
de gestos para identificar esses gestos personalizados. Você pode criar um gerenciador de gestos com
o modificador pointerInput
, que fornece acesso aos eventos de ponteiro
brutos.
O código a seguir detecta eventos brutos de ponteiro:
@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}" } } } } ) } }
Se você dividir esse snippet, os componentes principais serão:
- O modificador
pointerInput
. Você transmite uma ou mais chaves a ele. Quando o valor de uma dessas teclas muda, a lambda de conteúdo do modificador é executada novamente. O exemplo transmite um filtro opcional para o elemento combinável. Se o valor desse filtro mudar, o manipulador de eventos do ponteiro deverá ser executado novamente para garantir que os eventos certos sejam registrados. awaitPointerEventScope
cria um escopo de corrotina que pode ser usado para aguardar eventos de ponteiro.awaitPointerEvent
suspende a corrotina até que um próximo evento de ponteiro ocorra.
Embora detectar eventos de entrada brutos seja eficiente, também é complexo escrever um gesto personalizado com base nesses dados brutos. Para simplificar a criação de gestos personalizados, há muitos métodos utilitários disponíveis.
Detectar gestos completos
Em vez de processar os eventos de ponteiro brutos, você pode detectar gestos específicos
que ocorrem e responder adequadamente. O AwaitPointerEventScope
fornece
métodos para detectar:
- Tocar, tocar duas vezes e tocar e manter pressionado:
detectTapGestures
- Arraste:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
edetectDragGesturesAfterLongPress
- Transformações:
detectTransformGestures
Esses são detectores de nível superior. Portanto, não é possível adicionar vários detectores em um
modificador pointerInput
. O snippet a seguir detecta apenas os toques, não os
arrastos:
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" } } ) }
Internamente, o método detectTapGestures
bloqueia a corrotina, e o segundo
detector nunca é alcançado. Se você precisar adicionar mais de um listener de gestos a
um elemento combinável, use instâncias separadas do modificador 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" } } ) }
Processar eventos por gesto
Por definição, os gestos começam com um evento de ponteiro para baixo. É possível usar o método auxiliar
awaitEachGesture
em vez da repetição while(true)
que
transmite cada evento bruto. O método awaitEachGesture
reinicia o
bloco que o contém quando todos os ponteiros são levantados, indicando que o gesto foi
concluído:
@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() } } } ) }
Na prática, quase sempre é recomendável usar awaitEachGesture
, a menos que você esteja
respondendo a eventos de ponteiro sem identificar gestos. Um exemplo disso é o
hoverable
, que não responde a eventos de ponteiro para baixo ou para cima. Ele só precisa
saber quando um ponteiro entra ou sai dos limites.
Aguardar um evento ou subgesto específico
Há um conjunto de métodos que ajuda a identificar partes comuns dos gestos:
- Suspenda até que um ponteiro desça com
awaitFirstDown
ou aguarde até que todos eles subam comwaitForUpOrCancellation
. - Crie um listener de arrastar de baixo nível usando
awaitTouchSlopOrCancellation
eawaitDragOrCancellation
. O gerenciador de gestos é suspenso primeiro até que o ponteiro alcance a área de toque e, em seguida, é suspenso até que um primeiro evento de arrastar chegue. Se você tem interesse apenas em arrastar ao longo de um único eixo, useawaitHorizontalTouchSlopOrCancellation
maisawaitHorizontalDragOrCancellation
ouawaitVerticalTouchSlopOrCancellation
maisawaitVerticalDragOrCancellation
. - Suspenda até que um toque longo aconteça com
awaitLongPressOrCancellation
. - Use o método
drag
para detectar eventos de arrastar continuamente ouhorizontalDrag
ouverticalDrag
para detectar eventos de arrastar em um eixo.
Aplicar cálculos para eventos multitoque
Quando um usuário está realizando um gesto multitoque usando mais de um ponteiro,
é complexo entender a transformação necessária com base nos valores brutos.
Se o modificador transformable
ou os métodos detectTransformGestures
não fornecerem um controle detalhado suficiente para seu caso de uso, será possível
detectar os eventos brutos e aplicar cálculos neles. Esses métodos auxiliares
são calculateCentroid
, calculateCentroidSize
,
calculatePan
, calculateRotation
e calculateZoom
.
Envio de eventos e teste de hit
Nem todo evento de ponteiro é enviado para cada modificador pointerInput
. O envio
de eventos funciona da seguinte maneira:
- Os eventos de ponteiro são enviados para uma hierarquia combinável. No momento em que um novo ponteiro aciona o primeiro evento de ponteiro, o sistema começa a testar os elementos combináveis "qualificados". Um elemento combinável é considerado qualificado quando tem recursos de processamento de entrada de ponteiro. O teste de hit flui da parte superior da árvore de interface para a parte inferior. Um elemento combinável é "hit" quando o evento de ponteiro ocorreu dentro dos limites desse elemento. Esse processo resulta em uma cadeia de elementos combináveis que têm um teste de hit positivo.
- Por padrão, quando há vários elementos combináveis qualificados no mesmo nível da
árvore, apenas aquele com o maior Z-index é "hit". Por
exemplo, quando você adiciona dois elementos combináveis
Button
sobrepostos a umBox
, somente o mostrado na parte de cima recebe eventos de ponteiro. Teoricamente, é possível substituir esse comportamento criando sua própria implementação dePointerInputModifierNode
e definindosharePointerInputWithSiblings
como verdadeiro. - Outros eventos para o mesmo ponteiro são enviados para a mesma cadeia de elementos combináveis e fluem de acordo com a lógica de propagação de eventos. O sistema não realiza mais testes de hit para esse ponteiro. Isso significa que cada elemento combinável na cadeia recebe todos os eventos desse ponteiro, mesmo quando eles ocorrem fora dos limites do elemento. Os elementos combináveis que não estão na cadeia nunca recebem eventos de ponteiro, mesmo quando o ponteiro está dentro dos limites.
Os eventos de passagem de cursor, acionados pelo movimento do mouse ou com uma stylus, são uma exceção às regras definidas aqui. Os eventos de passagem de cursor são enviados para qualquer elemento combinável quando eles atingem. Portanto, quando um usuário passa o ponteiro do mouse sobre os limites de um elemento combinável para o próximo, em vez de enviar os eventos para o primeiro elemento combinável, os eventos são enviados para o novo elemento combinável.
Consumo de eventos
Quando mais de um elemento combinável tem um gerenciador de gestos atribuído, eles não podem entrar em conflito. Por exemplo, veja esta interface:
Quando um usuário toca no botão de favorito, a lambda onClick
do botão processa esse
gesto. Quando um usuário toca em qualquer outra parte do item da lista, o ListItem
processa esse gesto e navega para o artigo. Em termos de entrada do ponteiro,
o botão precisa consumir esse evento para que o pai saiba
que não precisa mais reagir a ele. Os gestos incluídos em componentes prontos para uso e os
modificadores de gestos comuns incluem esse comportamento de consumo, mas, se você estiver
escrevendo seu próprio gesto personalizado, precisará consumir eventos manualmente. Para fazer isso,
use o método PointerInputChange.consume
:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
O consumo de um evento não interrompe a propagação dele para outros elementos combináveis. Um elemento combinável precisa ignorar explicitamente os eventos consumidos. Ao programar gestos personalizados, verifique se um evento já foi consumido por outro elemento:
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 } } } }
Propagação do evento
Como mencionado antes, as mudanças do ponteiro são transmitidas para cada elemento combinável atingido.
No entanto, se mais de um elemento combinável existir, em que ordem os eventos
se propagam? Se você usar o exemplo da última seção, essa interface será convertida na
árvore da interface abaixo, em que apenas ListItem
e Button
respondem a
eventos de ponteiro:
Os eventos de ponteiro fluem por cada um desses elementos combináveis três vezes, durante três "passagens":
- No passe inicial, o evento flui da parte superior da árvore da interface para a
parte inferior. Esse fluxo permite que um pai intercepte um evento antes que o filho possa
consumi-lo. Por exemplo, as dicas precisam interceptar
um toque longo em vez de transmiti-lo aos filhos. No nosso
exemplo,
ListItem
recebe o evento antes doButton
. - No cartão principal, o evento flui dos nós de folha da árvore da interface até a
raiz da árvore da interface. Essa é a fase em que você normalmente consome gestos e é
o cartão padrão ao detectar eventos. O processamento de gestos nessa transmissão
significa que os nós de folha têm precedência sobre os pais, que é o
comportamento mais lógico para a maioria dos gestos. No nosso exemplo, o
Button
recebe o evento antes doListItem
. - Na Transferência final, o evento flui mais uma vez da parte de cima da árvore de interface para os nós de folha. Esse fluxo permite que elementos mais altos na pilha respondam ao consumo de eventos pelos pais. Por exemplo, um botão remove a indicação de ondulação quando o pressionamento se transforma em uma ação de arrastar do pai rolável.
Visualmente, o fluxo de eventos pode ser representado da seguinte maneira:
Depois que uma mudança de entrada é consumida, essas informações são transmitidas desse ponto em diante no fluxo:
No código, você pode especificar o cartão do seu interesse:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
No snippet de código, o mesmo evento idêntico é retornado por cada uma dessas chamadas de método "await", embora os dados sobre o consumo possam ter mudado.
Testar gestos
Nos métodos de teste, é possível enviar eventos de ponteiro manualmente usando o
método performTouchInput
. Isso permite realizar gestos completos
de nível superior (como fazer gesto de pinça ou clique longo) ou gestos de baixo nível (como
mover o cursor em uma determinada quantidade de pixels):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Consulte a documentação do performTouchInput
para mais exemplos.
Saiba mais
Saiba mais sobre gestos no Jetpack Compose usando estes recursos:
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Acessibilidade no Compose
- Rolagem
- Tocar e pressionar