Lo scorrimento nidificato è un sistema in cui più componenti di scorrimento contenuti l'uno nell'altro collaborano reagendo a un singolo gesto di scorrimento e comunicando i relativi delta di scorrimento (modifiche).
Il sistema di scorrimento nidificato consente il coordinamento tra i componenti scorrevoli e collegati gerarchicamente (nella maggior parte dei casi condividendo lo stesso elemento principale). Questo sistema collega i container di scorrimento e consente l'interazione con i delta di scorrimento che vengono propagati e condivisi tra loro.
Compose offre diversi modi per gestire lo scorrimento nidificato tra i componibili. 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 alcun intervento da parte tua. I gesti che avviano un'azione di scorrimento vengono propagati automaticamente dagli elementi secondari a quelli principali, in modo che quando l'elemento secondario non può più scorrere, il gesto viene gestito dall'elemento principale.
Lo scorrimento nidificato automatico è supportato e fornito immediatamente da alcuni componenti e modificatori di Compose: verticalScroll, horizontalScroll, scrollable, API Lazy e TextField. Ciò significa che quando l'utente scorre un elemento secondario interno di componenti nidificati, i modificatori precedenti propagano i delta di scorrimento agli elementi principali che supportano lo scorrimento nidificato.
L'esempio seguente mostra gli elementi con un modificatore verticalScroll applicato all'interno di un container a cui è applicato anche un modificatore 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) ) } } } } }
Utilizzo del modificatore nestedScroll
Se devi creare uno scorrimento coordinato avanzato tra più elementi, il modificatore nestedScroll offre una maggiore flessibilità definendo una gerarchia di scorrimento nidificata. Come
menzionato nella sezione precedente, alcuni componenti hanno il supporto integrato per lo scorrimento
nidificato. Tuttavia, per i composabili che non sono scorrevoli automaticamente, come Box o Column, i delta di scorrimento su questi componenti non verranno propagati nel sistema di scorrimento nidificato e i delta non raggiungeranno NestedScrollConnection né il componente principale. Per risolvere il problema, puoi utilizzare nestedScroll per conferire questo supporto ad altri componenti, inclusi quelli personalizzati.
Ciclo di scorrimento nidificato
Il ciclo di scorrimento nidificato è il flusso di delta di scorrimento che vengono inviati su e giù
per l'albero della gerarchia attraverso 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 evento di attivazione (ad esempio un gesto) viene rilevato da un componente scorrevole, prima ancora che venga attivata l'azione di scorrimento effettiva, i delta generati vengono inviati al sistema di scorrimento nidificato e passano attraverso tre fasi: pre-scorrimento, consumo del nodo e post-scorrimento.
Nella prima fase, prima dello scorrimento, il componente che ha ricevuto i delta degli eventi di attivazione invia questi eventi verso l'alto, attraverso l'albero della gerarchia, fino al componente principale più in alto. Gli eventi delta verranno quindi propagati verso il basso, il che significa che i delta verranno propagati dal livello principale più alto verso il livello secondario che ha avviato il ciclo di scorrimento nidificato.
In questo modo, i componenti principali di scorrimento nidificati (componibili che utilizzano nestedScroll o modificatori scorrevoli) hanno la possibilità di eseguire un'azione con il delta prima che il nodo stesso possa utilizzarlo.
Nella fase di consumo del nodo, il nodo stesso utilizzerà il delta non utilizzato dai relativi elementi principali. Questo è il momento in cui il movimento di scorrimento viene effettivamente eseguito ed è visibile.
Durante questa fase, il bambino può scegliere di consumare tutto o parte del rotolo rimanente. Tutto ciò che rimane verrà inviato nuovamente per passare alla fase di post-scorrimento.
Infine, nella fase post-scorrimento, tutto ciò che non è stato consumato dal nodo stesso verrà inviato nuovamente ai suoi predecessori per il consumo.
La fase post-scorrimento funziona in modo simile alla fase pre-scorrimento, in cui uno qualsiasi dei genitori può scegliere se consumare o meno.
Analogamente allo scorrimento, quando un gesto di trascinamento termina, l'intenzione dell'utente può essere tradotta in una velocità utilizzata per scorrere (scorrimento utilizzando un'animazione) il contenitore scorrevole. Anche lo scorrimento rapido fa parte del ciclo di scorrimento nidificato e le velocità generate dall'evento di trascinamento passano attraverso fasi simili: pre-scorrimento rapido, consumo del nodo e post-scorrimento rapido. Tieni presente che l'animazione di scorrimento rapido è associata solo al gesto di tocco e non verrà attivata da altri eventi, come l'accessibilità o lo scorrimento hardware.
Partecipa al ciclo di scorrimento nidificato
La partecipazione al ciclo significa intercettare, consumare e segnalare il consumo di delta lungo la gerarchia. Compose fornisce un insieme di strumenti per influenzare il funzionamento del sistema di scorrimento nidificato e come interagire direttamente con questo, ad esempio quando devi eseguire un'operazione con i delta di scorrimento prima che un componente scorrevole inizi a scorrere.
Se il ciclo di scorrimento nidificato è un sistema che agisce su una catena di nodi, il modificatore nestedScroll è un modo per intercettare e inserire queste modifiche e influenzare i dati (delta di scorrimento) propagati nella catena. Questo modificatore può essere posizionato in qualsiasi punto della gerarchia e comunica con le istanze del modificatore di scorrimento nidificate nell'albero in modo da poter condividere informazioni tramite questo canale. I componenti di base di questo modificatore sono NestedScrollConnection
e NestedScrollDispatcher.
NestedScrollConnection
offre 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 veloce:
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 sul delta propagato:
delta available per quella particolare fase e delta consumed consumato nelle
fasi precedenti. Se in qualsiasi momento vuoi interrompere la propagazione dei delta verso l'alto 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 tipo NestedScrollSource.
NestedScrollDispatcher
inizializza il ciclo di scorrimento nidificato. L'utilizzo di un dispatcher e la chiamata dei relativi metodi attiva il ciclo. I container scorrevoli hanno un dispatcher integrato che invia al sistema i delta acquisiti durante i gesti. 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 ai delta già esistenti anziché inviarne di nuovi.
Per altri utilizzi, consulta NestedScrollDispatcherSample.
Ridimensiona un'immagine durante lo scorrimento
Man mano che l'utente scorre, puoi creare un effetto visivo dinamico in cui l'immagine cambia dimensioni in base alla posizione di scorrimento.
Ridimensiona un'immagine in base alla posizione di scorrimento
Questo snippet mostra come ridimensionare un'immagine all'interno di un LazyColumn in base alla posizione di scorrimento verticale. L'immagine si rimpicciolisce quando l'utente scorre verso il basso e si ingrandisce quando scorre verso l'alto, rimanendo entro i limiti di dimensioni minime e massime 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 variazione delle dimensioni dell'immagine in base al delta di scorrimento.- La variabile di stato
currentImageSizememorizza le dimensioni correnti dell'immagine, vincolate traminImageSizeemaxImageSize. imageScale, derivate dacurrentImageSize. LazyColumncompensa in base acurrentImageSize.Imageutilizza un modificatoregraphicsLayerper applicare la scala calcolata.- La classe
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:
Interoperabilità dello scorrimento nidificato
Quando provi a nidificare elementi View scorrevoli in composabili 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 è dovuto alle aspettative integrate nei componibili scorrevoli.
I componibili scorrevoli hanno una regola "scorrimento nidificato per impostazione predefinita", il che significa che qualsiasi contenitore scorrevole deve partecipare alla catena di scorrimento nidificato, sia come elemento principale tramite NestedScrollConnection sia come elemento secondario tramite NestedScrollDispatcher.
Il componente secondario attiverà quindi uno scorrimento nidificato per il componente principale quando il componente 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 a quello principale non è possibile.
Per abilitare l'API di interoperabilità dello scorrimento nidificato tra elementi View scorrevoli e componibili scorrevoli, nidificati in entrambe le direzioni, puoi utilizzare l'API di interoperabilità dello scorrimento nidificato per mitigare questi problemi nei seguenti scenari.
Un genitore cooperante View contenente un figlio ComposeView
Un elemento principale cooperante View è un elemento che implementa già
NestedScrollingParent3
e pertanto è in grado di ricevere delta di scorrimento da un elemento secondario nidificato componibile cooperante. In questo caso, ComposeView agirebbe come elemento secondario e dovrebbe implementare (indirettamente) NestedScrollingChild3.
Un esempio di genitore cooperante è
androidx.coordinatorlayout.widget.CoordinatorLayout.
Se hai bisogno dell'interoperabilità dello scorrimento nidificato tra i container principali View scorrevoli e i componibili secondari scorrevoli nidificati, puoi utilizzare rememberNestedScrollInteropConnection().
rememberNestedScrollInteropConnection()
consente e memorizza la
NestedScrollConnection
che consente l'interoperabilità dello scorrimento nidificato tra un elemento principale View che
implementa
NestedScrollingParent3
e un elemento secondario di Compose. Deve essere utilizzato insieme a un modificatore nestedScroll. 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 i componibili.
Un caso d'uso frequente è l'utilizzo di CoordinatorLayout, CollapsingToolbarLayout e
un composable secondario, 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>
Nella tua Activity o nel tuo Fragment, devi configurare il composable secondario e l'oggetto
NestedScrollConnection
richiesto:
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 secondario AndroidView
Questo scenario riguarda l'implementazione dell'API di interoperabilità dello scorrimento nidificato sul lato Compose, quando hai un composable principale contenente un AndroidView secondario. AndroidView implementa
NestedScrollDispatcher,
poiché agisce come elemento secondario di un elemento principale di scorrimento di Compose, nonché
NestedScrollingParent3
, poiché agisce come elemento principale di un elemento secondario di scorrimento View. Il componente principale di Compose potrà quindi ricevere delta di scorrimento nidificati da un componente secondario scorrevole nidificato View.
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 viene utilizzata l'API di interoperabilità dello scorrimento nidificato con
BottomSheetDialogFragment
per ottenere un comportamento di trascinamento e chiusura efficace:
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 dei delta dal livello Compose al livello View. In questo modo, l'elemento può partecipare allo scorrimento nidificato, ma lo scorrimento degli elementi non viene attivato automaticamente. Per i composabili che non sono scorrevoli automaticamente, come Box o Column, i delta di scorrimento su questi componenti non verranno propagati nel sistema di scorrimento nidificato e non raggiungeranno il NestedScrollConnection fornito da rememberNestedScrollInteropConnection(), pertanto non raggiungeranno il componente View principale. Per risolvere il problema, assicurati di impostare anche i modificatori di scorrimento su questi tipi di elementi componibili nidificati. Per informazioni più dettagliate, puoi consultare la sezione precedente sullo scorrimento nidificato.
Un elemento principale non cooperante View contenente un elemento secondario ComposeView
Una View non cooperante è una View che non implementa le interfacce NestedScrolling necessarie sul lato View. Tieni presente che ciò significa che l'interoperabilità dello scorrimento nidificato con questi Views non funziona immediatamente. I Views non cooperanti sono RecyclerView e ViewPager2.
Risorse aggiuntive
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Informazioni sui gesti
- Esegui la migrazione di
CoordinatorLayouta Compose - Utilizzo delle visualizzazioni in Compose