Na minha última publicação sobre Jetpack Compose, eu afirmei que o Jetpack Compose está faltando algumas componentes básicas (do ponto de vista de minha opinião), e uma delas é a tooltip.

Na época, não havia nenhuma composável integrada para exibir tooltips e havia várias soluções alternativas circulando na internet. O problema com essas soluções era que assim que a versão mais nova do Jetpack Compose fosse lançada, essas soluções poderiam quebrar. Portanto, isso não era ideal e a comunidade ficou esperançosa que, em algum momento no futuro, fosse adicionada ajuda para tooltips.

Estou feliz em dizer que, desde a versão 1.1.0 do Compose Material 3, agora temos suporte integrado para tooltips. 👏

Enquanto isso é excelente, mais de um ano passou desde que essa versão foi lançada. E com as versões subsequentes, a API relacionada aos tooltips mudou drasticamente também.

Se você olhar para o registro de mudanças, verá como as APIs públicas e internas foram alteradas. Portanto, lembre-se, que quando você ler essa公共ao, as coisas podem ter continuado a mudar, já que tudo relacionado aos tooltips ainda está marcado com a anotação ExperimentalMaterial3Api::class.

📌 A versão do material 3 usada nesse artigo é a 1.2.1, que foi lançada em 6 de março de 2024.

Tipos de Tooltips

Agora temos suporte para dois tipos diferentes de tooltips:

  1. Tooltip simples

  2. Dica de ferramenta de mídia rica

Simpática dica de ferramenta

Você pode usar o primeiro para fornecer informações sobre um botão de ícone que não seria claro de outra forma. Por exemplo, você pode usar uma dica de ferramenta simples para indicar ao usuário o que o botão de ícone representa.

Para adicionar uma dica de ferramenta a sua aplicação, você usa o TooltipBox composável. Este composável aceita vários argumentos:

fun TooltipBox(
    positionProvider: PopupPositionProvider,
    tooltip: @Composable TooltipScope.() -> Unit,
    state: TooltipState,
    modifier: Modifier = Modifier,
    focusable: Boolean = true,
    enableUserInput: Boolean = true,
    content: @Composable () -> Unit,
)

Alguns desses devem estar familiarizados se você tiver usado Composables antes. Eu vou destacar aqueles que têm um caso de uso específico aqui:

  • positionProvider – De PopupPositionProvider tipo, e é usado para calcular a posição da dica de ferramenta.

  • tooltip – É aqui que você pode projetar a UI de como a dica de ferramenta vai se parecer.

  • state – Este mantém o estado associado com uma instância específica de Dica de ferramenta. Ele expõe métodos como mostrar/esconder a dica de ferramenta e quando instanciando uma instância de uma, você pode declarar se a dica de ferramenta deve ser persistente ou não (isto é, se deve manter visível na tela até o usuário realizar uma ação de clique fora da dica de ferramenta).

  • conteúdo – Este é a UI que o tooltip exibirá acima/abaixo.

Aqui está um exemplo de instanciar um BasicTooltipBox com todos os argumentos relevantes preenchidos:

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun BasicTooltip() {
    val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
    val tooltipState = rememberBasicTooltipState(isPersistent = false)

    BasicTooltipBox(positionProvider = tooltipPosition,
        tooltip =  { Text("Hello World") } ,
        state = tooltipState) {
        IconButton(onClick = { }) {
            Icon(imageVector = Icons.Filled.Favorite, 
                 contentDescription = "Your icon's description")
        }
    }
}

Jetpack Compose tem uma classe integrada chamada TooltipDefaults. Você pode usar essa classe para ajudá-lo a instanciar argumentos que compõem um TooltipBox. Por exemplo, você poderia usar TooltipDefaults.rememberPlainTooltipPositionProvider para posicionar corretamente o tooltip em relação ao elemento de ancora.

Rich Tooltip

Um rich media tooltip ocupa mais espaço do que um tooltip simples e pode ser usado para fornecer mais contexto sobre a funcionalidade de um botão de ícone. Quando o tooltip é mostrado, você pode adicionar botões e links a ele para fornecer explicações ou definições adicionais.

Ele é instanciado de forma semelhante a um tooltip simples, dentro de um TooltipBox, mas você usa o composável RichTooltip.

TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {
                        Text("This is where a description would go.")
                  }
        },
        state = tooltipState) {
        IconButton(onClick = {
            /* Evento de clique do botão de ícone */
        }) {
            Icon(imageVector = tooltipIcon,
                contentDescription = "Your icon's description",
                tint = iconColor)
        }
    }

Algumas coisas para notar sobre um Rich tooltip:

  1. Um Rich tooltip tem suporte para um carretel.

  2. Você pode adicionar uma ação (isto é, um botão) à tooltip para conceder aos usuários a opção de descobrir mais informações.

  3. Você pode adicionar lógica para fechar a tooltip.

Casos de Limite

Quando você escolhe marcar seu estado de tooltip como persistente, isso significa que assim que o usuário interagir com a UI que mostra sua tooltip, ela permanecerá visível até o usuário pressionar algum outro local na tela.

Se você olhar para o exemplo de uma tooltip Rica acima, você pode ter notado que nós adicionamos um botão para fechar a tooltip assim que ela for clicada.

Há um problema que acontece assim que o usuário pressiona esse botão. Como a ação de fechar é executada na tooltip, se um usuário quiser executar outro toque longo na item de UI que invoca essa tooltip, a tooltip não será mostrada novamente. Isso significa que o estado da tooltip é persistente após seu fechamento. Então, como resolver isso?

Para “reverter” o estado da tooltip, temos que chamar o método onDispose que é exposto através do estado da tooltip. Assim que fizermos isso, o estado da tooltip é redefinido e a tooltip será mostrada novamente quando o usuário realizar um toque longo no item de UI.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RichTooltip() {
    val tooltipPosition = TooltipDefaults.rememberRichTooltipPositionProvider()
    val tooltipState = rememberTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = TooltipDefaults.caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()  /// <---- AQUI
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {

                  }
        },
        state = tooltipState) {
        IconButton(onClick = {  }) {
            Icon(imageVector = Icons.Filled.Call, contentDescription = "Your icon's description")
        }
    }
}

Outro cenário em que o estado da tooltip não é resetado é se, em vez de chamarmo-nos pelo método de fechar por ação do usuário, o usuário clicar fora da tooltip, fazendo com que ela seja fechada. Isto chama o método de fechar em cima do fundo e o estado da tooltip é definido como fechado. A longa pressão no elemento UI para ver a nossa tooltip novamente resultará em nada.

Nossa lógica que chama o método onDispose da tooltip não é disparada, então como podemos resetar o estado da tooltip?

Atualmente, não consegui descobrir isso. Pode ser relacionado ao MutatorMutex da tooltip. Talvez com novas versões, haja uma API para isso. Notei que se outras tooltips estiverem presentes na tela e forem pressionadas, isso reseta a tooltip clicada anteriormente.

Se você quiser ver o código aqui apresentado, pode ir para este repositório GitHub

Se você quiser ver tooltips em uma aplicação, pode conferir aqui.

Referências