Tooltips En Android

Las Tooltips muestran información sobre otro elemento con el fin de comunicar al usuario sobre su propósito.

Tipos de Tooltips en Material Design

Existen dos tipos:

  • Plain tooltip: Usada para descripciones cortas (comúnmente en action buttons)
  • Rich tooltip: Usada para descripciones con más detalle e incluso acciones asociadas

Dicho la anterior, en este tutorial veremos cómo crear ambos tipos de tooltip con la librería Jetpack Compose de Android y como leer los eventos del usuario para su aparición en pantalla.

Proyecto De Tooltips De Android Studio

1. Para estudiar el uso de las tooltips, crea un nuevo proyecto en Android Studio con la plantilla de Jetpack Compose y nombralo «Tooltips».

2. Luego crea un archivo Kotlin llamado TooltipsScreen.kt y añade una función componible con el mismo nombre. Esta representará el diseño de la pantalla donde mostraremos nuestros ejemplos.

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun TooltipsScreen() {

}

@Preview
@Composable
private fun Preview() {
    TootipsTheme {
        TooltipsScreen()
    }
}

3. Abre MainActivity.kt y reemplaza el contenido de setContent() por la invocación de la función que añadimos en el paso anterior:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TootipsTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    TooltipsScreen()
                }
            }
        }
    }
}

Por otro lado, si deseas ver el código final de inmediato o tuviste problemas siguiendo las explicaciones, puedes encontrar el proyecto completo en mi repositorio de GitHub:


1. Plain Tooltip

Ejemplo de Plain Tooltip
Ejemplo de Plain Tooltip

El primer ejemplo que veremos será el uso de una Plain Tooltip sobre un Floating Action Button como se presenta en la ilustración anterior.

El FAB representa una acción de creación, por lo que nuestro objetivo es mostrar una etiqueta que describa dicha función cuando el usuario haga un Long Click.

Con esto en mente, veamos cómo hacerlo.

Crear Floating Action Button

Iniciemos declarando un elemento Scaffold en TooltipsScreen() y añadiéndole el FAB que necesitamos:

@Composable
fun TooltipsScreen() {
    Scaffold(
        topBar = { TopBar() },
        floatingActionButton = { Fab() },
        content = { padding -> MainContent(padding) }
    )
}

Crea la función Fab() en un nuevo archivo Fab.kt e invoca a LargeFloatingActionButton() con el icono y descripción de creación:

@Composable
fun Fab() {
    LargeFloatingActionButton(onClick = { /*TODO*/ }) {
        Icon(
            imageVector = Icons.Default.Add,
            contentDescription = "Crear"
        )
    }
}

Crear Tooltip Box

Tanto las plain tooltips como las rich tooltips usan al componente TooltipBox() para organizar la superposición de la tooltip con el elemento a describir:

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

Con este componente evitamos preocuparnos por el posicionamiento y las especificaciones estándar de la tooltip. E incluso maneja el evento de Long Press del usuario y oculta la tooltip luego de un tiempo.

Continuando, recubre al FAB con TooltipBox() y pasa como argumentos los siguientes valores:

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeFloatingActionButton
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun Fab() {
    TooltipBox(
        positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
        tooltip = {  },
        state = rememberTooltipState()
    ) {
        LargeFloatingActionButton(onClick = { /*TODO*/ }) {
            Icon(
                imageVector = Icons.Default.Add,
                contentDescription = "Crear"
            )
        }
    }
}

Del código anterior:

  • positionProvider: Elemento usado para posicionar de la tooltip con respecto a content. Usamos la utilidad rememberPlaintTooltipPositionProvider() para que el framework ubique correctamente la tootip simple
  • tooltip: Aquí irá la tooltip que veremos a continuación
  • state: Estado de la tooltip. La función rememberTooltipState() crea y recuerda un valor por defecto
  • content: Trailing lambda con parámetro componible para ubicar nuestro Fab

Crear Plain Tooltip

La pequeña superficie con la descripción de la plain tooltip es producida por la función componible PlainTooltip():

