Lo scorrimento nidificato è un sistema in cui più componenti di scorrimento contenuti l'uno nell'altro funzionano insieme reagendo a un singolo gesto di scorrimento e comunicando le relative modifiche (delta).
Il sistema di scorrimento nidificato consente la coordinazione tra i componenti scorrevoli e collegati gerarchicamente (il più delle volte condividendo lo stesso elemento principale). Questo sistema collega i container di scorrimento e consente l'interazione con le modifiche di scorrimento che vengono propagate e condivise tra di essi.
Compose offre diversi modi per gestire lo scorrimento nidificato tra i composable. Un esempio tipico di scorrimento nidificato è un elenco all'interno di un altro elenco, mentre un caso più complesso è una barra degli strumenti comprimibile.
Scorrimento nidificato automatico
Lo scorrimento nidificato semplice non richiede alcuna azione da parte tua. I gesti che avviano un'azione di scorrimento vengono propagati automaticamente dagli elementi secondari agli elementi principali, in modo che quando l'elemento secondario non può scorrere ulteriormente, il gesto venga gestito dal relativo elemento principale.
Lo scorrimento nidificato automatico è supportato e fornito immediatamente da alcuni componenti e modificatori di
Compose:
verticalScroll,
horizontalScroll,
scrollable,
Lazy API e TextField. Ciò significa che quando l'utente scorre un elemento secondario interno dei componenti nidificati, i modificatori precedenti propagano le modifiche di scorrimento agli elementi principali che supportano lo scorrimento nidificato.
L'esempio seguente mostra gli elementi con un
verticalScroll
modificatore applicato all'interno di un container a cui è stato applicato anche un verticalScroll
modificatore.
@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) ) } } } } }
Utilizzare il modificatore nestedScroll
Se devi creare uno scorrimento coordinato avanzato tra più elementi,
il
nestedScroll
modificatore ti offre maggiore flessibilità definendo una gerarchia di scorrimento nidificata. Come indicato nella sezione precedente, alcuni componenti supportano lo scorrimento nidificato integrato. Tuttavia, per i composable che non sono scorrevoli automaticamente, come Box o Column, le modifiche di scorrimento su questi componenti non vengono propagate nel sistema di scorrimento nidificato e non raggiungono NestedScrollConnection né il componente principale. Per risolvere questo problema, puoi utilizzare nestedScroll per conferire questo supporto ad altri componenti, inclusi i componenti personalizzati.
Ciclo di scorrimento nidificato
Il ciclo di scorrimento nidificato è il flusso di modifiche di scorrimento che vengono inviate verso l'alto e verso il basso nell'albero della gerarchia tramite tutti i componenti (o nodi) che fanno parte del sistema di scorrimento nidificato, ad esempio utilizzando componenti e modificatori scorrevoli o nestedScroll.
Fasi del ciclo di scorrimento nidificato
Quando un componente scorrevole rileva un evento di trigger (ad esempio, un gesto), prima ancora che venga attivata l'azione di scorrimento effettiva, le modifiche generate vengono inviate al sistema di scorrimento nidificato e attraversano tre fasi: pre-scorrimento, consumo dei nodi e post-scorrimento.
Nella prima fase, quella di pre-scorrimento, il componente che ha ricevuto le modifiche dell'evento di trigger invierà questi eventi verso l'alto, attraverso l'albero della gerarchia, fino all'elemento principale di livello più alto. Gli eventi di modifica verranno quindi propagati verso il basso, il che significa che le modifiche verranno propagate dall'elemento principale di livello più alto verso l'elemento secondario che ha avviato il ciclo di scorrimento nidificato.
In questo modo, gli elementi principali di scorrimento nidificato (composable che utilizzano modificatori nestedScroll o scorrevoli) hanno la possibilità di eseguire un'azione con la modifica prima che il nodo stesso possa utilizzarla.
Nella fase di consumo dei nodi, il nodo stesso utilizzerà le modifiche non utilizzate dai relativi elementi principali. È in questo momento che il movimento di scorrimento viene effettivamente eseguito ed è visibile.
Durante questa fase, l'elemento secondario può scegliere di utilizzare tutte o parte delle modifiche di scorrimento rimanenti. Tutto ciò che rimane verrà inviato di nuovo verso l'alto per passare alla fase di post-scorrimento.
Infine, nella fase di post-scorrimento, tutto ciò che il nodo stesso non ha utilizzato verrà inviato di nuovo verso l'alto ai relativi antenati per essere utilizzato.
La fase di post-scorrimento funziona in modo simile alla fase di pre-scorrimento, in cui uno qualsiasi degli elementi principali può scegliere di utilizzare o meno le modifiche.
Analogamente allo scorrimento, al termine di un gesto di trascinamento, l'intenzione dell'utente può essere tradotta in una velocità utilizzata per scorrere (scorrere utilizzando un'animazione) il container scorrevole. Anche lo scorrimento rapido fa parte del ciclo di scorrimento nidificato e le velocità generate dall'evento di trascinamento attraversano fasi simili: pre-scorrimento rapido, consumo dei nodi e post-scorrimento rapido. Tieni presente che l'animazione di scorrimento rapido è associata solo al gesto di tocco e non viene attivata da altri eventi, come lo scorrimento hardware o di accessibilità.
Partecipare al ciclo di scorrimento nidificato
La partecipazione al ciclo significa intercettare, utilizzare e segnalare l'utilizzo delle modifiche lungo la gerarchia. Compose fornisce un insieme di strumenti per influenzare il funzionamento del sistema di scorrimento nidificato e interagire direttamente con esso, ad esempio quando devi eseguire un'azione con le modifiche di scorrimento prima ancora che un componente scorrevole inizi a scorrere.
Se il ciclo di scorrimento nidificato è un sistema che agisce su una catena di nodi, il
nestedScroll
modificatore è un modo per intercettare e inserire queste modifiche e
influenzare i dati (modifiche di scorrimento) che vengono propagati nella catena. Questo modificatore può essere posizionato ovunque nella gerarchia e comunica con le istanze del modificatore di scorrimento nidificato nell'albero in modo da poter condividere informazioni tramite questo canale. I blocchi di base di questo modificatore sono NestedScrollConnection e NestedScrollDispatcher.
NestedScrollConnection
fornisce un modo per rispondere alle fasi del ciclo di scorrimento nidificato e influenzare il sistema di scorrimento nidificato. È composto da quattro metodi di callback, ognuno dei quali rappresenta una delle fasi di consumo: pre/post-scorrimento e pre/post-scorrimento rapido:
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 } }
Ogni callback fornisce anche informazioni sulla modifica propagata: la modifica available per quella fase specifica e la modifica consumed utilizzata nelle fasi precedenti. Se in qualsiasi momento vuoi interrompere la propagazione delle modifiche nella gerarchia, puoi utilizzare la connessione di scorrimento nidificata:
val disabledNestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource ): Offset { return if (source == NestedScrollSource.SideEffect) { available } else { Offset.Zero } } } }
Tutti i callback forniscono informazioni sul
NestedScrollSource
tipo.
NestedScrollDispatcher
inizializza il ciclo di scorrimento nidificato. L'utilizzo di un dispatcher e la chiamata dei relativi metodi attivano il ciclo. I container scorrevoli hanno un dispatcher integrato che invia le modifiche acquisite durante i gesti al sistema. Per questo motivo, la maggior parte dei casi d'uso della personalizzazione dello scorrimento nidificato prevede l'utilizzo di NestedScrollConnection anziché di un dispatcher, per reagire alle modifiche già esistenti anziché inviarne di nuove.
Per ulteriori utilizzi, consulta
NestedScrollDispatcherSample.
Ridimensionare un'immagine durante lo scorrimento
Quando l'utente scorre, puoi creare un effetto visivo dinamico in cui le dimensioni dell'immagine cambiano in base alla posizione di scorrimento.
Ridimensionare un'immagine in base alla posizione di scorrimento
Questo snippet mostra come ridimensionare un'immagine all'interno di un LazyColumn in base a
posizione di scorrimento verticale. L'immagine si riduce quando l'utente scorre verso il basso e si ingrandisce quando scorre verso l'alto, rimanendo entro i limiti di dimensione minima e massima definiti:
@Composable fun ImageResizeOnScrollExample( modifier: Modifier = Modifier, maxImageSize: Dp = 300.dp, minImageSize: Dp = 100.dp ) { var currentImageSize by remember { mutableStateOf(maxImageSize) } var imageScale by remember { mutableFloatStateOf(1f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Calculate the change in image size based on scroll delta val delta = available.y val newImageSize = currentImageSize + delta.dp val previousImageSize = currentImageSize // Constrain the image size within the allowed bounds currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize) val consumed = currentImageSize - previousImageSize // Calculate the scale for the image imageScale = currentImageSize / maxImageSize // Return the consumed scroll amount return Offset(0f, consumed.value) } } } Box(Modifier.nestedScroll(nestedScrollConnection)) { LazyColumn( Modifier .fillMaxWidth() .padding(15.dp) .offset { IntOffset(0, currentImageSize.roundToPx()) } ) { // Placeholder list items items(100, key = { it }) { Text( text = "Item: $it", style = MaterialTheme.typography.bodyLarge ) } } Image( painter = ColorPainter(Color.Red), contentDescription = "Red color image", Modifier .size(maxImageSize) .align(Alignment.TopCenter) .graphicsLayer { scaleX = imageScale scaleY = imageScale // Center the image vertically as it scales translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f } ) } }
Punti chiave sul codice
- Questo codice utilizza un
NestedScrollConnectionper intercettare gli eventi di scorrimento. onPreScrollcalcola la modifica delle dimensioni dell'immagine in base alla modifica di scorrimento.- La variabile di stato
currentImageSizememorizza le dimensioni attuali dell'immagine, vincolate traminImageSizeemaxImageSize. imageScalederiva dacurrentImageSize. - Il
LazyColumncompensa in base acurrentImageSize. - Il
Imageutilizza un modificatoregraphicsLayerper applicare la scala calcolata. translationYall'interno digraphicsLayergarantisce che l'immagine rimanga centrata verticalmente durante il ridimensionamento.
Risultato
Lo snippet precedente genera un effetto di ridimensionamento dell'immagine durante lo scorrimento:
Interop dello scorrimento nidificato
Quando provi a nidificare elementi View scorrevoli in composable scorrevoli o viceversa, potresti riscontrare problemi. I più evidenti si verificano quando scorri l'elemento secondario e raggiungi i limiti di inizio o fine e ti aspetti che l'elemento principale prenda il controllo dello scorrimento. Tuttavia, questo comportamento previsto potrebbe non verificarsi o non funzionare come previsto.
Questo problema è il risultato delle aspettative integrate nei composable scorrevoli.
I composable scorrevoli hanno una regola "nested-scroll-by-default", il che significa che
qualsiasi container scorrevole deve partecipare alla catena di scorrimento nidificata, sia come
elemento principale tramite
NestedScrollConnection,
sia come elemento secondario tramite
NestedScrollDispatcher.
L'elemento secondario attiverebbe quindi uno scorrimento nidificato per l'elemento principale quando l'elemento secondario si trova al limite. Ad esempio, questa regola consente a Compose Pager e Compose LazyRow di funzionare bene insieme. Tuttavia, quando lo scorrimento di interoperabilità viene eseguito
con ViewPager2 o RecyclerView, poiché questi non implementano
NestedScrollingParent3,
lo scorrimento continuo dall'elemento secondario all'elemento principale non è possibile.
Per abilitare l'API di interoperabilità dello scorrimento nidificato tra elementi View scorrevoli e composable scorrevoli, nidificati in entrambe le direzioni, puoi utilizzare l'API di interoperabilità dello scorrimento nidificato per mitigare questi problemi nei seguenti scenari.
Un elemento principale cooperativo View contenente un elemento secondario ComposeView
Un elemento principale cooperativo View è uno che implementa già
NestedScrollingParent3
e quindi è in grado di ricevere modifiche di scorrimento da un composable secondario nidificato cooperativo. ComposeView fungerebbe da elemento secondario in questo caso e
dovrebbe implementare (indirettamente)
NestedScrollingChild3.
Un esempio di elemento principale cooperativo è androidx.coordinatorlayout.widget.CoordinatorLayout.
Se hai bisogno dell'interoperabilità dello scorrimento nidificato tra i container principali View scorrevoli e i composable secondari scorrevoli nidificati, puoi utilizzare
rememberNestedScrollInteropConnection().
rememberNestedScrollInteropConnection()
consente e memorizza
NestedScrollConnection
che abilita l'interoperabilità dello scorrimento nidificato tra un elemento principale View che
implementa
NestedScrollingParent3
e un elemento secondario Compose. Questo modificatore deve essere utilizzato insieme a un
nestedScroll
modificatore. Poiché lo scorrimento nidificato è abilitato per impostazione predefinita sul lato Compose, puoi utilizzare questa connessione per abilitare lo scorrimento nidificato sul lato View e aggiungere la logica di collegamento necessaria tra Views e composable.
Un caso d'uso frequente è l'utilizzo di CoordinatorLayout, CollapsingToolbarLayout e un composable secondario, come mostrato in questo esempio:
<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>
Nell'attività o nel fragment, devi configurare il composable secondario e il
richiesto
NestedScrollConnection:
open class MainActivity : ComponentActivity() { 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 composable principale contenente un elemento AndroidView secondario
Questo scenario riguarda l'implementazione dell'API di interoperabilità dello scorrimento nidificato sul lato Compose, quando hai un composable principale contenente un elemento AndroidView secondario. Il AndroidView implementa
NestedScrollDispatcher,
poiché funge da elemento secondario di un elemento principale di scorrimento Compose, nonché
NestedScrollingParent3
, poiché funge da elemento principale di un elemento secondario di scorrimento View. L'elemento principale Compose sarà quindi in grado di ricevere modifiche di scorrimento nidificate da un elemento secondario View scorrevole nidificato.
L'esempio seguente mostra come ottenere l'interoperabilità dello scorrimento nidificato in questo scenario, insieme a una barra degli strumenti comprimibile di 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( title = {}, 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) { // ... } } // ... }
Questo esempio mostra come utilizzare l'API con un modificatore 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(R.layout.list_item, null) .apply { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(this, true) } } ) } }
Infine, questo esempio mostra come l'API di interoperabilità dello scorrimento nidificato viene utilizzata con
BottomSheetDialogFragment
per ottenere un comportamento di trascinamento e chiusura corretto:
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 } } }
Tieni presente che
rememberNestedScrollInteropConnection()
installerà un
NestedScrollConnection
nell'elemento a cui lo colleghi. NestedScrollConnection è responsabile della trasmissione delle modifiche dal livello Compose al livello View. In questo modo l'elemento può partecipare allo scorrimento nidificato, ma non abilita automaticamente lo scorrimento degli elementi. Per i composable che non sono scorrevoli automaticamente, come Box o Column, le modifiche di scorrimento su questi componenti non vengono propagate nel sistema di scorrimento nidificato e non raggiungono NestedScrollConnection fornito da rememberNestedScrollInteropConnection(), pertanto queste modifiche non raggiungono il componente View principale. Per risolvere questo problema, assicurati di impostare anche i modificatori scorrevoli per questi tipi di composable nidificati. Per informazioni più dettagliate, consulta la sezione precedente sullo scorrimento nidificato.
Un elemento principale non cooperativo View contenente un elemento secondario ComposeView
Un elemento View non cooperativo è uno che non implementa le interfacce NestedScrolling necessarie sul lato View. Tieni presente che ciò significa che l'interoperabilità dello scorrimento nidificato con queste Views non funziona immediatamente. Le Views non cooperative sono RecyclerView e ViewPager2.
Risorse aggiuntive
- Creare un pulsante per abilitare lo scorrimento con ancoraggio
- Creare un effetto di scorrimento parallasse
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Informazioni sui gesti
- Eseguire la migrazione di
CoordinatorLayouta Compose - Utilizzare le View in Compose