Há vários termos e conceitos que é importante entender ao trabalhar com gestos em um aplicativo. Esta página explica os termos ponteiros, eventos de ponteiro e gestos e introduz diferentes para gestos. Ela também se aprofunda no consumo de eventos e propagação.
Definições
Para entender os diversos conceitos desta página, é preciso entender alguns da terminologia usada:
- Ponteiro: um objeto físico que pode ser usado para interagir com seu aplicativo.
Em dispositivos móveis, o ponteiro mais comum é a interação do seu dedo com
na tela touchscreen. Como alternativa, você pode usar uma stylus para colocar o dedo na posição.
Em telas grandes, você pode usar um mouse ou trackpad para interagir indiretamente com
na tela. Um dispositivo de entrada deve ser capaz de "apontar" em uma coordenada ser
considerado um ponteiro, um teclado, por exemplo, não pode ser considerado um
ponteiro. No Compose, o tipo de ponteiro é incluído nas mudanças dele 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 por ponteiro, como colocar
um dedo na tela ou arrastando um mouse, acionaria um evento. Em
Compose, todas as informações relevantes para esse evento estão contidas no
classe
PointerEvent
. - Gesto: uma sequência de eventos de ponteiro que pode ser interpretada como um único à ação. Por exemplo, um gesto de toque pode ser considerado uma sequência de um gesto de baixo seguido por um evento para cima. Há gestos comuns que são usados por muitos como tocar, arrastar ou transformar, mas você também pode criar seus próprios apps gesto quando necessário.
Diferentes níveis de abstração
O Jetpack Compose oferece diferentes níveis de abstração para processar gestos.
O nível superior é o suporte a componentes. Elementos combináveis, como Button
incluem automaticamente o suporte a gestos. Para adicionar suporte a gestos em
é possível adicionar modificadores de gesto, como clickable
, a componentes
que podem ser compostos. Por fim, se precisar de um gesto personalizado, você pode usar o
Modificador pointerInput
.
Via de regra, desenvolva no nível mais alto de abstração que ofereça
da funcionalidade de que você precisa. Assim, 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 um
implementação de pointerInput
.
Suporte a componentes
Muitos componentes prontos para uso no Compose incluem algum tipo de gesto interno
processamento. Por exemplo, uma LazyColumn
responde a gestos de arrastar da seguinte maneira:
rolando o conteúdo, uma Button
mostra uma ondulação quando você pressiona o botão.
e o componente SwipeToDismiss
contém lógica de deslizamento para dispensar um
. Esse tipo de processamento de gestos funciona automaticamente.
Ao lado do processamento de gestos interno, muitos componentes também exigem que o autor da chamada
o gesto. Por exemplo, um Button
detecta toques automaticamente
e aciona um evento de clique. Você transmite uma lambda onClick
ao Button
para
reagir ao gesto. Da mesma forma, você adiciona uma lambda onValueChange
a um
Slider
para reagir quando o usuário arrastar o controle deslizante.
No seu caso de uso, prefira gestos incluídos em componentes, porque eles
incluem suporte pronto para uso com foco e acessibilidade, além de
bem testadas. Por exemplo, uma Button
é marcada de forma especial para que
serviços de acessibilidade o descrevem corretamente como um botão, em vez de 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
É possível aplicar modificadores de gesto a qualquer elemento combinável arbitrário para tornar a
que pode ser composto
ouvir gestos. Por exemplo, você pode permitir que um Box
genérico
processe os gestos de toque tornando-os clickable
, ou permita que um Column
para lidar com a rolagem vertical aplicando verticalScroll
.
Há muitos modificadores para processar diferentes tipos de gestos:
- Processar toques e pressionamentos com o
clickable
,combinedClickable
,selectable
,toggleable
etriStateToggleable
. - Processar a rolagem com o
horizontalScroll
verticalScroll
e modificadoresscrollable
mais genéricos. - Processar a ação de arrastar com
draggable
eswipeable
. - Gerencie gestos multitoque, como deslocamento, rotação e zoom, com
o modificador
transformable
.
Como regra geral, prefira modificadores de gesto prontos para uso em vez do processamento de gestos personalizado.
Os modificadores adicionam mais funcionalidades ao processamento puro de eventos de ponteiro.
Por exemplo, o modificador clickable
não apenas adiciona detecção de pressionamentos e
mas também adiciona informações semânticas, indicações visuais sobre interações,
passar cursor, foco e suporte ao teclado. Você pode verificar o código-fonte
de clickable
para conferir como a funcionalidade
está sendo adicionado.
Adicionar um gesto personalizado a elementos combináveis arbitrários com o modificador pointerInput
Nem todos os gestos são implementados com um modificador de gestos pronto para uso. Para
por exemplo, não é possível usar um modificador para reagir a uma ação de arrastar após tocar e manter pressionado, um
Control + clique ou toque com três dedos. Em vez disso, escreva seu próprio gesto
gerenciador para identificar esses gestos personalizados. É possível criar um gerenciador de gestos com
O modificador pointerInput
, que dá acesso ao ponteiro bruto
eventos.
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 os seguintes:
- O modificador
pointerInput
. Você transmite uma ou mais chaves. Quando o o valor de uma dessas chaves mudar, o lambda do conteúdo modificador executadas novamente. O exemplo transmite um filtro opcional para o elemento combinável. Se o valor desse filtro mudar, o manipulador de eventos de ponteiro deverá ser executadas novamente para garantir que os eventos certos sejam registrados. - O
awaitPointerEventScope
cria um escopo de corrotina que pode ser usado para aguardar eventos de ponteiro. awaitPointerEvent
(link em inglês) suspende a corrotina até um próximo evento de ponteiro. de segurança.
Ouvir eventos de entrada brutos é eficiente, mas também é complexo escrever um gesto personalizado com base nesses dados brutos. Para simplificar a criação de objetos há vários métodos utilitários disponíveis.
Detectar gestos completos
Em vez de processar eventos brutos de ponteiro, você pode detectar gestos específicos
ocorrer e responder adequadamente. O AwaitPointerEventScope
oferece
métodos para detectar:
- Toque, toque duas vezes e mantenha pressionado:
detectTapGestures
- Arrastar:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
edetectDragGesturesAfterLongPress
- Transformações:
detectTransformGestures
Como eles são de nível superior, não é possível adicionar vários detectores em um
Modificador pointerInput
. O snippet a seguir detecta apenas os toques, não os
arrasta:
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 for atingido. Se você precisar adicionar mais de um listener de gestos ao
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. Você pode usar o
Método auxiliar awaitEachGesture
em vez da repetição while(true)
que
passa por cada evento bruto. O método awaitEachGesture
reinicia a
contendo um bloco quando todos os ponteiros foram levantados, indicando que o gesto é
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, é recomendável usar awaitEachGesture
, a menos que você
responder a eventos de ponteiro sem identificar gestos; Um exemplo disso é
hoverable
, que não responde a eventos de ponteiro para baixo ou para cima, apenas
precisa saber quando um ponteiro entra ou sai dos limites.
Aguardar um evento ou subgesto específico
Há um conjunto de métodos que ajudam a identificar partes comuns dos gestos:
- Suspenda até que um ponteiro desça com
awaitFirstDown
ou aguarde até que o evento termine para cima comwaitForUpOrCancellation
. - Criar um listener de arrastar de baixo nível usando
awaitTouchSlopOrCancellation
eawaitDragOrCancellation
. O gerenciador de gestos é suspenso pela primeira vez até o ponteiro atinge a tolerância de toque e fica suspenso até que o primeiro evento de arrastar aparecer. Caso queira apenas arrastar ao longo de um único eixo, use Mais deawaitHorizontalTouchSlopOrCancellation
awaitHorizontalDragOrCancellation
ou Mais deawaitVerticalTouchSlopOrCancellation
awaitVerticalDragOrCancellation
. - Suspender até que a ação de tocar e manter pressionado com
awaitLongPressOrCancellation
aconteça. - Use o método
drag
para detectar continuamente eventos de arrastar ouhorizontalDrag
ouverticalDrag
para detectar eventos de arrastar em um .
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 a detectTransformGestures
não oferecem um controle refinado o suficiente para seu caso de uso, é possível
ouvir os eventos brutos e aplicar cálculos com base neles. Esses métodos auxiliares
são calculateCentroid
, calculateCentroidSize
,
calculatePan
, calculateRotation
e calculateZoom
.
Envio de eventos e teste de hits
Nem todos os eventos de ponteiro são enviados para cada modificador pointerInput
. Evento
o envio funciona da seguinte forma:
- Os eventos de ponteiro são enviados para uma hierarquia de composição. O momento em que um novo ponteiro aciona o primeiro evento de ponteiro, o sistema inicia o teste de hit os "qualificados" que podem ser compostos. Um elemento combinável é considerado qualificado quando tem de processamento de entrada de ponteiro. Fluxos de teste de hit da parte de cima da interface árvore para baixo. Um elemento combinável é "hit" quando o evento do ponteiro ocorreu dentro dos limites da função. Esse processo resulta em uma cadeia de combináveis que fazem o teste de acertar de forma positiva.
- Por padrão, quando há vários elementos combináveis qualificados no mesmo nível
somente o elemento combinável com o maior Z-index é "hit". Para
exemplo, quando você adiciona dois elementos combináveis
Button
sobrepostos a umBox
, somente aquele desenhado na parte de cima recebe qualquer evento de ponteiro. Teoricamente, é possível substituir esse comportamento criando seu próprioPointerInputModifierNode
. implementação e definindosharePointerInputWithSiblings
como verdadeiro. - Outros eventos para o mesmo ponteiro são despachados para a mesma cadeia de combináveis e fluem de acordo com a lógica de propagação de eventos. O sistema não executa mais testes de hit para esse ponteiro. Isso significa que cada combinável na cadeia recebe todos os eventos desse ponteiro, mesmo quando que ocorrem fora dos limites dessa função. Elementos combináveis que não são na cadeia nunca recebem eventos de ponteiro, mesmo quando ele está dentro dos limites.
Eventos de passar o cursor, acionados pela passagem do mouse ou da stylus, são uma exceção aos regras definidas aqui. Os eventos de passagem de cursor são enviados para qualquer elemento combinável clicado. Então, quando um usuário passa o ponteiro do mouse dos limites de um elemento combinável para o próximo, em vez de enviar os eventos para esse primeiro elemento combinável, os eventos são enviados para o um novo elemento combinável.
Consumo de eventos
Quando mais de um elemento combinável tem um gerenciador de gestos atribuído, eles manipuladores não devem entrar em conflito. Por exemplo, confira 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 até o artigo. Em termos de entrada de ponteiro,
o botão precisa consumir esse evento, para que o pai saiba não
reage a ele. Gestos incluídos em componentes prontos para uso e no
modificadores de gesto comuns incluem esse comportamento de consumo, mas se você estiver
criando seu próprio gesto personalizado, você precisa consumir eventos manualmente. Você faz isso
com 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 O elemento combinável precisa ignorar explicitamente os eventos consumidos. Ao escrever gestos personalizados, verifique se um evento já foi consumido por outro :
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 anteriormente, as mudanças do ponteiro são transmitidas para cada elemento combinável que ele atinge.
No entanto, se existir mais de um desses elementos, em que ordem os eventos
propagar? Se você pegar o exemplo da última seção, essa interface se traduz em
a seguinte árvore da interface, em que apenas ListItem
e Button
respondem à
eventos de ponteiro:
Os eventos de ponteiro fluem por cada um desses elementos combináveis três vezes, durante três "aprova":
- No passe inicial, o evento flui do topo da árvore da interface para a
fundo. Esse fluxo permite que um pai intercepte um evento antes que o filho possa
consumi-los. Por exemplo, as dicas precisam interceptar uma
tocar e manter pressionada em vez de passá-la para os filhos. Em nossa
Por exemplo,
ListItem
recebe o evento antes deButton
. - No cartão principal, o evento flui dos nós folha da árvore da interface para o
raiz da árvore da interface. Essa fase é onde você normalmente consome gestos e é
a passagem padrão ao detectar eventos. Processar gestos no cartão
significa que os nós folha têm precedência sobre os pais, que são
o comportamento mais lógico para a maioria dos gestos. No nosso exemplo, o
Button
recebe o evento antes daListItem
. - No cartão final, o evento flui mais uma vez a partir da parte superior da interface. aos nós das folhas. Esse fluxo permite que elementos mais acima na pilha respondem ao consumo do evento pelo pai. Por exemplo, um botão remove a indicação de ondulação quando um pressionamento se transforma em uma ação de arrastar do pai rolável.
Visualmente, o fluxo de eventos pode ser representado da seguinte maneira:
Quando uma mudança de entrada é consumida, essas informações são transmitidas ponto do fluxo em diante:
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) } }
Nesse snippet de código, o mesmo evento idêntico é retornado por cada essas chamadas de método "await", embora os dados sobre o consumo possam mudou.
Testar gestos
Nos métodos de teste, é possível enviar manualmente eventos de ponteiro usando o
método performTouchInput
. Assim, é possível realizar campanhas
gestos completos (como gesto de pinça ou clique longo) ou gestos de baixo nível (como
movendo o cursor em uma certa quantidade de pixels):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Consulte a documentação de performTouchInput
para mais exemplos.
Saiba mais
Saiba mais sobre gestos no Jetpack Compose nas páginas a seguir recursos:
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Acessibilidade no Compose
- Rolagem
- Tocar e pressionar