Modificatori di scorrimento
I modificatori
verticalScroll
e
horizontalScroll
offrono il modo più semplice per consentire all'utente di scorrere un elemento quando
i limiti dei suoi contenuti sono maggiori dei vincoli di dimensione massima. Con i modificatori verticalScroll
e horizontalScroll
non è necessario tradurre o compensare i contenuti.
@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)) } } }
ScrollState
consente di modificare la posizione di scorrimento o ottenere il suo stato attuale. Per crearlo
con i parametri predefiniti, utilizza
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)) } } }
Modificatore scorrevole
Il modificatore
scrollable
è diverso dai modificatori di scorrimento in quanto scrollable
rileva i
gesti di scorrimento e acquisisce i delta, ma non compensa automaticamente i contenuti. Questa operazione viene invece delegata all'utente tramite
ScrollableState
, che è necessario per il corretto funzionamento di questo modificatore.
Quando crei ScrollableState
, devi fornire una funzione consumeScrollDelta
che verrà richiamata a ogni passaggio di scorrimento (tramite input tramite gesto, scorrimento
fluido o scorrimento rapido) con il delta in pixel. Questa funzione deve restituire la
quantità di distanza di scorrimento consumata, per garantire che l'evento venga propagato correttamente nei casi in cui sono presenti elementi nidificati con il modificatore scrollable
.
Il seguente snippet rileva i gesti e mostra un valore numerico per un offset, ma non sposta alcun 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()) } }
Scorrimento nidificato
Lo scorrimento nidificato è un sistema in cui più componenti di scorrimento contenuti l'uno all'interno dell'altro funzionano insieme reagendo a un singolo gesto di scorrimento e comunicando le loro variazioni di scorrimento.
Il sistema di scorrimento nidificato consente il coordinamento tra i componenti scorrevoli e collegati gerarchicamente (il più delle volte condividendo lo stesso elemento principale). Questo sistema collega i contenitori scorrevoli e consente l'interazione con i delta di scorrimento che vengono propagati e condivisi tra i contenitori.
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 alcun intervento da parte tua. I gesti che avviano un'azione di scorrimento vengono propagati automaticamente dai figli ai genitori, in modo che quando il figlio non può scorrere ulteriormente, il gesto venga gestito dal suo elemento padre.
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
figlio interno di componenti nidificati, i modificatori precedenti propagano i delta di scorrimento
ai genitori che supportano lo scorrimento nidificato.
L'esempio seguente mostra elementi con un modificatore
verticalScroll
applicato al loro interno in un contenitore a cui è stato 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
ti offre maggiore flessibilità definendo una gerarchia di scorrimento nidificata. Come
menzionato nella sezione precedente, alcuni componenti supportano lo scorrimento
nidificato integrato. Tuttavia, per i composable che non sono scorrevoli automaticamente, come
Box
o Column
, i delta di scorrimento su questi componenti non si propagano nel
sistema di scorrimento nidificato e non raggiungono NestedScrollConnection
né
il componente principale. Per risolvere il problema, puoi utilizzare nestedScroll
per estendere questo supporto ad altri componenti, inclusi quelli personalizzati.
Ciclo di scorrimento nidificato
Il ciclo di scorrimento nidificato è il flusso di delta di scorrimento inviati su e giù
nell'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 trigger (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 attraversano tre fasi: pre-scorrimento, consumo dei nodi e post-scorrimento.
Nella prima fase di pre-scorrimento, il componente che ha ricevuto i delta dell'evento trigger li invierà verso l'alto, attraverso l'albero della gerarchia, fino al genitore di primo livello. Gli eventi delta verranno quindi propagati verso il basso, il che significa che i delta verranno propagati dalla radice principale verso l'elemento secondario che ha avviato il ciclo di scorrimento nidificato.
In questo modo, i genitori dello scorrimento nidificato (i composable che utilizzano nestedScroll
o
modificatori scorrevoli) hanno la possibilità di fare qualcosa con il delta prima che il
nodo stesso possa utilizzarlo.
Nella fase di consumo dei nodi, il nodo stesso utilizzerà il delta non utilizzato dai relativi nodi principali. Questo è il momento in cui il movimento di scorrimento viene effettivamente eseguito ed è visibile.
Durante questa fase, il ragazzo può scegliere di consumare tutto o parte dello scroll rimanente. Tutto ciò che rimane verrà inviato di nuovo per essere sottoposto alla fase post-scorrimento.
Infine, nella fase post-scorrimento, tutto ciò che il nodo stesso non ha consumato verrà inviato di nuovo ai suoi antenati per il consumo.
La fase post-scroll funziona in modo simile alla fase pre-scroll, in cui uno qualsiasi dei genitori può scegliere di 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 rapidamente (scorrere utilizzando un'animazione) il contenitore scorrevole. Lo scorrimento rapido fa parte anche del ciclo di scorrimento nidificato e le velocità generate dall'evento di trascinamento passano attraverso fasi simili: pre-scorrimento rapido, consumo dei nodi e post-scorrimento rapido. Tieni presente che l'animazione di scorrimento è associata solo al gesto di tocco e non viene attivata da altri eventi, come l'accessibilità o lo scorrimento hardware.
Partecipare al ciclo di scorrimento nidificato
La partecipazione al ciclo comporta l'intercettazione, il consumo e la segnalazione del consumo dei delta lungo la gerarchia. Compose fornisce un insieme di strumenti per influenzare il funzionamento del sistema di scorrimento nidificato e il modo di interagire direttamente con esso, ad esempio quando devi fare qualcosa 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) che vengono propagati nella catena. Questo
modificatore può essere posizionato ovunque nella gerarchia e comunica con
le istanze del modificatore di scorrimento nidificato verso l'alto dell'albero, in modo da poter condividere informazioni tramite
questo canale. Gli elementi costitutivi 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-trascinamento:
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:
il delta available
per quella fase specifica e il delta consumed
utilizzato nelle
fasi precedenti. Se in qualsiasi momento vuoi interrompere la propagazione dei delta 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 di
NestedScrollSource
.
NestedScrollDispatcher
inizializza il ciclo di scorrimento nidificato. L'utilizzo di un dispatcher e la chiamata dei relativi metodi
attivano il ciclo. I contenitori 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
.
Ridimensionare un'immagine durante lo scorrimento
Man mano che 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 il ridimensionamento di un'immagine all'interno di un LazyColumn
in base alla posizione di scorrimento verticale. L'immagine si rimpicciolisce man mano che l'utente scorre verso il basso e si ingrandisce
man mano che 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 del codice
- Questo codice utilizza un
NestedScrollConnection
per intercettare gli eventi di scorrimento. onPreScroll
calcola la variazione delle dimensioni dell'immagine in base allo scorrimento delta.- La variabile di stato
currentImageSize
memorizza le dimensioni attuali dell'immagine, vincolate traminImageSize
emaxImageSize. imageScale
, derivate dacurrentImageSize
. - I compensi
LazyColumn
si basano sucurrentImageSize
. Image
utilizza un modificatoregraphicsLayer
per applicare la scala calcolata.- Il
translationY
all'interno digraphicsLayer
assicura che l'immagine rimanga centrata verticalmente durante il ridimensionamento.
Risultato
Lo snippet precedente genera un effetto di scalabilità dell'immagine durante lo scorrimento:
Interoperabilità dello scorrimento annidato
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 iniziali o finali 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 contenitore scorrevole deve partecipare alla catena di scorrimento nidificato, sia come
elemento principale tramite
NestedScrollConnection
,
sia come elemento secondario tramite
NestedScrollDispatcher
.
Il componente secondario scorre in modo nidificato per il componente principale quando si trova
al limite. Ad esempio, questa regola consente a Compose Pager
e Compose LazyRow
di funzionare bene insieme. Tuttavia, quando lo scorrimento dell'interoperabilità viene eseguito
con ViewPager2
o RecyclerView
, poiché questi non implementano
NestedScrollingParent3
,
lo scorrimento continuo da figlio a genitore 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 genitore che collabora View
contenente un figlio ComposeView
Un genitore cooperativo View
è uno che implementa già
NestedScrollingParent3
e pertanto è in grado di ricevere delta di scorrimento da un elemento componibile figlio
nidificato cooperativo. ComposeView
fungerebbe da figlio in questo caso e dovrebbe
implementare (indirettamente)
NestedScrollingChild3
.
Un esempio di genitore collaborativo è
androidx.coordinatorlayout.widget.CoordinatorLayout
.
Se hai bisogno dell'interoperabilità dello scorrimento nidificato tra i contenitori View
principali scorrevoli e i composable secondari scorrevoli nidificati, puoi utilizzare rememberNestedScrollInteropConnection()
.
rememberNestedScrollInteropConnection()
consente e memorizza
NestedScrollConnection
che attiva l'interoperabilità dello scorrimento nidificato tra un elemento principale View
che
implementa
NestedScrollingParent3
e un elemento secondario Compose. Deve essere utilizzato insieme a un modificatore
nestedScroll
. Poiché lo scorrimento nidificato è attivato per impostazione predefinita sul lato Compose, puoi utilizzare questa connessione per attivare lo scorrimento nidificato sul lato View
e aggiungere la logica di collegamento necessaria tra Views
e i 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 figlio e il
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 composable secondario AndroidView
Questo scenario riguarda l'implementazione dell'API di interoperabilità dello scorrimento nidificato sul lato Compose, quando hai un composable principale contenente un composable secondario AndroidView
. AndroidView
implementa
NestedScrollDispatcher
,
in quanto funge da elemento secondario di un elemento principale di scorrimento di Compose, nonché
NestedScrollingParent3
, in quanto funge da elemento principale di un elemento secondario di scorrimento View
. Il componente padre potrà
quindi ricevere i 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(
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(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)
}
}
)
}
}
Infine, questo esempio mostra come viene utilizzata l'API di interoperabilità dello scorrimento nidificato con
BottomSheetDialogFragment
per ottenere un comportamento di trascinamento e chiusura riuscito:
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 differenze dal livello di composizione al livello View
. In questo modo
l'elemento può partecipare allo scorrimento nidificato, ma non
consente lo scorrimento automatico degli elementi. Per i composable non scorrevoli
automaticamente, come Box
o Column
, i delta di scorrimento su questi componenti non
si propagheranno nel sistema di scorrimento nidificato e i delta non raggiungeranno
NestedScrollConnection
fornito da rememberNestedScrollInteropConnection()
,
pertanto non raggiungeranno il componente View
principale. Per risolvere il problema,
assicurati di impostare anche i modificatori scorrevoli per questi tipi di
componenti componibili nidificati. Per informazioni più dettagliate, puoi fare riferimento alla sezione precedente sullo scorrimento
nidificato.
Un genitore non collaborativo View
contenente un bambino ComposeView
Una visualizzazione non cooperativa è una visualizzazione 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
- Eseguire la migrazione di
CoordinatorLayout
a Compose - Utilizzare le visualizzazioni in Scrivi