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)) } } }
La función ScrollState
te permite cambiar la posición de desplazamiento o 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 modificador scrollable
difiere de los modificadores de desplazamiento en que 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 se requiere 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 suave o deslizamiento) 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 { 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()) } }
Desplazamiento anidado
El desplazamiento anidado es un sistema en el que varios componentes de desplazamiento contenidos entre sí funcionan juntos reaccionando a un solo gesto de desplazamiento y comunicando sus deltas de desplazamiento (cambios).
El sistema de desplazamiento anidado permite la coordinación entre componentes que se pueden desplazar y que están vinculados de manera jerárquica (la mayoría de las veces, cuando comparten el mismo elemento superior). Este sistema vincula los contenedores de desplazamiento y permite la interacción con los delta de desplazamiento 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 un caso más complejo es una barra de herramientas que se contrae.
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 se proporciona de forma predeterminada con algunos de los componentes y modificadores de Compose: verticalScroll
, horizontalScroll
, las APIs de scrollable
, Lazy
y TextField
. Esto significa que, cuando el usuario se desplaza por un elemento secundario interno de componentes anidados, los modificadores anteriores propagan los delta de desplazamiento a los elementos superiores que admiten este tipo de desplazamiento.
En el siguiente ejemplo, se muestran los elementos con un modificador verticalScroll
aplicado dentro de un contenedor que también tiene aplicado un modificador verticalScroll
.
@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) ) } } } } }
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 se mencionó en la sección anterior, algunos componentes tienen compatibilidad integrada con el desplazamiento anidado. 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 el sistema de desplazamiento anidado, y estos no llegarán a NestedScrollConnection
ni al componente superior. Para resolver este problema, puedes usar nestedScroll
para conferir esa compatibilidad a 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 en el árbol de jerarquía a través de todos los componentes (o nodos) que forman parte del sistema de desplazamiento anidado, por ejemplo, mediante componentes y modificadores desplazables, o nestedScroll
.
Fases del ciclo de desplazamiento anidado
Cuando un componente desplazable detecta un evento activador (por ejemplo, un gesto), antes de que se active la acción real, los deltas generados se envían al sistema de desplazamiento anidado y pasan por tres fases: predesplazamiento, consumo de nodos y posterior al desplazamiento.
En la primera fase, previa al desplazamiento, el componente que recibió los delta del evento activador enviará esos eventos hacia arriba, a través del árbol de jerarquía, al elemento superior superior. Luego, los eventos delta se expandirán, lo que significa que los deltas se propagarán desde el elemento superior raíz hacia el elemento secundario que inició el ciclo de desplazamiento anidado.
Esto brinda a los elementos superiores de desplazamiento anidado (que admiten composición con nestedScroll
o modificadores desplazables) la oportunidad de realizar alguna acción con el delta antes de que el nodo pueda consumirlo.
En la fase de consumo del nodo, el nodo usará cualquier delta que sus elementos superiores no hayan usado. Esto ocurre cuando el movimiento de desplazamiento termina y es visible.
Durante esta fase, el elemento secundario puede elegir consumir todo el desplazamiento restante o parte de él. Todo lo que quede se enviará de nuevo para pasar por la fase posterior al desplazamiento.
Por último, en la fase posterior al desplazamiento, todo lo que el nodo en sí no consumió se enviará de nuevo a sus principales para su consumo.
La fase posterior al desplazamiento funciona de manera similar a la fase previa, en la que cualquiera de los elementos superiores puede optar por consumir o no.
De manera similar al desplazamiento, cuando finaliza un gesto de arrastre, la intención del usuario puede traducirse en una velocidad que se usa para deslizar (desplazar con una animación) el contenedor desplazable. El desplazamiento también forma parte del ciclo de desplazamiento anidado, y las velocidades que genera el evento de arrastre atraviesan fases similares: lanzamiento previo, consumo de nodos y posterior al lanzamiento. Ten en cuenta que la animación de deslizamiento solo se asocia con el gesto táctil y no se activará con otros eventos, como a11y o desplazamiento por hardware.
Participa en el ciclo de desplazamiento anidado
La participación en el ciclo implica interceptar, consumir e informar el consumo de 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 cómo interactuar directamente con él, por ejemplo, cuando necesitas realizar alguna acción con los deltas de desplazamiento antes de que un componente desplazable comience a desplazarse.
Si el ciclo de desplazamiento anidado es un sistema que actúa sobre una cadena de nodos, el modificador nestedScroll
es una forma de interceptar estos cambios e insertarlos en estos cambios, además de influir en los datos (deltas de desplazamiento) que se propagan en la cadena. Este modificador se puede colocar en cualquier lugar de la jerarquía y se comunica con las instancias del modificador de desplazamiento anidadas en el árbol para poder compartir información a través de 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 e influir en el sistema de desplazamiento anidado. Está compuesto por cuatro métodos de devolución de llamada, cada uno de los cuales representa una de las fases de consumo: antes y después del desplazamiento, y antes y después de navegar:
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 delta que se propaga: available
delta para esa fase en particular y consumed
delta consumido en las fases anteriores. Si en algún momento quieres dejar de propagar deltas en la jerarquía, 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 tipo NestedScrollSource
.
NestedScrollDispatcher
inicializa el ciclo de desplazamiento anidado. 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 al sistema. Por esta razón, la mayoría de los casos de uso para personalizar el desplazamiento anidado implican usar NestedScrollConnection
en lugar de un despachador para reaccionar a los deltas existentes en lugar de enviar otros nuevos.
Consulta NestedScrollDispatcherSample
para ver más usos.
Interoperabilidad de desplazamiento anidado
Cuando intentas anidar elementos View
desplazables en elementos componibles desplazables, o viceversa, es posible que encuentres problemas. Los más notables suceden cuando te desplazas por el elemento secundario y alcanzas sus límites de inicio o finalización, y se espera que el elemento superior se desplace por el elemento superior. Sin embargo, es posible que este comportamiento esperado no ocurra 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 View
cooperativo es aquel que ya implementa NestedScrollingParent3
y, por lo tanto, puede recibir deltas de desplazamiento de un elemento secundario anidado cooperativo que admite composición. En este caso, ComposeView
actuaría como elemento secundario y tendría que implementar NestedScrollingChild3
(de forma indirecta).
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
. Dado que el desplazamiento anidado está habilitado de forma predeterminada en el lado de 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 consiste en 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 secundario componible y el NestedScrollConnection
requerido:
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. Luego, el elemento superior de Compose podrá recibir deltas de desplazamiento anidados desde un View
secundario de desplazamiento anidado.
En el siguiente ejemplo, se muestra cómo lograr una interoperabilidad de desplazamiento anidada en esta situación, 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 que el elemento participe en el desplazamiento anidado, pero no habilita el desplazamiento de elementos automáticamente. 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 el sistema de desplazamiento anidado y los deltas no alcanzarán el NestedScrollConnection
proporcionado por rememberNestedScrollInteropConnection()
, por lo tanto, esos deltas no llegarán al componente View
superior. Para resolver este problema, asegúrate de configurar también los modificadores desplazables a estos tipos de elementos componibles anidados. Puedes consultar la sección anterior sobre Desplazamiento anidado para obtener información más detallada.
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
.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Información sobre los gestos
- Cómo migrar
CoordinatorLayout
a Compose - Cómo usar objetos View en Compose