Quando escrevi meu último artigo sobre Jetpack Compose, afirmei que o Jetpack Compose está faltando algumas componentes básicas (do meu ponto de vista), e uma delas é a dica de ferramenta.

Na época, não havia um composável interno para exibir dicas de ferramenta e havia várias soluções alternativas circulando na internet. O problema com essas soluções era que assim que o Jetpack Compose lançasse novas versões, essas soluções poderiam quebrar. Portanto, não era ideal e a comunidade ficou esperando que algum dia fosse adicionada a funcionalidade de suporte para dicas de ferramenta.

Estou feliz em dizer que, desde a versão 1.1.0 do Compose Material 3, agora temos suporte interno para dicas de ferramenta. 👏

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 às dicas de ferramenta 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, quando você ler esse artigo, as coisas podem ter continuado a mudar, pois tudo relacionado às dicas de ferramenta ainda está marcado pela anotação ExperimentalMaterial3Api::class.

📌 A versão do Material 3 usada neste artigo é a 1.2.1, lançada em 6 de março de 2024

Tipos de Dicas de Ferramenta

Nós agora temos suporte para dois tipos diferentes de dicas de ferramenta:

  1. Dica de ferramenta simples

  2. Dica de ferramenta de mídia rica

Simpática dica de ferramenta

Você pode usar o primeiro tipo 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 à sua aplicação, você usa o compositivo TooltipBox. Este compositivo 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 com você se tiver usado compositivos antes. Eu destacarei 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 é onde 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 aparecendo 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 interna chamada TooltipDefaults. Você pode usar essa classe para otimizar os argumentos que compõem um TooltipBox. Por exemplo, você poderia usar TooltipDefaults.rememberPlainTooltipPositionProvider para posicionar corretamente o tooltip em relação ao elemento de anclagem.

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 for 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 composable 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 a serem observadas sobre um Rich tooltip:

  1. Um Rich tooltip tem suporte para um caret.

  2. Você pode adicionar uma ação (ou seja, um botão) à dica para dar aos usuários a opção de obter mais informações.

  3. Você pode adicionar lógica para dispensar a dica.

Casos de Borda

Quando você escolhe marcar o estado da sua dica como persistente, significa que, uma vez que o usuário interaja com a interface do usuário que mostra sua dica, ela permanecerá visível até que o usuário pressione em qualquer outro lugar da tela.

Se você olhou para o exemplo de uma Dica Rica acima, pode ter notado que adicionamos um botão para dispensar a dica quando ela for clicada.

Existe um problema que acontece quando um usuário pressiona esse botão. Uma vez que a ação de dispensar é realizada na dica, se um usuário quiser realizar outro longo pressionamento no item da interface que invoca essa dica, a dica não será mostrada novamente. Isso significa que o estado da dica é persistente em relação a ser dispensada. Então, como resolvermos isso?

Para “redefinir” o estado da dica, temos que chamar o método onDispose que é exposto através do estado da dica. Uma vez que fizemos isso, o estado da dica é redefinido e a dica será mostrada novamente quando o usuário realizar um longo pressionamento no item da interface do usuário.

@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 onde o estado da tooltip não é redefinido é se, em vez de chamarmo de forma automática pelo método de descarte por ação do usuário, o usuário clicar fora da tooltip, fazendo com que ela seja descartada. Isto chama o método de descarte em background e o estado da tooltip é definido como descartado. apertar longo no elemento de UI para ver a nossa tooltip novamente resultará em nada.

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

Atualmente, não consegui descobrir isso. Talvez seja 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 redefine a tooltip clicada anteriormente.

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

Se você quiser ver tooltips em um aplicativo, você pode conferir aqui.

Referências