En este tutorial veremos los modificadores de gestos que existen en Jetpack Compose con el fin de procesar los eventos generados por el usuario a través de la pantalla del dispositivo. Eventos como clics, scroll, drag, swipe y multitouch.
Ejemplo De Modificadores De Gestos
Para mostrar la implementación del manejo de los gestos hemos creado un nuevo módulo llamado p5_gestos
dentro del proyecto Android Studio de guía de Compose:
Cada sección tiene asociado un archivo Kotlin del paquete examples
. Puedes probar cada uno, reemplazando la invocación de la función componible desde GesturesActivity
en setContent()
:
class GesturesActivity : ComponentActivity() {
@ExperimentalMaterialApi
@ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RotationExample()
}
}
}
Descargar el proyecto desde el siguiente enlace:
Teniendo esto en cuenta, comencemos con el procesamiento del primer gesto.
Manejar Gestos De Tapping
Usa el modificador clickable()
para permitir que un elemento de UI reciba clics desde la pantalla (toque breve con la punta del dedo) o el evento de clic del servicio de accesibilidad.
fun Modifier.clickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
): Modifier
Sus parámetros permiten:
enabled
: Activa y desactiva este modificadoronClickLabel
: Etiqueta de accesibilidad para la acción de clicrole
: Describe el tipo de elemento en la interfaz de usuario para los servicios de accesibilidadonClick
: Manejador ejecutado cuando se hace clic en el elemento
A diferencia del sistema de views, clickable incluye los efectos visuales cuando se presiona el elemento en pantalla.
Por ejemplo:
Modifiquemos el color de fondo un elemento Box
cuando el usuario tapea su contenido:
@Composable
@Preview
fun ClickableExample() {
var background by remember {
mutableStateOf(Color.Blue)
}
Box(
Modifier
.background(background)
.size(150.dp)
.clickable {
background = randomColor()
}
)
}
fun randomColor() = Color(Random.nextLong(0xFFFFFFFF))
En el código anterior hemos aplicado clickable()
con una lambda al final para onClick
, donde modificamos el estado background
a partir de la función randomColor()
.
Si ejecutas la App y cliqueas la caja, verás el cambio de background aleatorio:
Lectura: Aprende más sobre el componente Box y estados en Compose.
Doble Clic Y Clic Prolongado
En el caso que desees manejar eventos de doble clic y clic prolongado, invoca al modificador combinedClickable()
.
@ExperimentalFoundationApi
fun Modifier.combinedClickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: () -> Unit = null,
onDoubleClick: () -> Unit = null,
onClick: () -> Unit
): @ExperimentalFoundationApi Modifier
Este provee el procesamiento de ambos eventos con los parámetros adicionales onDoubleClick
y onLongClick
.
Por ejemplo:
Manejemos los eventos de tap, double tap y long press desde un Text. Adicional, en cada interacción cambiamos su estado por un String que represente al gesto detectado.
@ExperimentalFoundationApi
@Composable
fun CombinedClickableExample() {
var text by remember {
mutableStateOf("Ninguno")
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Evento: $text",
Modifier
.combinedClickable(
onDoubleClick = {
text = "Double tap"
},
onLongClick = {
text = "Long press"
},
onClick = {
text = "Tap"
}),
fontSize = 24.sp
)
}
}
El ejemplo anterior retiene el tipo de evento manejado como estado del elemento Text
. Luego asignamos 3 lambdas a onDoubleClick
, onLongClick
y onClick
para mutar este valor con el texto correspondiente.
Al ejecutar el ejemplo y aplicar los eventos, el resultado se vería así:
Manejar Gestos De Scrolling
Scroll Horizontal
Aplica el modificador horizontalScroll()
sobre un elemento de UI, para habilitar su desplazamiento horizontal cuando su contenido es mas grande que sus restricciones máximas de ancho.
fun Modifier.horizontalScroll(
state: ScrollState,
enabled: Boolean = true,
flingBehavior: FlingBehavior? = null,
reverseScrolling: Boolean = false
): Modifier
Donde:
state
: Es el estado del scrollenabled
: Determina si el scroll está activo o noflingBehavior
: Especifica la lógica del comportamiento cuando finaliza el desplazamientoreverseScrolling
: Invierte la dirección del scrolling. Si pasastrue
, la posición inicial será la derecha, si pasasfalse
será la izquierda
Como ves, es posible invocar el modificador sin los últimos 3 parámetros, ya que tienen valores por defecto comunes a la mayoría de situaciones.
En el caso de state
, crearemos y recordaremos el estado con la función rememberScrollState()
, la cual facilita la configuración de una instancia de tipo ScrollState
.
Por ejemplo:
Creemos una fila que llene el ancho total de la pantalla con scroll horizontal. Luego añadimos 10 cajas como hijas con colores de fondo aleatorios.
@Composable
fun HorizontalScrollExample() {
val scrollState = rememberScrollState()
Row(
Modifier
.fillMaxWidth()
.horizontalScroll(scrollState)
) {
repeat(10) {
Box(
modifier = Modifier
.size(100.dp)
.background(randomColor())
)
}
}
}
El código anterior añade a la cadena de modificadores a horizontalScroll()
junto a su estado. Luego usamos la función repeat()
de Kotlin para generar las 10 cajas con una sola línea. La salida será:
Scroll Vertical
Añade el modificador verticalScroll()
para permitir el desplazamiento vertical en el caso que el alto del contenido exceda las restricciones del eje Y. Sus parámetros son exactamente iguales que horizontaScroll()
.
Por ejemplo:
Creemos una columna cuya altura sea de 300dp y posea scroll vertical. Luego añadamos 10 cajas con diferentes colores de fondo para visualizar el desplazamiento.
@Composable
fun VerticalScrollExample() {
val scrollState = rememberScrollState()
Column(
Modifier
.height(300.dp)
.fillMaxWidth()
.verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally
) {
repeat(10) {
Box(
modifier = Modifier
.size(100.dp)
.background(randomColor())
)
}
}
}
Al desplazar el contenido verticalmente:
Scroll Sin Movimiento
Otra forma para detectar el scroll de un contenido es el modificador scrollable()
. A diferencia del scroll vertical y horizontal, este no desplaza el contenido sobre las restricciones, si no que hace un seguimiento de la distancia del scroll a partir de su parámetro state
.
fun Modifier.scrollable(
state: ScrollableState,
orientation: Orientation,
enabled: Boolean = true,
reverseDirection: Boolean = false,
flingBehavior: FlingBehavior? = null,
interactionSource: MutableInteractionSource? = null
): Modifier
Esta función requiere de la orientación del scroll y la creación del estado con rememberScrollableState()
. La cual recibe una lambda del tipo (Float) -> Float
, donde los parámetro son los píxeles recorridos del scroll y el resultado la cantidad de scroll consumida en el evento.
Por ejemplo:
Aumentemos o disminuyamos el canal alfa de un rectángulo cuando se realiza un gesto de scroll vertical en su contenido:
@Composable
fun ScrollableExample() {
val boxSize = 300f
var scrollDeltaSum by remember {
mutableStateOf(boxSize)
}
Column(
Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Alpha = ${scrollDeltaSum / boxSize}")
Box(
modifier = Modifier
.size(boxSize.dp)
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState { delta ->
scrollDeltaSum = (scrollDeltaSum - delta / 2).coerceIn(0f, boxSize)
delta
}
)
.alpha(scrollDeltaSum / boxSize)
.background(Color.Magenta)
)
}
}
Como ves, creamos un estado llamado scrollDeltaSum
con un valor inicial igual a las dimensiones de la caja (100%).
Esta variable llevará la suma de la cantidad de scroll en píxeles detectada en rememberScrollableState()
. No obstante, forzamos a que su valor se encuentre entre 0 y 300 con coerceIn()
.
Esto nos permite calcular el valor del modificador alpha()
a partir de la división entre scrollDeltaSum
y boxSize
.
El resultado será:
Scroll Anidado
Jetpack Compose te permite anidar modificadores de scrolling en una jerarquía si su comportamiento es simple.
Por ejemplo:
Creemos una fila con 5 columnas, donde cada columna tiene 5 textos hacia.
@Preview
@Composable
fun NestedScrollExample() {
Row(
Modifier
.fillMaxWidth()
.height(100.dp)
.horizontalScroll(rememberScrollState())
) {
repeat(5) {
Column(
Modifier
.size(100.dp)
.background(randomColor())
.verticalScroll(rememberScrollState())
) {
repeat(5) {
Text(
"Vertical",
Modifier.padding(16.dp)
)
}
}
}
}
}
Como ves, es solo usar horizontalScroll()
en la fila para desplazar las columnas y verticalScroll()
sobre cada fila para desplazar los elementos Text
. Este scroll anidado se proyecta así:
Nota: En el caso en el que requieras coordinación avanzada entre los desplazamientos, entonces usa el modificador nestedScroll()
para aplicar conexiones y despachos de eventos del scroll anidado con el scroll padre.
Manejar Gestos De Arrastre
El modificador dragglable()
le permite a un elemento recibir gestos de arrastre, donde el usuario toca el contenido y arrastra su dedo sin perder contacto con la superficie.
Este no realiza el desplazamiento automáticamente, por lo que debes actualizar el estado del modificador offset
para lograr el movimiento.
fun Modifier.draggable(
state: DraggableState,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
startDragImmediately: Boolean = false,
onDragStarted: suspend CoroutineScope.(Offset) -> Unit = {},
onDragStopped: suspend CoroutineScope.(Float) -> Unit = {},
reverseDirection: Boolean = false
): Modifier
Usa su parámetro state
del tipo DragglabeState
, para definir como los eventos de arrastre serán interpretados cuando el elemento aterrice sobre la superficie.
La función rememberDraggableState()
será la encargada de crear y recordar el estado, proporcionandote la cantidad de píxeles en su parámetro onDelta de tipo (Float)->Unit
.
Por ejemplo:
Permitamos que un círculo sea arrastrado verticalmente con el siguiente componible:
@Composable
fun DraggableExample() {
var offsetY by remember {
mutableStateOf(0f)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.offset { IntOffset(0, offsetY.toInt()) }
.background(Color.Magenta, CircleShape)
.size(50.dp)
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
offsetY += delta
})
)
}
}
La función componible anterior establece como estado el desplazamiento vertical del arrastre en offsetY
. Su modificación se realiza en rememberDraggableState()
y su actualización en el modificador offset()
.
Al ejecutar podrás ver lo siguiente:
Manejar Gestos De Swipe
Usa el modificador swipeable()
para habilitar gestos de swipe entre un conjunto de estados predefinidos. Es decir, el movimiento del contenido de un elemento a lo largo o ancho de sus límites.
<T : Any?> Modifier.swipeable(
state: SwipeableState<T>,
anchors: Map<Float, T>,
orientation: Orientation,
enabled: Boolean,
reverseDirection: Boolean,
interactionSource: MutableInteractionSource?,
thresholds: (from, to) -> ThresholdConfig,
resistance: ResistanceConfig?,
velocityThreshold: Dp
)
Ten en cuenta el propósito de los siguientes parámetros a la hora de su invocación:
state:
Estado del modificador de swipe. Contiene el valor actual actual del ancla que ha sido alcanzada por el gesto de swipe. Crearemos y recordaremos el estado conrememberSwipeableState()
.anchors:
Mapeado de anclas y estados. Las anclas (en píxeles) actúan como restricciones de desplazamiento con respecto a un estado del tipo Tthresholds: (from, to) -> ThresholdConfig
: Representa a los umbrales entre diferentes estados
Veamos un ejemplo:
Procesemos el gesto de swipe horizontal de un rectángulo con el fin de que se mueva a 4 posiciones fijas a lo largo de su carril horizontal:
@ExperimentalMaterialApi
@Composable
fun SwipeableExample() {
val width = 200.dp
val baseAnchor = 50.dp
val swipeableState = rememberSwipeableState(initialValue = "Fácil")
val sizePx = with(LocalDensity.current) { baseAnchor.toPx() }
val anchors =
mapOf(
0f to "Fácil",
sizePx to "Normal",
sizePx * 2 to "Difícil",
sizePx * 3 to "Demente"
)
Box(
modifier = Modifier
.width(width)
.swipeable(
swipeableState,
anchors = anchors,
orientation = Orientation.Horizontal,
thresholds = { _, _ -> FractionalThreshold(0.5f) }
)
.background(Color.LightGray)
) {
Box(
modifier = Modifier
.offset { IntOffset(swipeableState.offset.value.toInt(), 0) }
.size(baseAnchor)
.background(Color.Cyan))
}
}
En el código anterior:
- Declaramos el ancho de la pista que recorrerá la caja
- Definiremos cada límite de aterrizaje en 50dp
- Establecemos el estado inicial en el primer indicador
"Fácil"
- Convertimos el espacio de cada límite a píxeles en
sizePx
- Creamos las anclas usando múltiplos de
sizePx
- Habilitamos en la caja externa la detección de gestos con swipeable() y los parámetros creados previamente. Usamos la clase
FractionalThreshold
a fin de especificar la fracción la distancia mínima entre anclas, para permitir el aterrizaje - Aplicamos el modificador
offset()
en la caja interna para desplazarla a partir del valor que proporcionaswipeableState.offset
El resultado del gesto de swipe creará el siguiente carril con 4 anclas:
Manejar Gestos De Transformación
Por otro lado, Compose nos permite detectar gestos multitouch que permiten transformar el tamaño, posición y rotación de un elemento en pantalla.
Para ello hacemos uso del modificador transformable()
, el cual permite actualizar el estado de UI a partir de un parámetro TransformableState
.
Modifier.transformable(
state: TransformableState,
lockRotationOnZoomPan: Boolean,
enabled: Boolean
)
Manejamos el estado con la función rememberTransformableState()
. Esta recibe una lambda (onTransformation
) con tres parámetros que indican el zoom, offset y la rotación percibidos por el gesto.
@Composable
fun rememberTransformableState(
onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
)
Por ejemplo:
Permitamos al usuario rotar un rectángulo 360 grados en la dirección que desee con el uso de dos dedos en la superficie de la pantalla:
@Composable
fun RotationExample() {
var rotation by remember {
mutableStateOf(0f)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.rotate(rotation)
.transformable(
state = rememberTransformableState { _, _, degrees ->
rotation += degrees
})
.size(150.dp, 300.dp)
.background(Color.Red)
)
}
}
Debido a que no usamos los dos primeros parámetros de onTransformation
, los ignoramos con _
. El valor de la rotación es recordado en la variable rotation
, por lo que incrementamos el valor para asignarlo en el modificador rotate()
.
Al ejecutar la App, sostén la tecla Ctrl en el emulador de Android Studio para desplegar el indicador de multitouch. Y luego presiona el clic para rotar el contenido:
Ú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!