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 al desplazamiento
gestos

La ScrollState te permite cambiar la posición de 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 desplazable

El scrollable se diferencia de los modificadores de desplazamiento en que scrollable detecta gestos de desplazamiento y captura los deltas, pero no desplaza su contenido automáticamente. Esto se delega al usuario mediante ScrollableState , que es necesaria para que este modificador funcione correctamente.

Cuando construyas ScrollableState, debes proporcionar un consumeScrollDelta que se invocará en cada paso de desplazamiento (por entrada de gesto, desplazamiento) con el delta en píxeles. Esta función debe mostrar el la distancia de desplazamiento consumida para garantizar que el evento se realice se propagan en casos en los que hay elementos anidados que tienen el scrollable modificador.

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 { mutableStateOf(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
del dedo
ubicación

Desplazamiento anidado

El desplazamiento anidado es un sistema en el que múltiples componentes de desplazamiento contienen trabajan juntos reaccionando a un solo gesto de desplazamiento y comunicar sus deltas de desplazamiento (cambios).

El sistema de desplazamiento anidado permite la coordinación entre los componentes que se Son desplazables y están vinculadas jerárquicamente (la mayoría de las veces, cuando se comparte el mismo elemento superior). Este sistema vincula los contenedores de desplazamiento y permite la interacción con el desplazamiento deltas que se propagan y comparten.

Compose proporciona varias formas de controlar el desplazamiento anidado entre elementos componibles. Un ejemplo típico de desplazamiento anidado es una lista dentro de otra lista y una el caso complejo es la contracción barra de herramientas.

Desplazamiento anidado automático

El desplazamiento anidado simple no requiere ninguna acción de tu parte. Los gestos que inician una acción de desplazamiento se propagan de elementos secundarios a superiores de forma automática, de modo que cuando el elemento secundario no puede desplazarse más, se controla el gesto con el elemento superior.

El desplazamiento automático anidado es compatible y proporcionado de forma predeterminada por algunos de Componentes y modificadores de Compose: verticalScroll: horizontalScroll, scrollable, API de Lazy y TextField. Esto significa que, cuando el usuario se desplaza, secundario de los componentes anidados, los modificadores anteriores propagan el desplazamiento deltas a los elementos superiores que admiten desplazamiento anidado.

En el siguiente ejemplo, se muestran elementos con una verticalScroll modificador que se les aplica dentro de un contenedor que también tiene un verticalScroll se le aplicó un modificador.

@Composable
private fun AutomaticNestedScroll() {
    val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .verticalScroll(rememberScrollState())
            .padding(32.dp)
    ) {
        Column {
            repeat(6) {
                Box(
                    modifier = Modifier
                        .height(128.dp)
                        .verticalScroll(rememberScrollState())
                ) {
                    Text(
                        "Scroll here",
                        modifier = Modifier
                            .border(12.dp, Color.DarkGray)
                            .background(brush = gradient)
                            .padding(24.dp)
                            .height(150.dp)
                    )
                }
            }
        }
    }
}

Dos elementos de la IU de desplazamiento vertical anidados que responden a los gestos dentro y
fuera del interior
elemento

Usa el modificador nestedScroll

Si necesitas crear un desplazamiento coordinado avanzado entre varios elementos, el modificador nestedScroll te brinda más flexibilidad definiendo una jerarquía de desplazamiento anidada. Como mencionada en la sección anterior, algunos componentes tienen desplazamiento anidado integrado y asistencia. Sin embargo, en el caso de los elementos componibles que no se pueden desplazar automáticamente, como Box o Column, los deltas de desplazamiento en esos componentes no se propagarán en la de desplazamiento anidado y los deltas no llegarán a NestedScrollConnection ni el componente superior. Para resolver este problema, puedes usar nestedScroll para conferir la compatibilidad con otros componentes, incluidos los personalizados.

Ciclo de desplazamiento anidado

El ciclo de desplazamiento anidado es el flujo de deltas de desplazamiento que se envían hacia arriba y hacia abajo. el árbol de jerarquía a través de todos los componentes (o nodos) que forman parte del conjunto sistema de desplazamiento, por ejemplo, mediante el uso de componentes y modificadores desplazables, o nestedScroll

Fases del ciclo de desplazamiento anidado

Cuando un evento de activación (por ejemplo, un gesto) detecta un evento activador (por ejemplo, un gesto) de desplazamiento, antes de que se active la acción real de desplazamiento, Los deltas se envían al sistema de desplazamiento anidado y pasan por tres fases: antes del desplazamiento, el consumo de nodos y después del desplazamiento.

Fases del desplazamiento anidado
bicicleta

En la primera fase, previa al desplazamiento, el componente que recibió el evento activador deltas enviará esos eventos hacia arriba, a través del árbol de jerarquía, a la posición más alta superior. Los eventos delta aparecerán como burbujas, lo que significa que los deltas propagado desde el elemento superior con la raíz hacia abajo hasta el elemento secundario de desplazamiento anidado.

Fase previa al desplazamiento: envío
arriba

De esta manera, los elementos superiores de desplazamiento anidados (elementos componibles con nestedScroll o modificadores desplazables), la oportunidad de hacer algo con el delta antes del puede consumirlo.

Fase previa al desplazamiento: burbuja
hacia abajo

En la fase de consumo del nodo, el nodo usará cualquier delta que que usan sus elementos superiores. Esto ocurre cuando el movimiento de desplazamiento termina y se sean visibles.

Consumo de nodos
fase

Durante esta fase, el niño puede elegir consumir todo o parte del resto desplazar. Todo lo que quede se enviará de nuevo para pasar por la fase posterior al desplazamiento.

Por último, en la fase posterior al desplazamiento, cualquier cosa que el nodo en sí no consumió se enviarán nuevamente a sus principales para su consumo.

Fase posterior al desplazamiento: envío
arriba

La fase posterior al desplazamiento funciona de forma similar a la anterior, de los padres deciden consumir o no.

Fase posterior al desplazamiento: burbuja
hacia abajo

De manera similar al desplazamiento, cuando finaliza un gesto de arrastre, la intención del usuario puede ser se traduce en una velocidad que se usa para deslizar (desplazar con una animación) un contenedor desplazable. La acción de deslizar también forma parte del ciclo de desplazamiento anidado. y las velocidades generadas por el evento de arrastre atraviesan fases similares: consumo de nodo y posterior al lanzamiento. Ten en cuenta que la animación de desplazamiento solo se asocia con gestos táctiles y no se activará mediante otros eventos, como a11y o pergamino de hardware.

Participa en el ciclo de desplazamiento anidado

Participar en el ciclo significa interceptar, consumir e informar la y el consumo de los deltas a lo largo de la jerarquía. Compose proporciona un conjunto de herramientas para influir en el funcionamiento del sistema de desplazamiento anidado y en cómo interactuar directamente con él, por ejemplo, cuando debes hacer algo con los deltas de desplazamiento antes un componente desplazable incluso comienza a desplazarse.

Si el ciclo de desplazamiento anidado es un sistema que actúa en una cadena de nodos, el nestedScroll es una forma de interceptar e insertar en estos cambios, y también influyen en los datos (deltas de desplazamiento) que se propagan en la cadena. Esta el modificador se puede colocar en cualquier lugar de la jerarquía, y se comunica con instancias del modificador de desplazamiento anidado hacia arriba en el árbol para que pueda compartir información mediante este canal. Los componentes básicos de este modificador son NestedScrollConnection. y NestedScrollDispatcher.

NestedScrollConnection proporciona una forma de responder a las fases del ciclo de desplazamiento anidado y la influencia el sistema de desplazamiento anidado. Está compuesto por cuatro métodos de devolución de llamada, cada uno que representan una de las fases de consumo: antes y después del desplazamiento y antes y después del desplazamiento:

val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        println("Received onPreScroll callback.")
        return Offset.Zero
    }

    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        println("Received onPostScroll callback.")
        return Offset.Zero
    }
}

