Permitir que los usuarios arrastren físicamente componentes por una pantalla es fundamental para las apps modernas para Android. Ya sea que compiles un control deslizante personalizado, un componente de deslizar para descartar o un lienzo interactivo, Jetpack Compose proporciona un conjunto integral de APIs para controlar fácilmente los eventos de arrastre.
Las APIs de arrastre de Compose abstraen la complejidad de los eventos táctiles sin procesar.
En lugar de calcular manualmente las coordenadas táctiles y administrar los ciclos de vida de los gestos con pointerInput, puedes usar modificadores integrados que se vinculan directamente a los sistemas de administración de estado y animación de Compose.
En esta página, se describe cómo y cuándo usar cada API de arrastre.
Elige la API de arrastre correcta
Puedes usar varias APIs para implementar contenido que se pueda arrastrar.
- Elige componentes estándar de Material (como
SwipeToDismissBoxo paneles laterales de navegación) cuando: Necesitas el comportamiento predeterminado de deslizar o arrastrar de Material 3 (por ejemplo, borrar un correo electrónico, archivar una tarea o abrir un menú lateral o hojas inferiores). Proporcionan comportamientos integrados sin requerir un estado personalizado ni administración de diseño. - Elige
Modifier.draggablecuando: Solo necesitas deltas de arrastre sin procesar (por ejemplo, arrastrar un control deslizante personalizado). Para obtener más información, consulta Arrastra a lo largo de un solo eje con draggable.- Elige
Modifier.draggable2Dcuando: Necesitas un componente para que se mueva de forma continua a través de los ejes X y Y de forma simultánea (por ejemplo, desplazar un mapa o crear un botón de acción flotante que el usuario pueda arrastrar a cualquier lugar de la pantalla). Para obtener más información, consulta Arrastra en dos dimensiones conModifier.draggable2D.
- Elige
- Elige
Modifier.anchoredDraggablecuando: Estás compilando componentes interactivos personalizados que deben ajustarse entre dos o más posiciones discretas (por ejemplo, una hoja inferior, un panel lateral de navegación o una acción de deslizar para revelar). Para obtener más información, consulta Arrastra componentes entre puntos de anclaje conanchoredDraggable.
Arrastra a lo largo de un solo eje con draggable
El modificador draggable es el punto de entrada de alto nivel para arrastrar gestos en
una sola orientación y muestra la distancia de arrastre en píxeles.
Es importante tener en cuenta que este modificador es similar a scrollable, ya que solo detecta el gesto. Debes mantener el estado y representarlo en la pantalla
por ejemplo, moviendo el elemento a través del modificador offset:
@Composable private fun DraggableText() { var offsetX by remember { mutableFloatStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" ) }
Si necesitas controlar todo el gesto de arrastre, considera usar el detector de gestos de arrastre
en su lugar, a través del pointerInput modificador.
@Composable private fun DraggableTextLowLevel() { Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() offsetX += dragAmount.x offsetY += dragAmount.y } } ) } }
Arrastra componentes entre puntos de anclaje con anchoredDraggable
El modificador anchoredDraggable te permite arrastrar un componente entre un conjunto de
posiciones predefinidas llamadas puntos de anclaje. Cuando el usuario suelta el gesto de arrastre, el componente se asienta en el anclaje más cercano. anchoredDraggable es útil para crear componentes como hojas inferiores personalizadas, controles deslizantes complejos o paneles de cajones.
Superficie de la API
La API de AnchoredDraggable consta de las siguientes partes principales:
- Anclajes (
DraggableAnchors): Es un compilador que define la asignación entre tus estados personalizados (p.ej., una clase enum paraStart, Center, End) y sus valores de desplazamiento de píxeles correspondientes. - Estado (
AnchoredDraggableState<T>): Es un contenedor de estado que administra la posición actual (desplazamiento) del componente, el anclaje en el que se asienta y su progreso de arrastre. Se debe crear conrememberSaveableyAnchoredDraggableState.Saver<T>para conservar su estado en los cambios de configuración - Modificador de gestos (
Modifier.anchoredDraggable): Es un modificador que detecta gestos de arrastre a lo largo de unaOrientation, actualiza el estado con el delta de arrastre y controla el gesto de lanzamiento para animar el componente al anclaje más cercano cuando se suelta.
Ejemplo: Compila un componente SwipeToDismiss personalizado
En este ejemplo, se muestra cómo compilar un componente SwipeToDismiss personalizado (un banner blanco) que el usuario puede deslizar hacia la izquierda para descartar o arrastrar hacia la derecha para ver un efecto de sobredesplazamiento.
Define valores de anclaje. Define un enum que represente las posiciones discretas en las que se puede asentar el contenido que se puede arrastrar:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Crea instancias y guarda
AnchoredDraggableState. UsarememberSaveableconAnchoredDraggableState.Saver()para conservar la posición actual del componente en la recreación de la actividad y los cambios de configuración (p.ej., rotación de pantalla o cambios de tema):val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
Aplica los modificadores
layout,offsetydraggable. Para que el componente se pueda arrastrar y garantizar que sus puntos de anclaje se adapten a su tamaño, aplica los siguientes modificadores al contenido que debe moverse visualmente:Modifier.layout: Las posiciones de anclaje suelen depender del tamaño de un componente, que no se conoce durante la composición inicial. Usa el modificadorlayoutpara medir el componente, calcular el mapaDraggableAnchorsde forma dinámica según su ancho y actualizar el estado constate.updateAnchors().
Modifier.anchoredDraggable: Se conecta al estado, envía deltas de arrastre en la orientación especificada y controla el gesto de lanzamiento para animar el componente al anclaje más cercano cuando se suelta. Agrega este modificador al componente que debe responder a los gestos de arrastre y pásale elstatey laorientationde arrastre (y, de manera opcional,overscrollEffect).Modifier.offset: Lee el desplazamiento destate.requireOffset()dentro de la lambda de desplazamiento. Leer el desplazamiento dentro de esta lambda garantiza que el valor solo se lea durante la fase de ubicación, lo que evita recomposiciones innecesarias durante el arrastre. Aplica este modificador al contenido que debe moverse visualmente.
Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() }
[Opcional] Integra el sobredesplazamiento. La API de
overscrollEffectimplementa un efecto visual que se muestra cuando se alcanzan los bordes de un contenedor de desplazamiento con un desplazamiento o un lanzamiento. Puedes proporcionar comentarios visuales cuando un usuario arrastra más allá de los límites de anclaje definidos con un efecto de sobredesplazamiento (por lo general, un estiramiento o un brillo).- Envío: Pasa una
overscrollEffectinstancia aanchoredDraggablemodificador. Esto envía deltas de arrastre al estadoAnchoredDraggableo al efecto de sobredesplazamiento, según si el gesto está dentro o fuera de los límites de anclaje. - Renderización: Aplica el
Modifier.overscrollcon la mismaOverscrollEffectinstancia al componente que debe renderizar visualmente el efecto.
- Envío: Pasa una
En el siguiente fragmento, se muestra cómo agregar un efecto de estiramiento estándar:
val overscrollEffect = rememberOverscrollEffect() Box( modifier.fillMaxWidth() ) { Box(Modifier.fillMaxWidth()) { background() } Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() } }
Resultado
El fragmento de código anterior da como resultado un banner blanco, con el texto "¡Arrastra para descartar!", que el usuario puede deslizar hacia la izquierda para descartar (o arrastrar hacia la derecha para ver el efecto de estiramiento de desplazamiento excesivo):
Ejemplo: Crea un panel lateral de navegación personalizado controlado por gestos
En este ejemplo, se muestra cómo compilar un panel de navegación personalizado controlado por gestos. A medida que el usuario arrastra horizontalmente, la pantalla de primer plano (ScreenContents) se desliza hacia la derecha, se reduce de 1.0f a 0.8f y redondea sus esquinas para revelar el panel de navegación subyacente.
Define estados y posiciones de anclaje. En este ejemplo, los anclajes son los estados abiertos y cerrados del panel. Define un enum para los estados
OpenyClosed, y asígnalos a desplazamientos de píxeles conDraggableAnchors:enum class DrawerState { Open, Closed } val anchors = DraggableAnchors { DrawerState.Open at drawerWidth DrawerState.Closed at 0f } val state = remember { AnchoredDraggableState( initialValue = DrawerState.Closed, anchors = anchors, ) }
Anima
anchoredDraggableStatecuando se hace clic en un elemento. ColocaHomeScreenDraweren la parte inferior deBoxyScreenContentsen la parte superior. El panel permanece inmóvil, mientras que la capa superior se traduce y se escala sobre él.Cuando un usuario selecciona un elemento del panel, actualiza el estado de la pantalla activa y descarta automáticamente el panel con
state.animateTo(DrawerState.Closed):@Composable private fun AnchoredDraggableDrawerOnClickSample( modifier: Modifier = Modifier, state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { var screenState by remember { mutableStateOf(ScreenState.Home) } Box(modifier = modifier.fillMaxSize()) { HomeScreenDrawer( selectedScreen = screenState, onScreenSelected = { screen -> screenState = screen // Dismiss drawer when a destination is selected coroutineScope.launch { state.animateTo(DrawerState.Closed) } } ) // ScreenContents(...) } }
Usa
state.requireOffset()para cambiar varias propiedades visuales. En lugar de solo desplazar el contenido, puedes cambiar varias propiedades conModifier.graphicsLayerpara animar simultáneamente la traducción, la escala y el radio de la esquina según el progreso de arrastre:- Usa
state.requireOffset()para obtener el desplazamiento de arrastre actual. - Usa
lerp()(interpolación lineal) para calcular la escala proporcional (1fa0.8f) y el radio de la esquina (0.dpa32.dp) en relación con el desplazamiento y el ancho del panel. Modifier.graphicsLayerejecuta cambios en la fase de dibujo, lo que garantiza animaciones de alto rendimiento sin activar recomposiciones durante el arrastre.
@Composable private fun AnchoredDraggableDrawerGraphicsLayerSample( state: AnchoredDraggableState<DrawerState>, drawerWidth: Float, selectedScreen: ScreenState, onDrawerClicked: () -> Unit ) { ScreenContents( selectedScreen = selectedScreen, onDrawerClicked = onDrawerClicked, modifier = Modifier .graphicsLayer { this.translationX = state.requireOffset() val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth) this.scaleX = scale this.scaleY = scale val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth) this.shape = RoundedCornerShape(cornerRadius.dp) this.clip = true } .anchoredDraggable(state, Orientation.Horizontal) ) }
- Usa
Activa animaciones programáticas con
animateTo. Controla el panel de forma programática (por ejemplo, cuando presionas el ícono de menú o seleccionas un elemento del panel) iniciandostate.animateTo()dentro de un alcance de corrutina:fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
Resultado
Arrastra en dos dimensiones con Modifier.draggable2D
Usa el modificador draggable2D para mover elementos individuales de la IU a través de los ejes X y Y de forma simultánea.
Arrastra un elemento componible
En este ejemplo, se muestra el caso de uso más común para draggable2D, que permite que un usuario tome un elemento de la IU y lo cambie de posición en cualquier lugar dentro de un contenedor superior.
@Composable private fun DraggableComposableElement() { // 1. Track the position of the floating window var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) { Box( modifier = Modifier // 2. Apply the offset to the box's position .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } // ... // 3. Attach the 2D drag logic .draggable2D( state = rememberDraggable2DState { delta -> // 4. Update the position based on the movement delta offset += delta } ), contentAlignment = Alignment.Center ) { Text("Video Preview", color = Color.White, fontSize = 12.sp) } } }
El fragmento de código anterior incluye lo siguiente:
- Realiza un seguimiento de la posición de la caja con un estado
offset. - Usa el modificador
offsetpara cambiar la posición del componente según los deltas de arrastre. - Como no hay compatibilidad con el lanzamiento, la caja deja de moverse en el instante en que el usuario levanta el dedo.
Arrastra un elemento secundario componible según el área de arrastre del elemento superior
En este ejemplo, se muestra cómo usar draggable2D para crear un área de entrada 2D en la que una perilla de selector está restringida dentro de una superficie específica. A diferencia del ejemplo de elemento que se puede arrastrar, que mueve el componente en sí, esta implementación usa los deltas 2D para mover un "selector" componible secundario a través de un selector de color:
@Composable private fun ExampleColorSelector( // ... ) { // 1. Maintain the 2D position of the selector in state. var selectorOffset by remember { mutableStateOf(Offset.Zero) } // 2. Track the size of the background container. var containerSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .size(300.dp, 200.dp) // Capture the actual pixel dimensions of the container when it's laid out. .onSizeChanged { containerSize = it } .clip(RoundedCornerShape(12.dp)) .background( brush = remember(hue) { // Create a simple gradient representing Saturation and Value for the given Hue. Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))) } ) ) { Box( modifier = Modifier .size(24.dp) .graphicsLayer { // Center the selector on the finger by subtracting half its size. translationX = selectorOffset.x - (24.dp.toPx() / 2) translationY = selectorOffset.y - (24.dp.toPx() / 2) } // ... // 3. Configure 2D touch dragging. .draggable2D( state = rememberDraggable2DState { delta -> // 4. Calculate the new position and clamp it to the container bounds val newX = (selectorOffset.x + delta.x) .coerceIn(0f, containerSize.width.toFloat()) val newY = (selectorOffset.y + delta.y) .coerceIn(0f, containerSize.height.toFloat()) selectorOffset = Offset(newX, newY) } ) ) } }
El fragmento anterior incluye lo siguiente:
- Usa el modificador
onSizeChangedpara capturar las dimensiones reales del contenedor de gradiente. El selector sabe exactamente dónde están los bordes. - Dentro de
graphicsLayer, ajustatranslationXytranslationYpara asegurarse de que el selector permanezca centrado mientras se arrastra.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Información sobre los gestos
- Ejemplo de animación avanzada: Gestos {:#gesture-and-animation}
- Animaciones basadas en el valor