Desplázate

Modificadores de desplazamiento

Los selectores verticalScroll y horizontalScroll proporcionan la forma más sencilla de permitir que el usuario se desplace por un elemento cuando los límites del contenido sean más grandes que las restricciones de tamaño máximo. Con los modificadores verticalScroll y horizontalScroll, no necesitas traducir ni compensar el contenido.

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

Una lista vertical simple que responde a los gestos de desplazamiento
Figura 1: Una lista vertical simple que responde a los gestos de desplazamiento.

ScrollState te permite cambiar la posición del desplazamiento u obtener su estado actual. Para crearlo con los parámetros predeterminados, usa 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))
        }
    }
}

Modificador de área desplazable

El modificador scrollableArea es un componente fundamental para crear contenedores desplazables personalizados. Proporciona una abstracción de nivel superior sobre el modificador scrollable, ya que controla requisitos comunes, como la interpretación del delta de gestos, el recorte de contenido y los efectos de desplazamiento excesivo.

Si bien scrollableArea se usa para implementaciones personalizadas, en general, deberías preferir soluciones listas para usar, como verticalScroll, horizontalScroll o elementos componibles como LazyColumn para las listas de desplazamiento estándar. Estos componentes de nivel superior son más simples para los casos de uso comunes y se compilan con scrollableArea.

Diferencia entre los modificadores de scrollableArea y scrollable

La principal diferencia entre scrollableArea y scrollable radica en cómo interpretan los gestos de desplazamiento del usuario:

  • scrollable (delta sin procesar): El delta refleja directamente el movimiento físico de la entrada del usuario (p.ej., arrastre del puntero) en la pantalla.
  • scrollableArea (delta orientado al contenido): El delta se invierte semánticamente para representar el cambio seleccionado en la posición de desplazamiento y hacer que el contenido parezca moverse con el gesto del usuario, que suele ser lo contrario del movimiento del puntero.

Piensa en ello de la siguiente manera: scrollable te indica cómo se movió el puntero, mientras que scrollableArea traduce ese movimiento del puntero en cómo se debe mover el contenido dentro de una vista desplazable típica. Esta inversión es la razón por la que scrollableArea se siente más natural cuando se implementa un contenedor desplazable estándar.

En la siguiente tabla, se resumen los signos delta para situaciones comunes:

Gesto del usuario

Delta informado a dispatchRawDelta por scrollable

Delta informado a dispatchRawDelta por scrollableArea*

El puntero se mueve HACIA ARRIBA.

Negativo

Positivo

El puntero se mueve HACIA ABAJO.

Positivo

Negativo

El puntero se mueve hacia la IZQUIERDA.

Negativo

Positivo (negativo para RTL)

El puntero se mueve hacia la DERECHA.

Positivo

Negativo (positivo para RTL)

(*) Nota sobre el signo del delta de scrollableArea: El signo del delta de scrollableArea no es solo una inversión simple. Considera de forma inteligente lo siguiente:

  1. Orientación: Vertical u horizontal
  2. LayoutDirection: De izquierda a derecha o de derecha a izquierda (especialmente importante para el desplazamiento horizontal)
  3. Marca reverseScrolling: Indica si se invierte la dirección de desplazamiento.

Además de invertir el delta de desplazamiento, scrollableArea también recorta el contenido según los límites del diseño y controla la renderización de los efectos de desplazamiento excesivo. De forma predeterminada, se usa el efecto proporcionado por LocalOverscrollFactory. Puedes personalizar o inhabilitar esta opción con la sobrecarga scrollableArea que acepta un parámetro OverscrollEffect.

Cuándo usar el modificador scrollableArea

Debes usar el modificador scrollableArea cuando necesites compilar un componente de desplazamiento personalizado que no se pueda usar adecuadamente con los modificadores horizontalScroll o verticalScroll, o con los diseños Lazy. A menudo, esto implica casos con las siguientes características:

  • Lógica de diseño personalizada: Cuando la disposición de los elementos cambia de forma dinámica según la posición de desplazamiento.
  • Efectos visuales únicos: Aplicar transformaciones, ajustes de escala u otros efectos a los elementos secundarios a medida que se desplazan.
  • Control directo: Necesitas un control detallado sobre la mecánica de desplazamiento más allá de lo que exponen verticalScroll o los diseños Lazy.

Crea listas personalizadas similares a ruedas con scrollableArea

En el siguiente ejemplo, se muestra el uso de scrollableArea para compilar una lista vertical personalizada en la que los elementos se reducen a medida que se alejan del centro, lo que crea un efecto visual similar a una rueda. Este tipo de transformación dependiente del desplazamiento es un caso de uso perfecto para scrollableArea.

Figura 2: Una lista vertical personalizada que usa 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
            }
        }
    }
}
// ...

Modificador desplazable

El modificador scrollable difiere de los modificadores de desplazamiento porque scrollable detecta los gestos de desplazamiento y captura los deltas, pero no desplaza su contenido automáticamente. En cambio, se delega al usuario a través de ScrollableState, que es necesario para que este modificador funcione correctamente.

Cuando construyas ScrollableState, debes proporcionar una función consumeScrollDelta que se invoque en cada paso de desplazamiento (con entrada de gestos, desplazamiento suave o arrastrar y soltar) con el delta en píxeles. Esta función debe devolver la cantidad de distancia de desplazamiento consumida para garantizar que el evento se propague correctamente en los casos en que haya elementos anidados que tengan el modificador scrollable.

En el siguiente fragmento, se detectan los gestos y se muestra un valor numérico para un desplazamiento, pero no se desplaza ningún 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 de IU que detecta la presión del dedo y muestra el valor numérico de la ubicación del dedo
Figura 3: Un elemento de IU que detecta la presión del dedo y muestra el valor numérico de la ubicación del dedo.