Cada devolución de llamada también proporciona información sobre el parámetro delta que se propaga: available delta para esa fase en particular y consumed delta consumido en fases anteriores. Si en algún momento deseas dejar de propagar deltas en de desplazamiento, puedes usar la conexión de desplazamiento anidada para hacerlo:

val disabledNestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            return if (source == NestedScrollSource.SideEffect) {
                available
            } else {
                Offset.Zero
            }
        }
    }
}

Todas las devoluciones de llamada proporcionan información sobre el NestedScrollSource el tipo de letra.

NestedScrollDispatcher inicializa el ciclo de desplazamiento anidado. Cómo usar un despachador y llamar a sus métodos activa el ciclo. Los contenedores desplazables tienen un despachador integrado que envía deltas capturados durante los gestos en el sistema. Por esta razón, la mayoría de los casos de uso de la personalización del desplazamiento anidado implican el uso de NestedScrollConnection en su lugar de un despachador, para reaccionar a los deltas existentes en lugar de enviar otros nuevos. Consulta NestedScrollDispatcherSample para más usos.

Interoperabilidad de desplazamiento anidado

Cuando intentas anidar elementos View desplazables en elementos componibles desplazables, o al revés, podrías encontrar problemas. Los más notorios ocurrirán cuando te desplaces por el elemento secundario y alcances sus límites de inicio o finalización, y se espera que el elemento superior se desplace por él. Sin embargo, este comportamiento esperado puede que no suceda o no funcione como se espera.

