Entender os gestos

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:

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:

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:

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 um Box, somente aquele desenhado na parte de cima recebe qualquer evento de ponteiro. Teoricamente, é possível substituir esse comportamento criando seu próprio PointerInputModifierNode. implementação e definindo sharePointerInputWithSiblings 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:

Item da lista com uma imagem, uma coluna com dois textos e um botão.

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:

Estrutura de árvore. A camada superior é ListItem, a segunda camada tem Imagem, Coluna e Botão, e a coluna é dividida em dois textos. ListItem e Botão estão destacados.

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 de Button.
  • 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 da ListItem.
  • 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: