Modificatori di scorrimento

I modificatori verticalScroll e horizontalScroll offrono il modo più semplice per consentire all'utente di scorrere un elemento quando i limiti dei suoi contenuti sono maggiori dei vincoli di dimensione massima. Con i modificatori verticalScroll e horizontalScroll non è necessario traslare o compensare i contenuti.

@Composable
private fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

Un semplice elenco verticale che risponde ai gesti di scorrimento
Figura 1. Un semplice elenco verticale che risponde ai gesti di scorrimento.

ScrollState consente di modificare la posizione di scorrimento o di ottenere lo stato corrente. Per crearlo con i parametri predefiniti, utilizza rememberScrollState().

@Composable
private fun ScrollBoxesSmooth() {
    // Smoothly scroll 100px on first composition
    val state = rememberScrollState()
    LaunchedEffect(Unit) { state.animateScrollTo(100) }

    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .padding(horizontal = 8.dp)
            .verticalScroll(state)
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

Modificatore dell'area scorrevole

Il modificatore scrollableArea è un elemento di base fondamentale per la creazione di container scorrevoli personalizzati. Fornisce un'astrazione di livello superiore rispetto al modificatore scrollable, gestendo requisiti comuni come l'interpretazione del delta dei gesti, il ritaglio dei contenuti e gli effetti di overscroll.

Sebbene scrollableArea venga utilizzato per le implementazioni personalizzate, in genere dovresti preferire soluzioni pronte all'uso come verticalScroll, horizontalScroll o componibili come LazyColumn per gli elenchi a scorrimento standard. Questi componenti di livello superiore sono più semplici per i casi d'uso comuni e sono a loro volta creati utilizzando scrollableArea.

Differenza tra i modificatori scrollableArea e scrollable

La differenza principale tra scrollableArea e scrollable risiede nel modo in cui interpretano i gesti di scorrimento dell'utente:

  • scrollable (delta non elaborato): il delta riflette direttamente il movimento fisico dell'input dell'utente (ad es. il trascinamento del puntatore) sullo schermo.
  • scrollableArea (delta orientato ai contenuti): delta viene invertito semanticamente per rappresentare la variazione selezionata nella posizione di scorrimento per far sembrare che i contenuti si muovano con il gesto dell'utente, che di solito è l'opposto del movimento del cursore.

Pensa a questo: scrollable ti dice come si è spostato il puntatore, mentre scrollableArea traduce questo movimento del puntatore in come i contenuti dovrebbero spostarsi all'interno di una tipica visualizzazione scorrevole. Questa inversione è il motivo per cui scrollableArea sembra più naturale quando si implementa un contenitore scorrevole standard.

La tabella seguente riassume i segni delta per gli scenari comuni:

Attivazione dall'utente

delta segnalato a dispatchRawDelta da scrollable

delta segnalato a dispatchRawDelta da scrollableArea*

Il puntatore si sposta VERSO L'ALTO

Negative

Positive

Il puntatore si sposta IN BASSO

Positive

Negative

Il puntatore si sposta a SINISTRA

Negative

Positive (Negative per RTL)

Il puntatore si sposta a DESTRA

Positive

Negativo (positivo per RTL)

(*) Nota sul segno delta di scrollableArea: il segno del delta di scrollableArea non è una semplice inversione. Tiene conto in modo intelligente di:

  1. Orientamento: verticale o orizzontale.
  2. LayoutDirection: LTR o RTL (particolarmente importante per lo scorrimento orizzontale).
  3. Flag reverseScrolling: indica se la direzione di scorrimento è invertita.

Oltre a invertire il delta di scorrimento, scrollableArea ritaglia anche i contenuti in base ai limiti del layout e gestisce il rendering degli effetti di overscroll. Per impostazione predefinita, utilizza l'effetto fornito da LocalOverscrollFactory. Puoi personalizzare o disattivare questa opzione utilizzando l'overload scrollableArea che accetta un parametro OverscrollEffect.

Quando utilizzare il modificatore scrollableArea

Devi utilizzare il modificatore scrollableArea quando devi creare un componente di scorrimento personalizzato che non è supportato adeguatamente dai modificatori horizontalScroll o verticalScroll o dai layout con caricamento lento. Spesso si tratta di casi con:

  • Logica di layout personalizzata: quando la disposizione degli elementi cambia in modo dinamico in base alla posizione di scorrimento.
  • Effetti visivi unici: applicazione di trasformazioni, ridimensionamento o altri effetti agli elementi secondari durante lo scorrimento.
  • Controllo diretto: se hai bisogno di un controllo granulare sui meccanismi di scorrimento oltre a quello offerto da verticalScroll o dai layout lazy.

Crea elenchi personalizzati simili a ruote utilizzando scrollableArea

L'esempio seguente mostra l'utilizzo di scrollableArea per creare un elenco verticale personalizzato in cui gli elementi vengono ridotti man mano che si allontanano dal centro, creando un effetto visivo "a ruota". Questo tipo di trasformazione dipendente dallo scorrimento è un caso d'uso perfetto per scrollableArea.

Figura 2. Un elenco verticale personalizzato utilizzando scrollableArea.

@Composable
private fun ScrollableAreaSample() {
    // ...
    Layout(
        modifier =
            Modifier
                .size(150.dp)
                .scrollableArea(scrollState, Orientation.Vertical)
                .background(Color.LightGray),
        // ...
    ) { measurables, constraints ->
        // ...
        // Update the maximum scroll value to not scroll beyond limits and stop when scroll
        // reaches the end.
        scrollState.maxValue = (totalHeight - viewportHeight).coerceAtLeast(0)

        // Position the children within the layout.
        layout(constraints.maxWidth, viewportHeight) {
            // The current vertical scroll position, in pixels.
            val scrollY = scrollState.value
            val viewportCenterY = scrollY + viewportHeight / 2

            var placeableLayoutPositionY = 0
            placeables.forEach { placeable ->
                // This sample applies a scaling effect to items based on their distance
                // from the center, creating a wheel-like effect.
                // ...
                // Place the item horizontally centered with a layer transformation for
                // scaling to achieve wheel-like effect.
                placeable.placeRelativeWithLayer(
                    x = constraints.maxWidth / 2 - placeable.width / 2,
                    // Offset y by the scroll position to make placeable visible in the viewport.
                    y = placeableLayoutPositionY - scrollY,
                ) {
                    scaleX = scaleFactor
                    scaleY = scaleFactor
                }
                // Move to the next item's vertical position.
                placeableLayoutPositionY += placeable.height
            }
        }
    }
}
// ...

Modificatore scorrevole

Il modificatore scrollable è diverso dai modificatori di scorrimento in quanto scrollable rileva i gesti di scorrimento e acquisisce i delta, ma non compensa automaticamente i contenuti. Questa operazione viene invece delegata all'utente tramite ScrollableState , che è necessario per il corretto funzionamento di questo modificatore.

Quando crei ScrollableState, devi fornire una funzione consumeScrollDelta che verrà richiamata in ogni passaggio di scorrimento (tramite input di gesti, scorrimento fluido o scorrimento veloce) con il delta in pixel. Questa funzione deve restituire la quantità di distanza di scorrimento consumata per garantire che l'evento venga propagato correttamente nei casi in cui siano presenti elementi nidificati con il modificatore scrollable.

Il seguente snippet rileva i gesti e visualizza un valore numerico per un offset, ma non compensa alcun elemento:

@Composable
private fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableFloatStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                // Scrollable state: describes how to consume
                // scrolling delta and update offset
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

Un elemento UI che rileva la pressione del dito e mostra il valore numerico della posizione del dito
Figura 3. Un elemento UI che rileva la pressione del dito e mostra il valore numerico della posizione del dito.