Scorri

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 superiori ai limiti 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))
        }
    }
}

Un semplice elenco verticale che risponde ai gesti di scorrimento

ScrollState ti consente di modificare la posizione di scorrimento o di recuperare lo stato attuale. Per crearla 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 di scrollable è diverso da quelli di scorrimento perché scrollable rileva i gesti di scorrimento, ma non ne sposta i contenuti. Affinché questo modificatore funzioni correttamente, è necessario un valore ScrollableState. Quando crei ScrollableState devi fornire una funzione consumeScrollDelta che verrà richiamata a ogni passaggio di scorrimento (tramite input gesto, scorrimento fluido o scorrimento continuo) con il delta in pixel. Questa funzione deve restituire la quantità di distanza di scorrimento utilizzata, per garantire che l'evento venga propagato correttamente nei casi in cui sono presenti elementi nidificati che hanno il modificatore scrollable.

Lo snippet riportato di seguito rileva i gesti e mostra un valore numerico per l'offset, ma non sfalsa 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())
    }
}

Un elemento UI che rileva la pressione del dito e mostra il valore numerico della posizione del dito

Scorrimento nidificato

Compose supporta lo scorrimento nidificato,in cui più elementi reagiscono a un singolo gesto di scorrimento. Un esempio tipico di scorrimento nidificato è un elenco all'interno di un altro elenco, mentre un caso più complesso è costituito da una barra degli strumenti compressa.

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 dai bambini ai genitori. In questo modo, quando il bambino non può scorrere oltre, il gesto viene gestito dall'elemento principale.

Lo scorrimento nidificato automatico è supportato e fornito pronto all'uso da alcuni componenti e modificatori di Compose: verticalScroll, horizontalScroll, scrollable, le 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 hanno il supporto dello scorrimento nidificato.

L'esempio seguente mostra gli elementi a cui è applicato un modificatore verticalScroll 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)
                    )
                }
            }
        }
    }
}

Due elementi UI con scorrimento verticale nidificati, che rispondono ai gesti all'interno e all'esterno dell'elemento interno

Utilizzo del modificatore nestedScroll

Se hai bisogno di creare uno scorrimento coordinato avanzato tra più elementi, il modificatore di nestedScroll offre maggiore flessibilità definendo una gerarchia di scorrimento nidificato. Come accennato nella sezione precedente, alcuni componenti dispongono di un supporto dello scorrimento nidificato integrato. Tuttavia, per gli elementi componibili che non possono scorrere 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 fornire questo supporto ad altri componenti, inclusi i componenti personalizzati.

Interoperabilità dello scorrimento nidificato (a partire da Compose 1.2.0)

Quando provi a nidificare gli elementi View scorrevoli in componibili a scorrimento o viceversa, potresti riscontrare problemi. Quelli più evidenti si verificano quando scorri l'elemento secondario e raggiungi i limiti di inizio o fine e ti aspetti che lo scorrimento sia completato dal padre. Tuttavia, questo comportamento previsto potrebbe non verificarsi o non funzionare come previsto.

Questo problema è il risultato delle aspettative create negli elementi componibili scorrevoli. I componibili scorrevoli hanno una regola "nested-scroll-by-default", il che significa che qualsiasi container scorrevole deve partecipare alla catena di scorrimento nidificata, sia come padre tramite NestedScrollConnection sia come elemento secondario tramite NestedScrollDispatcher. L'asset secondario eseguirà quindi uno scorrimento nidificato per l'elemento padre quando quest'ultimo si trova al limite. Ad esempio, questa regola consente a Scrivi Pager e Scrivi LazyRow di funzionare bene insieme. Tuttavia, quando viene eseguito lo scorrimento dell'interoperabilità con ViewPager2 o RecyclerView, dato che queste non implementano NestedScrollingParent3, non è possibile lo scorrimento continuo da quello secondario all'elemento padre.

Per abilitare l'API di interoperabilità a scorrimento nidificata tra elementi View scorrevoli e componibili a scorrimento, nidificati in entrambe le direzioni, puoi utilizzare l'API di interoperabilità di scorrimento nidificata per mitigare questi problemi negli scenari seguenti.

Un padre che collabora con View e contiene un figlio ComposeView

Un elemento padre che collabora View è un elemento che implementa già NestedScrollingParent3 e pertanto può ricevere delta di scorrimento da un elemento componibile secondario nidificato che cooperano. In questo caso ComposeView agirebbe come figlio e dovrebbe implementare (indirettamente) NestedScrollingChild3. Un esempio di genitore che ha collaborato è androidx.coordinatorlayout.widget.CoordinatorLayout.

Se hai bisogno di un'interoperabilità dello scorrimento nidificata tra i container principali View scorrevoli e gli elementi componibili secondari nidificati, puoi utilizzare rememberNestedScrollInteropConnection().

rememberNestedScrollInteropConnection() consente e memorizza NestedScrollConnection che consente l'interoperabilità di scorrimento nidificata tra un elemento padre View che implementa NestedScrollingParent3 e un elemento secondario di Compose. Dovrebbe essere usato in combinazione con un modificatore di nestedScroll. Poiché lo scorrimento nidificato è abilitato per impostazione predefinita sul lato Scrivi, puoi utilizzare questa connessione per attivare sia lo scorrimento nidificato sul lato View sia aggiungere la logica necessaria per il colla tra Views e gli elementi componibili.

Un caso d'uso frequente utilizza CoordinatorLayout, CollapsingToolbarLayout e un elemento componibile 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>

In Attività o Frammento, devi configurare l'elemento componibile di tuo figlio e i requisiti NestedScrollConnection richiesti:

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 componibile principale contenente un elemento secondario AndroidView

Questo scenario riguarda l'implementazione dell'API di interoperabilità a scorrimento nidificata sul lato Scrivi, quando hai un componibile padre contenente un elemento figlio AndroidView. L'AndroidView implementa NestedScrollDispatcher, poiché agisce come elemento secondario per un elemento principale a scorrimento di Compose, nonché NestedScrollingParent3 , poiché agisce come padre per un elemento figlio che scorre View. L'elemento principale della scrittura potrà quindi ricevere i delta di scorrimento nidificati da un elemento figlio scorrevole nidificato View.

L'esempio seguente mostra come ottenere l'interoperabilità dello scorrimento nidificato in questo scenario, insieme a una barra degli strumenti di compressione 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 l'API di interoperabilità di scorrimento nidificata viene utilizzata con BottomSheetDialogFragment per ottenere un comportamento di trascinamento 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 dei delta dal livello di Compose al livello View. In questo modo l'elemento può partecipare allo scorrimento nidificato, ma non viene attivato automaticamente lo scorrimento degli elementi. Per gli elementi componibili che non possono essere scorretti 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 il valore NestedScrollConnection fornito da rememberNestedScrollInteropConnection(), pertanto non raggiungeranno il componente View principale. Per risolvere il problema, assicurati di impostare anche modificatori scorrevoli su questi tipi di elementi componibili nidificati. Per informazioni più dettagliate, consulta la sezione precedente relativa allo scorrimento nidificato.

Un elemento principale non cooperante, View, che contiene un account secondario ComposeView

Una vista che non collabora è un elemento 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 da subito. Views che non collaborano sono RecyclerView e ViewPager2.