Quando ho scritto il mio ultimo articolo su Jetpack Compose, l’ho descritto come mancante di alcuni componenti di base (a mio parere) e uno di essi è il tooltip.

All’epoca, non esisteva nessuna composabile integrata per mostrare i tooltip e erano in giro varie soluzioni alternative online. Il problema con queste soluzioni era che una volta che Jetpack Compose fosse stato rilasciato in versioni più nuove, queste soluzioni avrebbero potuto non funzionare. Perciò non era ideale e la comunità era lasciata sperare che in futuro si aggiungerebbe il supporto per i tooltip.

Sono contento di dire che da la versione 1.1.0 di Compose Material 3, ora abbiamo un supporto integrato per i tooltip. 👏

Anche se questo è già fantastico, più di un anno è passato da quando quella versione è stata rilasciata. E con le versioni successive, l’API relativa ai tooltip è cambiata drasticamente pure.

Se guardate il changelog, vedrete come le API pubbliche e interne sono state modificate. Quindi ricordate, che quando leggete questo articolo, le cose potrebbero aver continuato a cambiare poiché tutto ciò che riguarda i Tooltips è ancora contrassegnato dallaannotazione ExperimentalMaterial3Api::class.

❗️ La versione di material 3 usata per questo articolo è la 1.2.1, rilasciata il 6 marzo 2024

Tipi di tooltip

Ora abbiamo supporto per due tipi diversi di tooltip:

  1. Tooltip standard

  2. Tooltip multimediale

Semplice tooltip

Puoi usare il primo tipo per fornire informazioni su un pulsante icona che altrimenti non sarebbe chiaro. Per esempio, puoi usare un semplice tooltip per indicare all’utente cosa rappresenta il pulsante icona.

Per aggiungere un tooltip alla tua applicazione, usi il composable TooltipBox. Questo composable richiede diversi argomenti:

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

Alcuni di questi dovrebbero sembrarvi familiari se avete già usato i Composables prima. evidencerò qui solo quelli con uno specifico utilizzo:

  • positionProvider – Di tipo PopupPositionProvider, e viene usato per calcolare la posizione dell’tooltip.

  • tooltip – In questo campo puoi disegnare l’interfaccia grafica dell’tooltip.

  • state – In questo campo viene tenuta la situazione associata ad una specifica istanza di Tooltip. Presenta metodi come mostrare/nascondere l’tooltip e quando si crea un’istanza di una, puoi dichiarare se l’tooltip deve essere persistente o no (ovvero se deve rimanere visibile sullo schermo finché l’utente non esegue un’azione di clic esterno all’tooltip).

  • content – Questo è l’UI che il tooltip mostrerà sopra/sotto.

Ecco un esempio di istanziazione di un BasicTooltipBox con tutti i parametri rilevanti compilati:

@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 ha una classe integrata chiamata TooltipDefaults. Puoi usare questa classe per aiutarti nell’istanziazione di argomenti che compongono un TooltipBox. Per esempio, puoi usare TooltipDefaults.rememberPlainTooltipPositionProvider per posizionare correttamente il tooltip in relazione all’elemento dell’ancora.

Rich Tooltip

Un tooltip multimediale richiede più spazio di un tooltip semplice e può essere usato per fornire maggiori informazioni sulla funzionalità di un pulsante icona. Quando il tooltip è mostrato, puoi aggiungere pulsanti e link a esso per fornire spiegazioni o definizioni aggiuntive.

È istanziato in modo simile a un tooltip semplice, all’interno di un TooltipBox, ma usi il composabile 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 del click del pulsante icona */
        }) {
            Icon(imageVector = tooltipIcon,
                contentDescription = "Your icon's description",
                tint = iconColor)
        }
    }

Alcune cose da notare su un tooltip ricco:

  1. Un tooltip ricco supporta il cursore.

  2. Puoi aggiungere unaazione (ovvero un pulsante) alla tooltip per dar all’utente l’opzione di scoprire ulteriore informazione.

  3. Puoi aggiungere logica per chiudere la tooltip.

Casi estremi

Quando si sceglie di marcare lo stato tooltip come persistente, significa che una volta che l’utente interagisce con l’UI che mostra la tooltip, rimarrà visibile fino a quando l’utente preme ovunque sullo schermo.

Se avete guardato l’esempio di una tooltip Ricca da prima, avreste forse notato che abbiamo aggiunto un pulsante per chiudere la tooltip appena cliccato.

C’è un problema che si verifica appena l’utente preme quel pulsante. Poiché l’azione di chiusura viene eseguita sulla tooltip, se un utente vuole eseguire un altro lungo premuto sull’elemento UI che richiama questa tooltip, la tooltip non verrà mostrata di nuovo. Questo significa che lo stato della tooltip è persistente nel momento in cui è chiusa. Allora, come facciamo a risolvere questo?

Per “reimpostare” lo stato della tooltip, dobbiamo chiamare il metodo onDispose che è esposto attraverso lo stato della tooltip. Una volta che lo facciamo, lo stato della tooltip viene reimpostato e la tooltip verrà mostrata di nuovo quando l’utente esegue un lungo premuto sull’elemento 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()  /// <---- QUI
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {

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

Un altro scenario in cui lo stato del tooltip non viene reimpostato è se, invece di chiamarci per il metodo di scomparsa in risposta ad un’azione dell’utente, l’utente fa clic fuori dal tooltip, facendone scomparire. Questo chiama il metodo di scomparsa dietro le quinte e lo stato del tooltip viene impostato come “scomparso”. Catturare il tasto lungo sull’elemento dell’interfaccia utente per vedere di nuovo il tooltip non produce alcun risultato.

La nostra logica che chiama il metodo onDispose del tooltip non viene attivata, quindi come possiamo reimpostare lo stato del tooltip?

Attualmente, non ho trovato una soluzione. Potrebbe essere legato al MutatorMutex del tooltip. Forse con le prossime versioni, ci sarà un’API per questo. Osservo che se ci sono altri tooltip sullo schermo e vengono premuti, questo reimposta il tooltip precedentemente cliccato.

Se vuoi vedere il codice qui presente, puoi andare al repository GitHub

Se vuoi vedere i tooltip in un’applicazione, puoi provarla qui.

Riferimenti