Consentire agli utenti di trascinare fisicamente i componenti su uno schermo è fondamentale per le app per Android moderne. Che tu stia creando un cursore 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 astraggono la complessità degli eventi touch non elaborati.
Anziché calcolare manualmente le coordinate touch 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.
Questa pagina descrive come e quando utilizzare ogni API di trascinamento.
Scegliere l'API di trascinamento corretta
Esistono diverse API che puoi utilizzare per implementare 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, eliminare un'email, archiviare un'attività o aprire un menu laterale / riquadri inferiori). 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, trascinare un cursore personalizzato). Per ulteriori informazioni, vedi Trascinare lungo un singolo asse con draggable.- Scegli
Modifier.draggable2Dquando: hai bisogno di un componente che si sposti continuamente contemporaneamente lungo gli assi X e Y (ad esempio, eseguire la panoramica di una mappa o creare un Floating Action Button (FAB) che l' utente può trascinare ovunque sullo schermo). Per ulteriori informazioni, vedi Trascinare in due dimensioni conModifier.draggable2D.
- Scegli
- Scegli
Modifier.anchoredDraggablequando: stai creando componenti interattivi personalizzati che devono agganciarsi tra due o più posizioni discrete (ad esempio, un riquadro inferiore, un riquadro di navigazione a scomparsa o un'azione di scorrimento per rivelare). Per ulteriori informazioni, vedi Trascinare i componenti tra i punti di ancoraggio conanchoredDraggable.
Trascinare lungo un singolo asse con draggable
Il modificatore draggable è il punto di ingresso di alto livello per i gesti di trascinamento in
un singolo orientamento 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 offset modificatore:
@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 pointerInput modificatore.
@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 } } ) } }
Trascinare i componenti tra i punti di ancoraggio con anchoredDraggable
Il modificatore anchoredDraggable 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 a scomparsa.
Piattaforma API
L'API AnchoredDraggable è composta dalle seguenti parti principali:
- Ancoraggi (
DraggableAnchors): un generatore che definisce la mappatura tra gli stati personalizzati (ad es. una classe enum perStart, Center, End) e i relativi valori di offset in pixel. - Stato (
AnchoredDraggableState<T>): un contenitore di stato che gestisce la posizione corrente (offset) del componente, l'ancoraggio su cui è posizionato e l'avanzamento del trascinamento. Deve essere creato utilizzandorememberSaveableeAnchoredDraggableState.Saver<T>per conservare il suo stato tra le modifiche di configurazione changes. - Modificatore di 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 rapido per animare il componente all'ancoraggio più vicino al rilascio.
Esempio: creare 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'enumerazione che rappresenti le posizioni discrete in cui il contenuto trascinabile può essere posizionato:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Crea un'istanza e salva
AnchoredDraggableState. UtilizzarememberSaveableconAnchoredDraggableState.Saver()per rendere persistente la posizione corrente del componente tra la ricreazione dell'attività e le modifiche di configurazione (ad es. rotazione dello schermo o modifiche del 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 relativi 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, calcolare dinamicamente la mappaDraggableAnchorsin base alla sua larghezza e aggiornare 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 all'ancoraggio più vicino al rilascio. Aggiungi questo modificatore al componente che deve rispondere ai gesti di trascinamento e passagli ilstatee ilorientationdi trascinamento (e, facoltativamente,overscrollEffect).Modifier.offset: legge l'offset dastate.requireOffset()all'interno della lambda di offset. La lettura dell'offset all'interno di questa 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 container scorrevole sono stati raggiunti con uno scorrimento o uno scorrimento 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 una luce).- Invia: passa un'istanza
overscrollEffectalanchoredDraggablemodificatore. 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 di ancoraggio. - Esegui il rendering: applica
Modifier.overscrollcon la stessaOverscrollEffectistanza al componente che deve eseguire il rendering visivo dell' effetto.
- Invia: 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!" (Trascinami per chiudere) che l'utente può scorrere verso sinistra per chiudere (o trascinare verso destra per visualizzare l'effetto di allungamento dell'overscroll):
Esempio: creare un riquadro di navigazione a scomparsa personalizzato basato su gesti
Questo esempio mostra come creare un riquadro di navigazione personalizzato basato su gesti. Quando l'utente trascina orizzontalmente, la schermata in primo piano (ScreenContents) scorre verso destra, si riduce da 1.0f a 0.8f e arrotonda gli angoli per rivelare il riquadro di navigazione sottostante.
Definisci gli stati e le posizioni di ancoraggio. In questo esempio, gli ancoraggi sono gli stati di apertura e chiusura del riquadro. Definisci un'enumerazione per gli stati
OpeneClosede mappali agli offset in 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 di un elemento. PosizionaHomeScreenDrawernella parte inferiore diBoxeScreenContentsin alto. Il riquadro rimane fermo, mentre il livello superiore si sposta e si ridimensiona sopra di esso.Quando un utente seleziona un elemento del riquadro, aggiorna lo stato della schermata attiva e chiudi automaticamente il riquadro 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é solo spostare il contenuto, puoi modificare più proprietà conModifier.graphicsLayerper animare contemporaneamente la traslazione, la scalabilità 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 scalabilità proporzionale (1fa0.8f) e il raggio degli angoli (0.dpa32.dp) rispetto all'offset e alla larghezza del riquadro. 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 in modo programmatico (ad esempio quando tocchi l'icona del menu o selezioni un elemento del riquadro) avviandostate.animateTo()in 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
Trascinare in due dimensioni con Modifier.draggable2D
Utilizza il modificatore draggable2D per spostare i singoli elementi UI contemporaneamente lungo gli assi X e Y.
Trascinare un elemento componibile
Questo esempio mostra il caso d'uso più comune di draggable2D: consentire a un utente di selezionare un elemento UI e riposizionarlo ovunque all'interno di un container 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:
- Traccia 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 lo scorrimento rapido, la casella smette di muoversi non appena l'utente solleva il dito.
Trascinare un elemento componibile figlio in base all'area di trascinamento del componente principale
Questo esempio mostra come utilizzare draggable2D per creare un'area di input 2D in cui una manopola di selezione è vincolata a una superficie specifica. A differenza dell'esempio di elemento trascinabile, che sposta il componente stesso, questa implementazione utilizza i delta 2D per spostare un elemento componibile figlio "selettore" su un selettore di 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 container 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