Faire défiler

Modificateurs Scroll

Les modificateurs verticalScroll et horizontalScroll sont la solution la plus simple pour autoriser l'utilisateur à faire défiler un élément lorsque les limites de son contenu dépassent les contraintes de taille maximales. Avec les modificateurs verticalScroll et horizontalScroll, vous n'avez pas besoin de décaler le contenu ni d'effectuer une translation.

@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))
        }
    }
}

Liste verticale simple réagissant à un défilement
gestes

ScrollState vous permet de modifier la position de défilement ou d'obtenir son état actuel. Pour le créer avec des paramètres par défaut, utilisez 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))
        }
    }
}

Modificateur Scrollable

La scrollable diffère des modificateurs de défilement dans la mesure où scrollable détecte la gestes de défilement et capture les deltas, mais ne décale pas son contenu automatiquement. Celui-ci est délégué à l'utilisateur via ScrollableState , nécessaire au bon fonctionnement de ce modificateur.

Lorsque vous créez ScrollableState, vous devez fournir un consumeScrollDelta. qui sera appelée à chaque étape de défilement (par saisie gestuelle, (défilement ou glissement d'un geste vif) avec le delta en pixels. Cette fonction doit renvoyer la la distance de défilement consommée, pour garantir que l'événement propagée dans les cas où des éléments imbriqués ont le scrollable modificateur.

L'extrait de code suivant détecte les gestes et affiche une valeur numérique correspondant à un décalage, mais ne décale aucun élément :

@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())
    }
}

Élément d'interface utilisateur qui détecte la pression du doigt et affiche la valeur numérique de
du doigt
lieu

Défilement imbriqué

Le défilement imbriqué est un système dans lequel plusieurs composants de défilement contiennent interagissent entre eux en réagissant à un seul geste de défilement. communiquer leurs deltas de défilement (modifications).

Le système de défilement imbriqué permet de coordonner entre les composants qui sont les éléments à faire défiler et les liens hiérarchiques (le plus souvent en partageant le même parent). Ce système associe les conteneurs à faire défiler et permet d'interagir avec le défilement deltas qui sont propagés et partagés entre.

Compose propose plusieurs façons de gérer le défilement imbriqué entre les composables. Un exemple typique de défilement imbriqué est une liste à l'intérieur d'une autre liste, et un bouton plus une demande complexe est une réplication barre d'outils.

Défilement imbriqué automatique

Le défilement imbriqué simple ne nécessite aucune action de votre part. Les gestes qui déclenchent une action de défilement sont automatiquement propagés depuis les enfants vers les parents. Ainsi, lorsque l'enfant ne peut plus faire défiler la page, le geste est géré par son élément parent.

Le défilement imbriqué automatique est pris en charge et fourni immédiatement par certains des Composants et modificateurs de Compose: verticalScroll, horizontalScroll, scrollable, Lazy API et TextField. Cela signifie que lorsque l'utilisateur fait défiler une page interne enfant des composants imbriqués, les modificateurs précédents propagent le défilement deltas aux parents qui prennent en charge le défilement imbriqué.

L'exemple suivant montre des éléments avec verticalScroll un modificateur qui leur est appliqué dans un conteneur qui comporte également un élément verticalScroll. ou modificateur qui lui est appliqué.

@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)
                    )
                }
            }
        }
    }
}

Deux éléments d'interface utilisateur à défilement vertical imbriqués, réagissant aux gestes à l'intérieur et
en dehors de l'intérieur
élément

Utiliser le modificateur nestedScroll

Si vous devez créer un défilement coordonné plus complexe entre plusieurs éléments, le modificateur nestedScroll vous offre plus de flexibilité en définissant une hiérarchie de défilements imbriqués. En tant que mentionné dans la section précédente, certains composants intègrent des fonctionnalités de l'assistance. Toutefois, pour les composables qu'il n'est pas possible de faire défiler automatiquement, comme Box ou Column, les deltas de défilement sur ces composants ne se propagent pas dans le de défilement imbriqué et les deltas n'atteindront pas NestedScrollConnection ni le composant parent. Pour résoudre ce problème, vous pouvez utiliser nestedScroll pour transmettre ces la prise en charge d'autres composants, y compris les composants personnalisés.

Cycle de défilement imbriqué

Le cycle de défilement imbriqué est le flux de deltas de défilement qui sont répartis vers le haut et vers le bas. l'arborescence de l'arborescence à travers tous les composants (ou nœuds) qui font partie un système de défilement, par exemple à l'aide de composants et de modificateurs à défilement ; ou nestedScroll

Phases du cycle de défilement imbriqué

Lorsqu'un événement déclencheur (un geste, par exemple) est détecté par un élément , avant même que l'action de défilement ne soit déclenchée, le code les deltas sont envoyés au système de défilement imbriqué et passent par trois phases: le prédéfilement, la consommation de nœuds et le post-défilement.

