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

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

INSERISCI IL TESTO ALTERNATIVO QUI
Figura 1. Un elemento UI trascinato da un dito

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

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

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

  3. Applica i modificatori layout, offset e draggable. 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 modificatore layout per misurare il componente, calcolare dinamicamente la mappa DraggableAnchors in base alla sua larghezza e aggiornare 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 all'ancoraggio più vicino al rilascio. Aggiungi questo modificatore al componente che deve rispondere ai gesti di trascinamento e passagli il state e il orientation di trascinamento (e, facoltativamente, overscrollEffect).
    • Modifier.offset: legge l'offset da state.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()
    }

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

    1. Invia: passa un'istanza overscrollEffect al anchoredDraggable modificatore. 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 di ancoraggio.
    2. Esegui il rendering: applica Modifier.overscroll con la stessa OverscrollEffect istanza 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!" (Trascinami 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: 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.

  1. 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 Open e Closed e mappali agli offset in 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 di un elemento. Posiziona HomeScreenDrawer nella parte inferiore di Box e ScreenContents in 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(...)
        }
    }

  3. Utilizza state.requireOffset() per modificare più proprietà visive. Anziché solo spostare il contenuto, puoi modificare più proprietà con Modifier.graphicsLayer per 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 (1f a 0.8f) e il raggio degli angoli (0.dp a 32.dp) rispetto all'offset e alla larghezza del riquadro.
    • 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 in modo programmatico (ad esempio quando tocchi l'icona del menu o selezioni un elemento del riquadro) avviando state.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

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

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

Video 4. Una piccola casella viola viene riposizionata su uno sfondo grigio, mostrando il trascinamento 2D diretto in cui l'elemento smette di muoversi non appena l'utente solleva il dito.

Lo snippet di codice precedente include quanto segue:

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

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

Lo snippet precedente include quanto segue:

  • Utilizza il modificatore onSizeChanged per acquisire le dimensioni effettive del container 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.