@Composable
fun PlainTooltip(
    modifier: Modifier = Modifier,
    contentColor: Color = TooltipDefaults.plainTooltipContentColor,
    containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
    shape: Shape = TooltipDefaults.plainTooltipContainerShape,
    content: @Composable () -> Unit
): Unit

En su invocación solo necesitamos el uso de content para pasar el texto de la etiqueta a renderizar. Por lo que pasaremos un texto sin más:

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun Fab() {
    TooltipBox(
        tooltip = { PlainTooltip { Text("Crear") } },
        //..
    ) {
        //..
    }
}

Ahora corre el proyecto y prueba haciendo un click prolongado sobre el FAB para visualizar la etiqueta:

Plain Tooltip en acción
Plain Tooltip en acción

2. Rich Tooltip

Ejemplo de Rich Tooltip
Ejemplo de Rich Tooltip

En el segundo ejemplo desplegaremos una Rich Tooltip sobre un action button de una Top App Bar, al igual que en la ilustración anterior.

Este caso muestra como este tipo de tooltip puede informar con mayor detalle el objetivo de un componente de UI, e incluso proveer una acción asociada para ampliar aún más el detalle (esto lo veremos en el ejemplo de la persistent rich tooltip).

Crear Top App Bar

Crea un nuevo archivo Kotlin llamado TopBar.kt y añade la función TopBar(). En su interior invoca a CenterAlignedTopAppBar() para materializar una barra con título centrado. Añádele una acción para ajustes:

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun TopBar() {
    CenterAlignedTopAppBar(
        title = { AppBarTitle() },
        actions = { SettingsAction() }
    )
}

AppBarTitle() representa al texto de la top bar y SettingsAction() contiene la cobertura de la rich tooltip sobre el icon button de ajustes.

@Composable
private fun AppBarTitle() {
    Text(stringResource(R.string.app_name))
}

Crear Toolptip Box

Ya que el Icon Button será el elemento a describir, lo envolvemos con TooltipBox():

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun SettingsAction() {
    TooltipBox(
        positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
        tooltip = { SettingsTooltip() },
        state = rememberTooltipState(),
        content = { SettingsIconButton() }
    )
}

@Composable
private fun SettingsIconButton() {
    IconButton(onClick = {}) {
        Icon(
            imageVector = Icons.Default.Settings,
            contentDescription = "Ajustes"
        )
    }
}

Y similar a la plain tooltip, existe la función de utilidad rememberRichTooltipPositionProvider(), cuyo objetivo es ubicar correctamente la rich tooltip.

Crear Rich Tooltip

Ahora en el slot para la tooltip invocaremos a la función RichTooltip():

@Composable
fun RichTooltip(
    modifier: Modifier = Modifier,
    title: (@Composable () -> Unit)? = null,
    action: (@Composable () -> Unit)? = null,
    colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
    shape: Shape = TooltipDefaults.richTooltipContainerShape,
    text: @Composable () -> Unit
): Unit

En su firma tendremos los elementos de la anatomía:

  • title: Título
  • text: Texto de apoyo
  • action: Las acciones a usar

Por lo que la construimos con los siguientes argumentos:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SettingsTooltip() {
    RichTooltip(
        title = { Text("Ajustes") },
        text = { Text("Preferencias de tu cuenta y comportamiento de la App") },
    )
}

Mostrar Rich Tooltip

De nuevo, la aparición y el evento de long press los maneja la tooltip box, así solo resta correr el proyecto y comprobar que aparezca la rich tooltip al interactuar con el icono de ajustes en la Top App Bar.

Rich Tooltip en acción
Rich Tooltip en acción

3. Persistent Rich Tooltip

Ejemplo de Persistent Rich Tooltip
Ejemplo de Persistent Rich Tooltip

El comportamiento por defecto de las tooltips es desaparecer luego de alejarse del área del componente de UI descrito o cuando aparece otra tooltip.

No obstante, los lineamientos de M3 promueven el uso de Rich tooltips persistentes, las cuales se mantiene visibles hasta que se interactúe con otro elemento de UI o con la acción de la misma.

