Elementi componibili di trascinamento

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 SwipeToDismissBox o 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.draggable quando: 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.draggable2D quando: 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 con Modifier.draggable2D.
  • Scegli Modifier.anchoredDraggable quando: 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 con anchoredDraggable.

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
                    }
                }
        )
    }
}

INSERT ALT TEXT HERE
Figura 1. Un elemento UI trascinato da un dito

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 per Start, 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 utilizzando rememberSaveable e AnchoredDraggableState.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 un Orientation, 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.

  1. 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 }

  2. Crea un'istanza e salva AnchoredDraggableState. Utilizza rememberSaveable con AnchoredDraggableState.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)
        }

  3. Applica i modificatori layout, offset e draggable. 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 modificatore layout per misurare il componente, calcola la mappa DraggableAnchors in modo dinamico in base alla sua larghezza e aggiorna lo stato utilizzando state.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 passagli state e il trascinamento orientation (e, facoltativamente, overscrollEffect).
    • Modifier.offset: legge l'offset da state.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()
    }

  4. [Facoltativo] Integra l'overscroll. L'API overscrollEffect implementa 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).

    1. Invio: passa un'istanza overscrollEffect al modificatore anchoredDraggable. In questo modo, i delta di trascinamento vengono inviati allo stato AnchoredDraggable o all'effetto di overscroll, a seconda che il gesto si trovi all'interno o all'esterno dei limiti dell'ancoraggio.
    2. Rendering: applica Modifier.overscroll con la stessa istanza OverscrollEffect al componente che deve eseguire il rendering visivo dell'effetto.

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):

Video 2. Un'implementazione personalizzata del componente di scorrimento per chiudere.

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.

  1. 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 Open e Closed e mappali agli offset dei pixel utilizzando DraggableAnchors:

    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,
            )
        }

  2. Anima anchoredDraggableState al clic su un elemento. Posiziona HomeScreenDrawer nella parte inferiore di Box e ScreenContents nella 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(...)
        }
    }

  3. Utilizza state.requireOffset() per modificare più proprietà visive. Anziché limitarti a compensare i contenuti, puoi modificare più proprietà con Modifier.graphicsLayer per 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 (da 1f a 0.8f) e il raggio dell'angolo (da 0.dp a 32.dp) in relazione all'offset e alla larghezza del carrello.
    • Modifier.graphicsLayer esegue 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)
        )
    }

  4. 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) avviando state.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

Video 3. La schermata in primo piano (Sonno) viene trascinata verso destra, mostrando il riquadro di navigazione a scomparsa sottostante.

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)
        }
    }
}

Video 4. Un piccolo riquadro viola viene riposizionato su uno sfondo grigio, mostrando il trascinamento 2D diretto in cui l'elemento smette di muoversi nell'istante in cui l'utente solleva il dito.

Lo snippet di codice precedente include quanto segue:

  • Monitora la posizione della casella utilizzando uno stato offset.
  • Utilizza il modificatore offset per 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)
                    }
                )
        )
    }
}

Video 5. Una mappa a gradiente di colore con una manopola di selezione circolare bianca che può essere trascinata in qualsiasi direzione, mostrando come i delta 2D vengono bloccati ai limiti del contenitore per aggiornare i valori dei colori selezionati.

Lo snippet precedente include quanto segue:

  • Utilizza il modificatore onSizeChanged per acquisire le dimensioni effettive del contenitore del gradiente. Il selettore sa esattamente dove si trovano i bordi.
  • All'interno di graphicsLayer, regola translationX e translationY per assicurarsi che il selettore rimanga centrato durante il trascinamento.