Este problema es el resultado de las expectativas que se compilan en elementos componibles desplazables. Los elementos componibles desplazables tienen una regla de "desplazamiento anidado predeterminado", lo que significa que cualquier contenedor desplazable debe participar en la cadena de desplazamiento anidado, ambos como elemento superior mediante NestedScrollConnection y como elemento secundario a través de NestedScrollDispatcher. Luego, el elemento secundario generaría un desplazamiento anidado para el elemento superior cuando el secundario esté en el límite. A modo de ejemplo, esta regla permite que Compose Pager y Compose LazyRow funcionen bien en conjunto. Sin embargo, cuando el desplazamiento de interoperabilidad se realiza con ViewPager2 o RecyclerView, ya que no se implementa NestedScrollingParent3, el desplazamiento continuo del elemento secundario al superior no es posible.

A fin de habilitar la API de interoperabilidad de desplazamiento anidada entre elementos View desplazables y elementos componibles desplazables, anidados en ambas direcciones, puedes usar la API de interoperabilidad de desplazamiento anidada para mitigar estos problemas en las siguientes situaciones.

Un elemento superior cooperativo View que contiene un elemento secundario ComposeView

Un elemento superior cooperativo View es aquel que ya implementa NestedScrollingParent3 y, por lo tanto, puede recibir deltas de desplazamiento de un objeto anidado cooperativo. elemento componible secundario. En este caso, ComposeView actuaría como elemento secundario y necesitar (indirectamente) implementar NestedScrollingChild3 Un ejemplo de un elemento superior cooperativo es androidx.coordinatorlayout.widget.CoordinatorLayout

Si necesitas interoperabilidad de desplazamiento anidado entre contenedores superiores View desplazables y elementos componibles desplazables secundarios, puedes usar rememberNestedScrollInteropConnection().

rememberNestedScrollInteropConnection() permite y recuerda el NestedScrollConnection que permite la interoperabilidad de desplazamiento anidada entre un elemento superior de View que implementa NestedScrollingParent3 y un elemento secundario de Compose. Se debe usar junto con un modificador nestedScroll. Como el desplazamiento anidado está habilitado de forma predeterminada en Compose, puedes usar esta conexión para habilitar el desplazamiento anidado en el lado View y agregar la lógica de unión necesaria entre Views y los elementos componibles

Un caso de uso frecuente es usar CoordinatorLayout, CollapsingToolbarLayout y Un elemento secundario componible, como se muestra en este ejemplo:

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <!--...-->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

En tu actividad o fragmento, debes configurar el elemento componible secundario y el elemento obligatorio. NestedScrollConnection:

open class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalComposeUiApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                // Add the nested scroll connection to your top level @Composable element
                // using the nestedScroll modifier.
                LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {
                    items(20) { item ->
                        Box(
                            modifier = Modifier
                                .padding(16.dp)
                                .height(56.dp)
                                .fillMaxWidth()
                                .background(Color.Gray),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(item.toString())
                        }
                    }
                }
            }
        }
    }
}

