Consentire agli utenti di trascinare fisicamente i componenti su uno schermo è fondamentale per le app per Android moderne. Che tu stia creando uno slider personalizzato, un componente di scorrimento per chiudere o un canvas interattivo, Jetpack Compose fornisce un set completo di API per gestire facilmente gli eventi di trascinamento.
Le API di trascinamento di Compose eliminano la complessità degli eventi touch non elaborati.
Anziché calcolare manualmente le coordinate di tocco e gestire i cicli di vita dei gesti con pointerInput, puoi utilizzare i modificatori integrati che si collegano direttamente ai sistemi di gestione dello stato e di animazione di Compose.
In questa pagina viene descritto come e quando utilizzare ogni API di trascinamento.
Scegli l'API di trascinamento corretta
Esistono diverse API che puoi utilizzare per implementare i contenuti trascinabili.
- Scegli i componenti Material standard (come
SwipeToDismissBoxo i riquadri di navigazione a scomparsa) quando: hai bisogno del comportamento di scorrimento o trascinamento predefinito di Material 3 (ad esempio, eliminazione di un'email, archiviazione di un'attività o apertura di un menu laterale / riquadro inferiore). Forniscono comportamenti integrati senza richiedere la gestione personalizzata dello stato o del layout. - Scegli
Modifier.draggablequando: hai bisogno solo di delta di trascinamento non elaborati (ad esempio, trascinamento di un cursore personalizzato). Per saperne di più, consulta Trascina lungo un singolo asse con l'elemento trascinabile.- Scegli
Modifier.draggable2Dquando: hai bisogno di un componente per spostarti continuamente lungo gli assi X e Y contemporaneamente (ad esempio, per eseguire la panoramica di una mappa o creare un Floating Action Button che l'utente può trascinare in qualsiasi punto dello schermo). Per saperne di più, consulta Trascina in due dimensioni conModifier.draggable2D.
- Scegli
- Scegli
Modifier.anchoredDraggablequando: stai creando componenti interattivi personalizzati che devono agganciarsi tra due o più posizioni distinte (ad esempio, un riquadro inferiore, un riquadro di navigazione a scomparsa o un'azione di scorrimento per mostrare). Per saperne di più, consulta Trascina i componenti tra i punti di ancoraggio conanchoredDraggable.
Trascina lungo un singolo asse con draggable
Il modificatore draggable è l'entry point di alto livello per i gesti di trascinamento in un'unica direzione e segnala la distanza di trascinamento in pixel.
È importante notare che questo modificatore è simile a scrollable, in quanto rileva solo il gesto. Devi mantenere lo stato e rappresentarlo sullo schermo, ad esempio spostando l'elemento tramite il modificatore 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!" ) }
Se devi controllare l'intero gesto di trascinamento, valuta la possibilità di utilizzare il rilevatore di gesti di trascinamento tramite il modificatore pointerInput.
@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 } } ) } }
Trascina i componenti tra i punti di ancoraggio con anchoredDraggable
Il modificatore anchoredDraggable ti consente di trascinare un componente tra un insieme di posizioni predefinite chiamate punti di ancoraggio. Quando l'utente rilascia il gesto di trascinamento, il componente si posiziona sull'ancoraggio più vicino. anchoredDraggable è utile per creare componenti come riquadri inferiori personalizzati, cursori complessi o riquadri di riquadri a scomparsa.
Piattaforma API
L'API AnchoredDraggable è costituita dalle seguenti parti principali:
- Ancoraggi (
DraggableAnchors): un builder che definisce la mappatura tra i tuoi stati personalizzati (ad es. una classe enum perStart, Center, End) e i relativi valori di offset dei pixel corrispondenti. - Stato (
AnchoredDraggableState<T>): un contenitore di stato che gestisce la posizione corrente del componente (offset), l'ancoraggio in cui è posizionato e l'avanzamento del trascinamento. Deve essere creato utilizzandorememberSaveableeAnchoredDraggableState.Saver<T>per preservare lo stato tra le modifiche alla configurazione. - Modificatore dei gesti (
Modifier.anchoredDraggable): un modificatore che rileva i gesti di trascinamento lungo unOrientation, aggiorna lo stato con il delta di trascinamento e gestisce il gesto di scorrimento per animare il componente fino all'ancoraggio più vicino al rilascio.
Esempio: crea un componente SwipeToDismiss personalizzato
Questo esempio mostra come creare un componente SwipeToDismiss personalizzato (un banner bianco) che l'utente può scorrere verso sinistra per chiudere o trascinare verso destra per visualizzare un effetto di allungamento dell'overscroll.
Definisci i valori di ancoraggio. Definisci un enum che rappresenti le posizioni discrete in cui i contenuti trascinabili possono essere posizionati:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Crea un'istanza e salva
AnchoredDraggableState. UtilizzarememberSaveableconAnchoredDraggableState.Saver()per rendere persistente la posizione corrente del componente durante la ricreazione dell'attività e le modifiche alla configurazione (ad es. rotazione dello schermo o modifiche al tema):val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
Applica i modificatori
layout,offsetedraggable. Per rendere il componente trascinabile e assicurarti che i suoi punti di ancoraggio si adattino alle sue dimensioni, applica i seguenti modificatori ai contenuti che devono spostarsi visivamente:Modifier.layout: le posizioni di ancoraggio spesso dipendono dalle dimensioni di un componente, che non sono note durante la composizione iniziale. Utilizza il modificatorelayoutper misurare il componente, calcola la mappaDraggableAnchorsin modo dinamico in base alla sua larghezza e aggiorna lo stato utilizzandostate.updateAnchors().
Modifier.anchoredDraggable: si connette allo stato, invia i delta di trascinamento nell'orientamento specificato e gestisce il gesto di scorrimento rapido per animare il componente fino all'ancoraggio più vicino quando viene rilasciato. Aggiungi questo modificatore al componente che deve rispondere ai gesti di trascinamento e passaglistatee il trascinamentoorientation(e, facoltativamente,overscrollEffect).Modifier.offset: legge l'offset dastate.requireOffset()all'interno della funzione lambda dell'offset. La lettura dell'offset all'interno di questa funzione lambda garantisce che il valore venga letto solo durante la fase di posizionamento, evitando ricomposizioni non necessarie durante il trascinamento. Applica questo modificatore ai contenuti che devono spostarsi visivamente.
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() }
[Facoltativo] Integra l'overscroll. L'API
overscrollEffectimplementa un effetto visivo che viene visualizzato quando i bordi di un contenitore scorrevole vengono raggiunti con uno scorrimento o un movimento rapido. Puoi fornire un feedback visivo quando un utente trascina oltre i limiti di ancoraggio definiti con un effetto di overscroll (in genere un allungamento o un bagliore).- Invio: passa un'istanza
overscrollEffectal modificatoreanchoredDraggable. In questo modo, i delta di trascinamento vengono inviati allo statoAnchoredDraggableo all'effetto di overscroll, a seconda che il gesto si trovi all'interno o all'esterno dei limiti dell'ancoraggio. - Rendering: applica
Modifier.overscrollcon la stessa istanzaOverscrollEffectal componente che deve eseguire il rendering visivo dell'effetto.
- Invio: passa un'istanza
Il seguente snippet mostra come aggiungere un effetto di allungamento standard:
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() } }
Risultato
Lo snippet di codice precedente genera un banner bianco con il testo "Drag me to dismiss!" (Trascina per chiudere), che l'utente può scorrere verso sinistra per chiudere (o trascinare verso destra per visualizzare l'effetto di allungamento dell'overscroll):
Esempio: crea un riquadro di navigazione a scomparsa personalizzato basato sui gesti
Questo esempio mostra come creare un riquadro di navigazione a scomparsa personalizzato basato sui gesti. Mentre l'utente trascina in orizzontale, la schermata in primo piano (ScreenContents)
scorre verso destra, viene ridotta da 1.0f a 0.8f e i suoi angoli vengono arrotondati
per mostrare il riquadro di navigazione a scomparsa sottostante.
Definisci gli stati e le posizioni degli ancoraggi. In questo esempio, gli ancoraggi sono gli stati di apertura e chiusura del riquadro a scomparsa. Definisci un enum per gli stati
OpeneClosede mappali agli offset dei pixel utilizzandoDraggableAnchors: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
anchoredDraggableStateal clic su un elemento. PosizionaHomeScreenDrawernella parte inferiore diBoxeScreenContentsnella parte superiore. Il riquadro a scomparsa rimane fermo, mentre il livello superiore si sposta e si ridimensiona sopra di esso.Quando un utente seleziona un elemento del carrello, aggiorna lo stato della schermata attiva e chiudi automaticamente il carrello utilizzando
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(...) } }
Utilizza
state.requireOffset()per modificare più proprietà visive. Anziché limitarti a compensare i contenuti, puoi modificare più proprietà conModifier.graphicsLayerper animare contemporaneamente la traslazione, la scala e il raggio degli angoli in base all'avanzamento del trascinamento:- Utilizza
state.requireOffset()per ottenere l'offset di trascinamento corrente. - Utilizza
lerp()(interpolazione lineare) per calcolare la scala proporzionale (da1fa0.8f) e il raggio dell'angolo (da0.dpa32.dp) in relazione all'offset e alla larghezza del carrello. Modifier.graphicsLayeresegue le modifiche nella fase di disegno, garantendo animazioni ad alte prestazioni senza attivare ricomposizioni durante il trascinamento.
@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) ) }
- Utilizza
Attiva le animazioni programmatiche con
animateTo. Controlla il riquadro a scomparsa in modo programmatico (ad esempio quando tocchi l'icona del menu o selezioni un elemento del riquadro a scomparsa) avviandostate.animateTo()all'interno di un ambito di coroutine:fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
Risultato
Trascina in due dimensioni con Modifier.draggable2D
Utilizza il modificatore draggable2D per spostare singoli elementi UI contemporaneamente lungo gli assi X e Y.
Trascina un elemento componibile
Questo esempio mostra il caso d'uso più comune di draggable2D, ovvero consentire a un utente di selezionare un elemento UI e riposizionarlo in un punto qualsiasi all'interno di un contenitore principale.
@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) } } }
Lo snippet di codice precedente include quanto segue:
- Monitora la posizione della casella utilizzando uno stato
offset. - Utilizza il modificatore
offsetper spostare la posizione del componente in base ai delta di trascinamento. - Poiché non è supportato il fling, la casella smette di muoversi nel momento in cui l'utente solleva il dito.
Trascina un composable secondario in base all'area di trascinamento del composable principale
Questo esempio mostra come utilizzare draggable2D per creare un'area di input 2D in cui una manopola di selezione è vincolata all'interno di una superficie specifica. A differenza dell'esempio di elemento trascinabile, che sposta il componente stesso, questa implementazione utilizza i delta 2D per spostare un "selettore" componibile secondario su un selettore colori:
@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) } ) ) } }
Lo snippet precedente include quanto segue:
- Utilizza il modificatore
onSizeChangedper acquisire le dimensioni effettive del contenitore del gradiente. Il selettore sa esattamente dove si trovano i bordi. - All'interno di
graphicsLayer, regolatranslationXetranslationYper assicurarsi che il selettore rimanga centrato durante il trascinamento.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Informazioni sui gesti
- Esempio di animazione avanzata: gesti {:#gesture-and-animation}
- Animazioni basate sul valore