Phases du défilement imbriqué
à vélo

Au cours de la première phase de prédéfilement, le composant qui a reçu l'événement déclencheur les deltas acheminent ces événements vers le sommet le plus élevé, parent. Les événements delta s'affichent alors vers le bas, ce qui signifie que les deltas sont se propage du parent le plus proche de la racine vers l'enfant qui a démarré le et le cycle de défilement imbriqué.

Phase de pré-défilement : coordination
vers le haut

Cela donne aux parents de défilement imbriqués (composables utilisant nestedScroll ou modificateurs à défilement), la possibilité d'effectuer une action avec le delta avant la le nœud lui-même peut le consommer.

Phase de pré-défilement : ébullition
vers le bas

Dans la phase de consommation de nœuds, le nœud lui-même utilise le delta qui n'a pas été utilisés par ses parents. C'est à ce moment-là que le mouvement de défilement est réellement terminé et qu'il visible.

Consommation des nœuds
phase

Pendant cette phase, l'enfant peut choisir de consommer tout ou partie des faire défiler. Les éléments restants seront renvoyés vers le haut pour passer par la phase post-défilement.

Enfin, dans la phase post-défilement, tout ce que le nœud lui-même ne consomme pas sont à nouveau envoyés à ses ancêtres pour utilisation.

Phase post-défilement : coordination
vers le haut

La phase post-défilement fonctionne de la même manière que la phase de prédéfilement, où n'importe quelle des parents peuvent choisir de consommer ou non.

Phase post-défilement : ébullition
vers le bas

Comme pour le défilement, lorsqu'un geste de glissement se termine, l'intention de l'utilisateur peut être est traduite en une vitesse qui sert à faire glisser l'écran (défilement à l'aide d'une animation) conteneur déroulant. Le glissement d'un geste vif fait également partie du cycle de défilement imbriqué, et les vitesses générées par l'événement de glissement passent par des phases similaires: pré-glissement, la consommation des nœuds et le post-glissement. Notez que l'animation de glissement n'est associée à l'aide d'un geste tactile et qui ne sera pas déclenché par d'autres événements tels que l'accessibilité ou le défilement matériel.

Participer au cycle de défilement imbriqué

La participation au cycle implique d’intercepter, de consommer et de signaler la consommation de deltas le long de la hiérarchie. Compose fournit un ensemble d'outils influencent le fonctionnement du système de défilement imbriqué et la façon d'interagir directement par exemple, lorsque vous devez effectuer quelque chose avec les deltas de défilement avant un composant déroulant commence même à faire défiler.

Si le cycle de défilement imbriqué est un système agissant sur une chaîne de nœuds, nestedScroll est un moyen d'intercepter et d'insérer ces modifications. influencer les données (deltas de défilement) qui se propagent dans la chaîne. Ce peut être placé n'importe où dans la hiérarchie et communique avec des instances du modificateur de défilement imbriqué dans l'arborescence pour pouvoir partager des informations cette chaîne. Les éléments de base de ce modificateur sont NestedScrollConnection et NestedScrollDispatcher.

NestedScrollConnection permet de répondre aux phases du cycle de défilement imbriqué et influence le système de défilement imbriqué. Il est composé de quatre méthodes de rappel, chacune représentant l'une des phases de consommation (pré/post-défilement et pré/post-glissement) :

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
    }
}

Chaque rappel fournit également des informations sur le delta en cours de propagation: Delta de available pour cette phase particulière et delta de consumed consommé dans des phases précédentes. Si vous voulez arrêter de propager des deltas à un moment donné vous pouvez utiliser la connexion de défilement imbriqué pour ce faire:

val disabledNestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            return if (source == NestedScrollSource.SideEffect) {
                available
            } else {
                Offset.Zero
            }
        }
    }
}

Tous les rappels fournissent des informations sur le NestedScrollSource de mots clés.

NestedScrollDispatcher initialise le cycle de défilement imbriqué. Utiliser un coordinateur et appeler ses méthodes déclenche le cycle. Les conteneurs à défilement s'accompagnent d'un coordinateur intégré qui envoie deltas capturés lors des gestes dans le système. C'est pourquoi la plupart des cas d'utilisation la personnalisation du défilement imbriqué implique l'utilisation de NestedScrollConnection à la place d'un coordinateur, pour réagir aux deltas existants plutôt que d'en envoyer de nouveaux. Voir NestedScrollDispatcherSample pour d'autres utilisations.

Interopérabilité du défilement imbriqué

Lorsque vous essayez d'imbriquer des éléments View à défilement dans des composables à défilement, ou l'inverse, vous pourriez rencontrer des problèmes. Les plus perceptibles se produit lorsque vous faites défiler l'élément enfant et que vous atteignez les limites de début ou de fin et que au parent de reprendre le défilement. Toutefois, ce comportement attendu est soit risque de ne pas se produire ou de ne pas fonctionner comme prévu.

