Como processar interações do usuário

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

Os componentes da interface do usuário fornecem feedback ao usuário do dispositivo pela maneira como eles respondem às interações do usuário. Cada componente tem a própria maneira de responder a interações, o que ajuda o usuário a saber o que as interações dele estão fazendo. Por exemplo, se um usuário tocar em um botão na tela touchscreen de um dispositivo, ele provavelmente mudará de alguma forma, talvez adicionando uma cor de destaque. Essa mudança informa ao usuário que ele tocou no botão. Se o usuário não quiser fazer isso, vai saber que precisa arrastar o dedo para fora do botão antes de soltar. Caso contrário, o botão será ativado.

A documentação de Gestos do Compose aborda como os componentes do Compose processam eventos de ponteiro de baixo nível, como movimentos e cliques do ponteiro. O Compose abstrai esses eventos de baixo nível em interações de nível mais alto. Por exemplo, uma série de eventos de ponteiro pode ser adicionada a um botão de tocar e soltar. Entender essas abstrações de nível superior pode ajudar a personalizar a resposta da IU ao usuário. Por exemplo, você pode personalizar a aparência de um componente quando o usuário interage com ele ou apenas manter um registro dessas ações. Este documento fornece as informações necessárias para modificar os elementos de IU padrão ou criar seu próprio elemento.

Interações

Em muitos casos, você não precisa saber apenas como o componente do Compose está interpretando as interações do usuário. Por exemplo, Button depende de Modifier.clickable para descobrir se o usuário clicou no botão. Se estiver adicionando um botão típico ao seu app, você poderá definir o código onClick do botão, e o Modifier.clickable executará esse código quando adequado. Isso significa que você não precisa saber se o usuário tocou na tela ou selecionou o botão com um teclado. O Modifier.clickable descobre que o usuário executou um clique e responde executando o código onClick.

No entanto, se você quiser personalizar a resposta do componente de IU para o comportamento do usuário, talvez precise saber mais do que está acontecendo nos bastidores. Esta seção fornece algumas dessas informações.

Quando um usuário interage com um componente de IU, o sistema representa o comportamento gerando vários eventos de Interaction. Por exemplo, se um usuário tocar em um botão, ele vai gerar PressInteraction.Press. Se o usuário levantar o dedo dentro do botão, ele vai gerar um PressInteraction.Release, informando ao botão que o clique foi concluído. Por outro lado, se o usuário arrastar o dedo para fora do botão e levantar o dedo, o botão vai gerar PressInteraction.Cancel, para indicar que o pressionamento do botão foi cancelado, não concluído.

Essas interações são discretas. Ou seja, esses eventos de interação de baixo nível não pretendem interpretar o significado das ações do usuário ou a sequência delas. Eles também não interpretam quais ações do usuário podem ter prioridade sobre outras ações.

Essas interações geralmente vêm em pares, com um início e um fim. A segunda interação contém uma referência à primeira. Por exemplo, se um usuário tocar em um botão e levantar o dedo, o toque vai gerar uma interação PressInteraction.Press, e a liberação vai gerar um PressInteraction.Release. A Release tem uma propriedade press que identifica a PressInteraction.Press inicial.

Você pode ver as interações de um componente específico observando a InteractionSource. A InteractionSource é criada com base nos fluxos Kotlin para que você possa coletar as interações da mesma forma que trabalha com qualquer outro fluxo.

Estado de interação

Para estender a funcionalidade integrada dos componentes, você também precisa monitorar as interações por conta própria. Por exemplo, talvez você queira que um botão mude de cor quando for pressionado. A maneira mais simples de acompanhar as interações é observar o estado adequado da interação. InteractionSource oferece alguns métodos que revelam vários status de interação como estado. Por exemplo, se você quiser ver se um botão específico foi pressionado, chame o método InteractionSource.collectIsPressedAsState() correspondente:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(
    onClick = { /* do something */ },
    interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Além de collectIsPressedAsState(), o Compose também fornece collectIsFocusedAsState(), collectIsDraggedAsState() e collectIsHoveredAsState(). Na verdade, esses são métodos de conveniência baseados nas APIs InteractionSource de nível inferior. Em alguns casos, convém usar essas funções de nível inferior diretamente.

Por exemplo, suponha que você precise saber se um botão está sendo pressionado e também se ele está sendo arrastado. Se você usar collectIsPressedAsState() e collectIsDraggedAsState(), o Compose fará muito trabalho duplicado, e não há garantia de que você receberá todas as interações na ordem certa. Em situações como essa, você pode trabalhar diretamente com a InteractionSource. A seção a seguir descreve como você pode acompanhar as interações, conseguindo apenas as informações necessárias.

Trabalhar com InteractionSource

Se você precisar de informações de baixo nível sobre interações com um componente, use as APIs de fluxo padrão para a InteractionSource desse componente. Por exemplo, suponha que você queira manter uma lista das interações de pressionar e arrastar para uma InteractionSource. Esse código faz metade do trabalho, adicionando os novos pressionamentos à lista conforme eles chegam:

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
        }
    }
}

Mas, além de adicionar as novas interações, você também precisará removê-las quando elas terminarem, por exemplo, quando o usuário levantar o dedo do componente. Isso é fácil, porque as interações finais sempre carregam uma referência à interação inicial associada. O código a seguir mostra como remover as interações que terminaram:

val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is PressInteraction.Release -> {
                interactions.remove(interaction.press)
            }
            is PressInteraction.Cancel -> {
                interactions.remove(interaction.press)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
            is DragInteraction.Stop -> {
                interactions.remove(interaction.start)
            }
            is DragInteraction.Cancel -> {
                interactions.remove(interaction.start)
            }
        }
    }
}

Agora, se você quiser saber se o componente está sendo pressionado ou arrastado, basta verificar se interactions está vazio:

val isPressedOrDragged = interactions.isNotEmpty()

Para saber qual foi a interação mais recente, basta analisar o último item da lista. Por exemplo, veja como a implementação de ondulação do Compose determina a sobreposição de estado adequada para usar na interação mais recente:

val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}

Trabalhando com um exemplo

Para ver como criar componentes com uma resposta personalizada para entrada, veja um exemplo de botão modificado. Nesse caso, suponha que você queira um botão que responda aos pressionamentos mudando a aparência:

Animação de um botão que adiciona dinamicamente um ícone quando clicado

Para fazer isso, crie um elemento personalizado que pode ser composto com base em Button e use um parâmetro icon adicional para desenhar o ícone, neste caso, um carrinho de compras. Você chama collectIsPressedAsState() para rastrear se o usuário está passando o cursor sobre o botão. Quando estiver, você vai adicionar o ícone. O código vai ficar assim:

@Composable
fun PressIconButton(
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource =
        remember { MutableInteractionSource() },
) {
    val isPressed by interactionSource.collectIsPressedAsState()
    Button(onClick = onClick, modifier = modifier,
        interactionSource = interactionSource) {
        AnimatedVisibility(visible = isPressed) {
            if (isPressed) {
                Row {
                    icon()
                    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                }
            }
        }
        text()
    }
}

Veja como usar esse novo elemento de composição:

PressIconButton(
    onClick = {},
    icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
    text = { Text("Add to cart") }
)

Como esse novo PressIconButton é criado com base no Button do Material já existente, ele reage às interações do usuário de todas as maneiras normais. Quando o usuário pressiona o botão, ele muda um pouco a opacidade, como um Button comum do Material. Além disso, graças ao novo código, o HoverIconButton responde dinamicamente ao cursor ao adicionar um ícone.