Tocar y presionar

Muchos elementos componibles tienen compatibilidad integrada con presiones o clics, e incluyen una expresión lambda onClick. Por ejemplo, puedes crear un elemento Surface en el que se pueda hacer clic que incluya todo el comportamiento de Material Design adecuado para la interacción con plataformas:

Surface(onClick = { /* handle click */ }) {
    Text("Click me!", Modifier.padding(24.dp))
}

Sin embargo, los clics no son la única forma en que un usuario puede interactuar con los elementos componibles. Esta página se enfoca en los gestos que involucran un solo puntero, donde la posición de ese puntero no es significativa para el control de ese evento. En la siguiente tabla, se enumeran estos tipos de gestos:

Gesto

Description

Presiona (o haz clic)

El puntero baja y, luego, hacia arriba

Presionar dos veces

El puntero va hacia abajo, arriba, abajo, arriba

mantener presionado

El puntero deja de funcionar y se mantiene presionado durante más tiempo

Prensa

El puntero cae

Responder a presiones o clics

clickable es un modificador de uso general que hace que un elemento componible reaccione a los toques o clics. Este modificador también agrega funciones adicionales, como compatibilidad con el enfoque, el desplazamiento del mouse y la pluma stylus, y una indicación visual personalizable cuando se presiona. El modificador responde a "clics" en el sentido más amplio de la palabra, no solo con el mouse o el dedo, sino también con eventos de clic mediante la entrada del teclado o cuando se usan servicios de accesibilidad.

Imagina una cuadrícula de imágenes, en la que una imagen se muestra en pantalla completa cuando un usuario hace clic en ella:

Puedes agregar el modificador clickable a cada elemento de la cuadrícula para implementar este comportamiento:

@Composable
private fun ImageGrid(photos: List<Photo>) {
    var activePhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
    LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
        items(photos, { it.id }) { photo ->
            ImageItem(
                photo,
                Modifier.clickable { activePhotoId = photo.id }
            )
        }
    }
    if (activePhotoId != null) {
        FullScreenImage(
            photo = photos.first { it.id == activePhotoId },
            onDismiss = { activePhotoId = null }
        )
    }
}

El modificador clickable también agrega comportamiento adicional:

  • interactionSource y indication, que dibujan una onda de forma predeterminada cuando un usuario presiona el elemento componible. Descubre cómo personalizarlos en la página Cómo controlar las interacciones del usuario.
  • Permite que los servicios de accesibilidad interactúen con el elemento configurando la información semántica.
  • Admite la interacción con el teclado o el joystick, ya que permite el enfoque y presiona Enter, o el centro del pad direccional, para interactuar.
  • Permite que se pueda colocar el cursor sobre el elemento, de modo que responda al mouse o la pluma stylus que se coloca sobre él.

Mantén presionado para mostrar un menú contextual contextual

combinedClickable te permite agregar un comportamiento de presionar dos veces o mantener presionado, además del comportamiento normal de clics. Puedes usar combinedClickable para mostrar un menú contextual cuando un usuario toque y mantenga presionada una imagen de cuadrícula:

var contextMenuPhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
val haptics = LocalHapticFeedback.current
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
    items(photos, { it.id }) { photo ->
        ImageItem(
            photo,
            Modifier
                .combinedClickable(
                    onClick = { activePhotoId = photo.id },
                    onLongClick = {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                        contextMenuPhotoId = photo.id
                    },
                    onLongClickLabel = stringResource(R.string.open_context_menu)
                )
        )
    }
}
if (contextMenuPhotoId != null) {
    PhotoActionsSheet(
        photo = photos.first { it.id == contextMenuPhotoId },
        onDismissSheet = { contextMenuPhotoId = null }
    )
}

Como práctica recomendada, debes incluir respuestas táctiles cuando el usuario mantiene presionados los elementos, por lo que el fragmento incluye la invocación performHapticFeedback.

Presiona una lámina para descartar un elemento componible

En los ejemplos anteriores, clickable y combinedClickable agregan funcionalidades útiles a tus elementos componibles. Muestran una indicación visual sobre la interacción, responden al desplazamiento del cursor y, además, incluyen compatibilidad con el enfoque, el teclado y la accesibilidad. Sin embargo, este comportamiento adicional no siempre es conveniente.

Veamos la pantalla de detalles de la imagen. El fondo debe ser semitransparente, y el usuario debe poder presionarlo para descartar la pantalla de detalles:

En este caso, el fondo no debe tener ninguna indicación visual sobre la interacción, no debe responder al colocar el cursor sobre un elemento, no debe ser enfocable y su respuesta a los eventos de teclado y accesibilidad difiere de la de un elemento componible típico. En lugar de intentar adaptar el comportamiento de clickable, puedes desplazarte a un nivel de abstracción más bajo y usar directamente el modificador pointerInput en combinación con el método detectTapGestures:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun Scrim(onClose: () -> Unit, modifier: Modifier = Modifier) {
    val strClose = stringResource(R.string.close)
    Box(
        modifier
            // handle pointer input
            .pointerInput(onClose) { detectTapGestures { onClose() } }
            // handle accessibility services
            .semantics(mergeDescendants = true) {
                contentDescription = strClose
                onClick {
                    onClose()
                    true
                }
            }
            // handle physical keyboard input
            .onKeyEvent {
                if (it.key == Key.Escape) {
                    onClose()
                    true
                } else {
                    false
                }
            }
            // draw scrim
            .background(Color.DarkGray.copy(alpha = 0.75f))
    )
}

Como tecla del modificador pointerInput, pasas la lambda onClose. De esta manera, se volverá a ejecutar la expresión lambda automáticamente y se asegurará de que se llame a la devolución de llamada correcta cuando el usuario presione la lámina.

Presiona dos veces para acercar

A veces, clickable y combinedClickable no incluyen suficiente información para responder a la interacción de la manera correcta. Por ejemplo, es posible que los elementos componibles necesiten acceso a la posición dentro de los límites del elemento donde se llevó a cabo la interacción.

Veamos la pantalla de detalles de la imagen de nuevo. Una práctica recomendada es hacer que sea posible acercar la imagen presionando dos veces:

Como se puede ver en el video, el acercamiento se produce alrededor de la posición del evento de toque. El resultado es diferente cuando acercamos la imagen en la parte izquierda en comparación con la parte derecha. Podemos usar el modificador pointerInput junto con el detectTapGestures para incorporar la posición de toque en nuestro cálculo:

var zoomed by remember { mutableStateOf(false) }
var zoomOffset by remember { mutableStateOf(Offset.Zero) }
Image(
    painter = rememberAsyncImagePainter(model = photo.highResUrl),
    contentDescription = null,
    modifier = modifier
        .pointerInput(Unit) {
            detectTapGestures(
                onDoubleTap = { tapOffset ->
                    zoomOffset = if (zoomed) Offset.Zero else
                        calculateOffset(tapOffset, size)
                    zoomed = !zoomed
                }
            )
        }
        .graphicsLayer {
            scaleX = if (zoomed) 2f else 1f
            scaleY = if (zoomed) 2f else 1f
            translationX = zoomOffset.x
            translationY = zoomOffset.y
        }
)