Estas ya no aparecen por un evento de long press, si no, al hacer tap sobre el elemento de UI; o automáticamente para introducir una nueva característica de la App.

Para este caso veremos un ejemplo en el que al iniciar el aplicativo, se presenta un texto con la cantidad de tareas pendientes, como una nueva característica añadida.

Pasemos a implementarlo.

Crear Contenido Del Scaffold

La sección rectangular que se observa en la ilustración del ejemplo es representada por una columna con un texto en su interior. Esta será ubicada en el contenido principal del Scaffold. Así que crearemos en un archivo nuevo llamado MainContent.kt y la función respectiva:

@Composable
fun MainContent(
    padding: PaddingValues
) {
    Box(
        modifier = Modifier
            .padding(padding)
            .fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        NewFeaturePanel()
    }
}

La función NewFeaturePanel() será donde pondremos el panel de tareas pendientes y lo recubriremos con la tooltip box persistente.

Crear Persistent Rich Tooltip

Al interior de NewFeaturePanel() declaramos el estado personalizado para una rich tooltip persistente y se lo pasamos a la invocación de TooltipBox():

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun NewFeaturePanel() {
    val state = rememberTooltipState(
        initialIsVisible = true,
        isPersistent = true
    )
    TooltipBox(
        positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
        tooltip = { PersistentRichTooltip(state) },
        enableUserInput = false,
        state = state,
        content = { PendingTasksInfo(state.isVisible) }
    )
}

La configuración de nuestra tooltip persistente se basa en aplicar:

  • initialIsVisible -> Usa true para mostrar la tooltip automáticamente
  • isPersistent -> Usa true para marcar persistencia de la tooltip
  • enableUserInput-> Usa false para evitar procesar el long press del usuario

Acto seguido, pasamos a definir la rich tooltip en PersistentRichTooltip() con los valores en la ilustración del ejemplo:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PersistentRichTooltip(state: TooltipState) {
    RichTooltip(
        title = {
            Row(verticalAlignment = Alignment.CenterVertically) {
                NewReleaseIcon()
                Spacer(Modifier.size(4.dp))
                Text("Nueva característica disponible")
            }
        },
        action = {
            TextButton(onClick = { state.dismiss() }) {
                Text("Lo tengo")
            }
        },
        text = {
            Text(
                "Ahora puedes ver la cantidad de tareas pendientes en " +
                        "este panel para mantenerte al tanto de tu progreso"
            )
        }
    )
}

La función recibe el estado como parámetro, el cual nos permitirá ocultar la tooltip cuando el Text Button sea presionado.

Efecto que se logra invocando a TooltipState.dismiss() en onClick (Usa show() cuando desees mostrar una tooltip persistente por evento de click)

Cambiar Bakground De Panel

Con la función PendingTaskInfo() defineremos el diseño del panel:

@Composable
private fun PendingTasksInfo(isFocused: Boolean) {
    Column(
        Modifier
            .width(300.dp)
            .height(56.dp)
    ) {

        Text(
            text = "Cantidad de tareas pendientes: 10",
            modifier = Modifier
                .background(color = pendingTasksInfoColor(isFocused))
                .border(
                    width = Dp.Hairline,
                    color = Color.LightGray,
                    shape = RoundedCornerShape(
                        4.dp
                    )
                )
                .padding(8.dp)
                .fillMaxWidth()
        )
    }
}

@Composable
private fun pendingTasksInfoColor(isFocused: Boolean): Color {
    return if (isFocused)
        MaterialTheme.colorScheme.primary.copy(alpha = 0.08f)
    else
        MaterialTheme.colorScheme.surface
}

El color del background en el panel es determinado por la visibilidad de la tooltip, por eso se usa el valor de la propiedad TooltipState.isVisible como argumento de PendingTaskInfo().

Finaliza esta lectura ejecutando el proyecto Android Studio y evidencia como la Rich Tooltip aparece al iniciar el aplicativo. Además de no ocultarse a menos que presiones el botón o en un área por fuera de la tooltip.

Persistent Rich Tooltip en acción
Persistent Rich Tooltip en acción

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!