Ce problème est dû aux attentes que les développeurs fondent sur les composables à défilement. Les composables à défilement disposent d'une règle "nested-scroll-by-default", qui signifie que tout conteneur à défilement doit participer à la chaîne de défilement imbriquée, à la fois en tant que parent via NestedScrollConnection et en tant qu'enfant via NestedScrollDispatcher. Une fois sa limite atteinte, l'enfant doit déclencher un défilement imbriqué pour l'élément parent. Par exemple, cette règle permet aux composants Pager et LazyRow de Compose de fonctionner correctement ensemble. Toutefois, lorsque l'interopérabilité du défilement est assurée par ViewPager2 ou RecyclerView, comme ces composants n'implémentent pas NestedScrollingParent3, le défilement continu de l'enfant vers le parent est impossible.

Pour permettre l'interopérabilité du défilement imbriqué entre des éléments View à défilement et des composables à défilement, imbriqués dans les deux sens, vous pouvez utiliser l'API dédiée. Cela permettra de limiter ces problèmes dans les scénarios suivants.

Un View parent coopératif contenant un ComposeView enfant

Un View parent coopératif est déjà un élément qui implémente déjà NestedScrollingParent3 et est donc capable de recevoir des deltas de défilement d'un composable enfant. Dans ce cas, ComposeView agirait en tant qu'enfant. devez implémenter (indirectement) NestedScrollingChild3 Un exemple de parent coopérant est androidx.coordinatorlayout.widget.CoordinatorLayout

Pour assurer l'interopérabilité du défilement imbriqué entre des conteneurs parents View à défilement et des composables enfants imbriqués à défilement, vous pouvez utiliser rememberNestedScrollInteropConnection().

rememberNestedScrollInteropConnection() autorise et mémorise le NestedScrollConnection qui permet l'interopérabilité du défilement imbriqué entre un View parent implémentant NestedScrollingParent3 et un enfant Compose. Il doit être utilisé conjointement avec un modificateur nestedScroll. Étant donné que le défilement imbriqué est activé par défaut dans Compose, vous pouvez vous pouvez utiliser cette connexion pour activer le défilement imbriqué du côté View et ajouter la logique Glue nécessaire entre Views et les composables.

Les cas d'utilisation suivants sont fréquent : CoordinatorLayout, CollapsingToolbarLayout et un composable enfant, présenté dans cet exemple:

<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>

Dans votre activité ou fragment, vous devez configurer votre composable enfant et le composant obligatoire NestedScrollConnection:

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 composable parent contenant un AndroidView enfant

Ce scénario concerne l'implémentation de l'API d'interopérabilité du défilement imbriqué du côté de Compose, lorsqu'un composable parent contient un AndroidView enfant. Le composant AndroidView implémente NestedScrollDispatcher, car il agit en tant qu'enfant d'un parent à défilement Compose, et aussi NestedScrollingParent3, car il agit en tant que parent d'un enfant à défilement View. Le parent Compose va pouvoir recevoir des deltas de défilement imbriqué d'un enfant à défilement imbriqué View

L'exemple suivant montre comment assurer l'interopérabilité du défilement imbriqué dans ce dans ce scénario, ainsi qu'une barre d'outils pouvant être réduite 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) {
            // ...
        }
    }
    // ...
}

L'exemple suivant montre comment utiliser l'API avec un modificateur 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)
                    }
            }
        )
    }
}

Enfin, l'exemple suivant montre comment obtenir un comportement de type "faire glisser pour fermer la vue" en utilisant l'API d'interopérabilité du défilement imbriqué avec BottomSheetDialogFragment :

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
        }
    }
}

Notez que rememberNestedScrollInteropConnection() installera NestedScrollConnection dans l'élément auquel vous l'associez. NestedScrollConnection assure la transmission des deltas depuis le niveau Compose vers le niveau View. Cela permet l'élément de participer au défilement imbriqué, mais il ne permet pas défilement automatique des éléments. À des composables qu'il n'est pas possible de faire défiler automatiquement, comme Box ou Column, les deltas de défilement sur ces composants se propagent dans le système de défilement imbriqué et les deltas n'atteignent pas NestedScrollConnection fourni par rememberNestedScrollInteropConnection(), Par conséquent, ces deltas n'atteindront pas le composant View parent. Pour résoudre ce problème, assurez-vous de définir également les modificateurs de défilement sur ces types de composables. Vous pouvez vous reporter à la section précédente Imbriquée faites défiler la page pour afficher des informations.

Un View parent non coopératif contenant un ComposeView enfant

Un View non coopérant est une vue qui n'implémente pas les interfaces NestedScrolling nécessaires côté View. Cela signifie que l'interopérabilité du défilement imbriqué avec ces Views ne fonctionne pas directement. RecyclerView et ViewPager2 sont des exemples de Views non coopérants.