Un elemento superior componible que contiene un AndroidView secundario

En esta situación, se trata la implementación de la API de interoperabilidad de desplazamiento anidada en el lado de Compose, cuando tienes un elemento superior que admite composición y que contiene un AndroidView secundario. El AndroidView implementa NestedScrollDispatcher, ya que actúa como un elemento secundario de un elemento superior de desplazamiento de Compose, así como NestedScrollingParent3 porque actúa como superior para unView elemento secundario de desplazamiento. El elemento superior de Compose hará lo siguiente: luego podrá recibir deltas de desplazamiento anidados desde un elemento secundario anidado View

En el siguiente ejemplo, se muestra cómo lograr una interoperabilidad de desplazamiento anidada en esta junto con una barra de herramientas que se contrae de Compose:

@Composable
private fun NestedScrollInteropComposeParentWithAndroidChildExample() {
    val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }

    // Sets up the nested scroll connection between the Box composable parent
    // and the child AndroidView containing the RecyclerView
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Updates the toolbar offset based on the scroll to enable
                // collapsible behaviour
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
        TopAppBar(
            modifier = Modifier
                .height(ToolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
        )

        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
                        with(findViewById<RecyclerView>(R.id.main_list)) {
                            layoutManager = LinearLayoutManager(context, VERTICAL, false)
                            adapter = NestedScrollInteropAdapter()
                        }
                    }.also {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(it, true)
                    }
            },
            // ...
        )
    }
}

private class NestedScrollInteropAdapter :
    Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {
    val items = (1..10).map { it.toString() }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): NestedScrollInteropViewHolder {
        return NestedScrollInteropViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {
        // ...
    }

    class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {
        fun bind(item: String) {
            // ...
        }
    }
    // ...
}

En este ejemplo, se muestra cómo usar la API con un modificador scrollable:

@Composable
fun ViewInComposeNestedScrollInteropExample() {
    Box(
        Modifier
            .fillMaxSize()
            .scrollable(rememberScrollableState {
                // View component deltas should be reflected in Compose
                // components that participate in nested scrolling
                it
            }, Orientation.Vertical)
    ) {
        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(android.R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

Por último, en este ejemplo, se muestra cómo se usa la API de interoperabilidad de desplazamiento anidada con BottomSheetDialogFragment para lograr un comportamiento exitoso de arrastre y descarte:

class BottomSheetFragment : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)

        rootView.findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                LazyColumn(
                    Modifier
                        .nestedScroll(nestedScrollInterop)
                        .fillMaxSize()
                ) {
                    item {
                        Text(text = "Bottom sheet title")
                    }
                    items(10) {
                        Text(
                            text = "List item number $it",
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }
            return rootView
        }
    }
}

Ten en cuenta que rememberNestedScrollInteropConnection() instalará un NestedScrollConnection en el elemento al que lo adjuntas. NestedScrollConnection es responsable de transmitir los deltas del nivel de Compose al nivel de View. Esto permite el elemento participe en el desplazamiento anidado, pero no habilita el desplazamiento de elementos automáticamente. A elementos componibles que no se pueden desplazar automáticamente, como Box o Column, los deltas de desplazamiento en esos componentes no se se propagan en el sistema de desplazamiento anidado y los deltas no llegarán a NestedScrollConnection proporcionado por rememberNestedScrollInteropConnection(), por lo que esos deltas no llegarán al componente View superior. Para solucionar esto, asegúrate de establecer también modificadores desplazables para estos tipos elementos componibles. Puedes consultar la sección anterior sobre Nested desplazamiento para obtener información más detallada información.

Un elemento superior View que no coopera y que contiene un elemento secundario ComposeView

Una vista no cooperativa es la que no implementa las interfaces NestedScrolling necesarias del lado de View. Ten en cuenta que esto significa que la interoperabilidad de desplazamiento anidado con estos Views no funciona de forma inmediata. Los Views que no cooperativos son RecyclerView y ViewPager2.