Há vários termos e conceitos importantes para entender ao trabalhar com o processamento de gestos em um aplicativo. Nesta página, explicamos os termos ponteiros, eventos de ponteiro e gestos, além de apresentar os diferentes níveis de abstração para gestos. Ele também se aprofunda no consumo e na propagação de eventos.
Definições
Para entender os vários conceitos desta página, é preciso compreender parte da terminologia usada:
- Ponteiro: um objeto físico que você pode usar para interagir com o aplicativo.
Em dispositivos móveis, o ponteiro mais comum é o dedo interagindo com
a tela touch. Como alternativa, você pode usar uma stylus em vez do dedo.
Em telas grandes, é possível usar um mouse ou trackpad para interagir indiretamente com
a tela. Um dispositivo de entrada precisa ser capaz de "apontar" para uma coordenada para ser
considerado um ponteiro. Portanto, um teclado, por exemplo, não pode ser considerado um
ponteiro. 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 um 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 de pressionamento seguido por um evento de liberação. 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. As funções que podem ser compostas, como Button,
incluem automaticamente suporte a gestos. Para adicionar suporte a gestos a componentes
personalizados, adicione modificadores de gesto, como clickable, a elementos de composição
arbitrários. Por fim, se você precisar de um gesto personalizado, use o
modificador pointerInput.
Como regra, use o nível mais alto de abstração que oferece a
funcionalidade necessária. 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, um LazyColumn responde a gestos de arrastar
rolando o conteúdo, um Button mostra uma ondulação quando você o pressiona
e o componente SwipeToDismiss contém uma lógica de deslizar para dispensar um
elemento. Esse tipo de processamento de gestos funciona automaticamente.
Além do processamento de gestos interno, muitos componentes também exigem que o autor da chamada
processe o gesto. Por exemplo, um Button detecta automaticamente toques
e aciona um evento de clique. Você transmite uma lambda onClick para o Button para
reagir ao gesto. Da mesma forma, adicione uma 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 em componentes, porque eles
incluem suporte pronto para uso para foco e acessibilidade e são
bem testados. Por exemplo, um Button é marcado de uma maneira especial para que
os serviços de acessibilidade o descrevam corretamente como um botão, em vez de apenas um
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 de composição arbitrários com modificadores
É possível aplicar modificadores de gesto a qualquer elemento combinável arbitrário para que ele
detecte gestos. Por exemplo, é possível 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:
- Gerencie toques e pressionamentos com os modificadores
clickable,combinedClickable,selectable,toggleableetriStateToggleable. - Gerencie a rolagem com os modificadores
horizontalScroll,verticalScrolle o mais genéricoscrollable. - Processar o arrasto com os modificadores
draggableeswipeable. - Gerencie gestos multitoque, como movimentação, rotação e zoom, com
o modificador
transformable.
Como regra, prefira modificadores de gestos prontos para uso em vez de processamento de gestos personalizado.
Os modificadores adicionam mais funcionalidades ao 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 em interações,
passagem do cursor, foco e suporte ao teclado. Confira o código-fonte
de clickable para ver como a funcionalidade
está sendo adicionada.
Adicionar gestos personalizados a elementos combináveis arbitrários com o modificador pointerInput
Nem todos os gestos são implementados com um modificador de gesto pronto para uso. Por
exemplo, não é possível usar um modificador para reagir a um arrasto após tocar e manter pressionado, um
clique com a tecla Control ou um toque com três dedos. Em vez disso, você pode criar seu próprio gerenciador de gestos
para identificar esses gestos personalizados. É possível criar um gerenciador de gestos com
o modificador pointerInput, que dá acesso aos eventos de ponteiro
brutos.
O código a seguir detecta eventos de ponteiro brutos:
@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. Quando o valor de uma dessas chaves 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 de ponteiro precisará ser executado novamente para garantir que os eventos certos sejam registrados. awaitPointerEventScopecria um escopo de corrotina que pode ser usado para aguardar eventos de ponteiro.awaitPointerEventsuspende a corrotina até que ocorra um próximo evento do ponteiro.
Embora a detecção de eventos de entrada brutos seja eficiente, também é complexo criar um gesto personalizado com base nesses dados brutos. Para simplificar a criação de gestos personalizados, muitos métodos utilitários estão disponíveis.
Detectar gestos completos
Em vez de processar os eventos de ponteiro brutos, você pode detectar gestos específicos
e responder adequadamente. O AwaitPointerEventScope fornece
métodos para detectar:
- Pressionar, tocar, tocar duas vezes e tocar e manter pressionado:
detectTapGestures - Arrastos:
detectHorizontalDragGestures,detectVerticalDragGestures,detectDragGesturesedetectDragGesturesAfterLongPress - Transformações:
detectTransformGestures
Esses são detectores de nível superior, então 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 do loop while(true) que passa por cada evento bruto. O método awaitEachGesture reinicia o
bloco de contenção 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 é melhor usar awaitEachGesture, a menos que você esteja
respondendo a eventos de ponteiro sem identificar gestos. Um exemplo disso é
hoverable, que não responde a eventos de pressionar ou soltar o ponteiro. 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 de gestos:
- Suspenda até que um ponteiro seja pressionado com
awaitFirstDownou aguarde até que todos os ponteiros sejam liberados comwaitForUpOrCancellation. - Crie um listener de arrastar de baixo nível usando
awaitTouchSlopOrCancellationeawaitDragOrCancellation. O gerenciador de gestos é suspenso até que o ponteiro alcance o limite de toque e, em seguida, até que um primeiro evento de arrastar ocorra. Se você só tiver interesse em arrastar ao longo de um único eixo, useawaitHorizontalTouchSlopOrCancellationmaisawaitHorizontalDragOrCancellationouawaitVerticalTouchSlopOrCancellationmaisawaitVerticalDragOrCancellation. - Suspender até que um toque e manter pressionado aconteça com
awaitLongPressOrCancellation. - Use o método
dragpara detectar continuamente eventos de arrastar ouhorizontalDragouverticalDragpara detectar eventos de arrastar em um eixo.
Aplicar cálculos para eventos multitoque
Quando um usuário faz 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 oferecerem controle refinado o suficiente para seu caso de uso, você poderá
detectar os eventos brutos e aplicar cálculos a eles. Esses métodos auxiliares
são calculateCentroid, calculateCentroidSize,
calculatePan, calculateRotation e calculateZoom.
Envio de eventos e teste de hit
Nem todos os eventos de ponteiro são enviados para todos os modificadores pointerInput. A expedição 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 hits dos elementos que podem ser compostos "qualificados". Um elemento combinável é considerado qualificado quando tem recursos de processamento de entrada de ponteiro. O teste de hit flui de cima para baixo na árvore da interface. Um elemento combinável é "atingido" quando o evento de ponteiro ocorre dentro dos limites dele. Esse processo resulta em uma cadeia de funções de composição 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 o elemento com o maior índice z é "atingido". Por
exemplo, quando você adiciona dois elementos que podem ser compostos
Buttonsobrepostos a umBox, apenas o que está desenhado por cima recebe eventos de ponteiro. Teoricamente, é possível substituir esse comportamento criando sua própria implementação dePointerInputModifierNodee definindosharePointerInputWithSiblingscomo verdadeiro. - Outros eventos para o mesmo ponteiro são enviados para essa mesma cadeia de composição 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 desse elemento. Os elementos que não estão na cadeia nunca recebem eventos de ponteiro, mesmo quando o ponteiro está dentro dos limites deles.
Os eventos de passagem, acionados por um mouse ou uma stylus, são uma exceção às regras definidas aqui. Os eventos de passagem do cursor são enviados para qualquer elemento combinável que eles atingirem. Assim, quando um usuário passa um ponteiro dos limites de um elemento combinável para o próximo, em vez de enviar os eventos para o primeiro elemento, eles são enviados para o novo.
Consumo de eventos
Quando mais de um elemento combinável tem um gerenciador de gestos atribuído, esses gerenciadores não podem entrar em conflito. Por exemplo, confira esta interface:
Quando um usuário toca no botão de favoritos, 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 até o artigo. Em termos de entrada de ponteiro,
o botão precisa consumir esse evento para que o pai saiba que não deve
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 isso,
use o método PointerInputChange.consume:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Consumir um evento não interrompe a propagação dele para outros elementos combináveis. Em vez disso, um elemento que pode ser composto precisa ignorar explicitamente os eventos consumidos. Ao criar 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 de eventos
Como mencionado antes, as mudanças de ponteiro são transmitidas para cada elemento combinável que elas atingem.
Mas se houver mais de um elemento combinável desse tipo, em que ordem os eventos
vão se propagar? Se você usar o exemplo da última seção, essa interface será traduzida para
a árvore de interface abaixo, em que apenas ListItem e Button respondem a
eventos de ponteiro:
Os eventos de ponteiro fluem por cada um desses elementos que podem ser compostos três vezes, durante três "passagens":
- Na passagem inicial, o evento flui da parte de cima da árvore da IU para a
de baixo. 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,
ListItemrecebe o evento antes deButton. - Na passagem principal, o evento flui dos nós de folha da árvore da IU até a
raiz dela. Essa fase é onde você normalmente consome gestos e é
a passagem padrão ao detectar eventos. Processar gestos nessa passagem
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
Buttonrecebe o evento antes doListItem. - Na passagem final, o evento flui mais uma vez da parte de cima da árvore da interface para os nós de folha. Esse fluxo permite que os elementos mais altos na pilha respondam ao consumo de eventos pelo elemento pai. Por exemplo, um botão remove a indicação de ondulação quando um toque se transforma em um arrasto do pai rolável.
Visualmente, o fluxo de eventos pode ser representado da seguinte forma:
Depois que uma mudança de entrada é consumida, essas informações são transmitidas desse ponto em diante no fluxo:
No código, é possível especificar o cartão em que você tem interesse:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
Nesse snippet de código, o mesmo evento idêntico é retornado por cada uma dessas chamadas de método de espera, 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 que você execute gestos completos de nível mais alto (como pinça ou clique longo) ou gestos de nível mais baixo (como mover o cursor por uma determinada quantidade de pixels):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Consulte a documentação do performTouchInput para mais exemplos.
Saiba mais
Para saber mais sobre gestos no Jetpack Compose, consulte os seguintes recursos:
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Acessibilidade no Compose
- Rolagem
- Toque e pressione