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 de desplazamiento o conocer 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 básico fundamental para crear contenedores desplazables personalizados. Proporciona una abstracción de nivel superior sobre el modificador scrollable, que controla los requisitos comunes, como la interpretación de diferencias de gestos, el recorte de contenido y los efectos de sobredesplazamiento.

Si bien scrollableArea se usa para implementaciones personalizadas, por lo 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 sencillos para los casos de uso comunes y se compilan con scrollableArea.

Diferencia entre los modificadores 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., arrastrar el puntero) en la pantalla.
  • scrollableArea (delta orientado al contenido): El delta se invierte de forma semántica para representar el cambio seleccionado en la posición de desplazamiento para que el contenido parezca moverse con el gesto del usuario, que suele ser lo opuesto al movimiento del puntero.

Piensa en esto 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 el motivo por el 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 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 a los límites del diseño y controla la renderización de los efectos de sobredesplazamiento. De forma predeterminada, usa el efecto que proporciona 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 entregue de forma adecuada con los modificadores horizontalScroll o verticalScroll, o bien con los diseños Lazy. Esto suele implicar 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: Aplica transformaciones, escalamiento o cualquier otro efecto a los elementos secundarios a medida que se desplazan.
  • Control directo: Necesitas un control detallado de los mecanismos de desplazamiento más allá de lo que exponen verticalScroll o los diseños diferidos.

Crea listas personalizadas similares a ruedas con scrollableArea

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

Figura 2. Una lista vertical personalizada con 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 en que scrollable detecta los gestos de desplazamiento y captura los deltas, pero no compensa su contenido automáticamente. En cambio, esto 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 invocará en cada paso de desplazamiento (por entrada de gesto, desplazamiento fluido o deslizamiento rápido) con el delta en píxeles. Esta función debe mostrar 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 la IU que detecta la presión del dedo y muestra el valor numérico de la ubicación del dedo.