Faire glisser des composables

Permettre aux utilisateurs de faire glisser physiquement des composants sur un écran est fondamental pour les applications Android modernes. Que vous créiez un curseur personnalisé, un composant "balayer pour fermer" ou un canevas interactif, Jetpack Compose fournit un ensemble complet d'API pour gérer facilement les événements de glissement.

Les API de glissement de Compose font abstraction de la complexité des événements tactiles bruts. Au lieu de calculer manuellement les coordonnées tactiles et de gérer les cycles de vie des gestes avec pointerInput, vous pouvez utiliser des modificateurs intégrés qui sont directement liés aux systèmes de gestion de l'état et d'animation de Compose.

Cette page explique comment et quand utiliser chaque API de glissement.

Choisir la bonne API de glissement

Vous pouvez utiliser plusieurs API pour implémenter du contenu pouvant être déplacé.

  • Choisissez des composants Material standards (comme SwipeToDismissBox ou des panneaux de navigation) lorsque : vous avez besoin du comportement de balayage ou de glissement Material 3 par défaut (par exemple, pour supprimer un e-mail, archiver une tâche ou ouvrir un menu latéral / des bottom sheets). Ils fournissent des comportements intégrés sans nécessiter de gestion personnalisée de l'état ou de la mise en page.
  • Choisissez Modifier.draggable lorsque : vous n'avez besoin que des deltas de glissement bruts (par exemple, pour faire glisser un curseur personnalisé). Pour en savoir plus, consultez la section Faire glisser le long d'un seul axe avec draggable.
    • Choisissez Modifier.draggable2D lorsque : vous avez besoin qu'un composant se déplace en continu sur les axes X et Y simultanément (par exemple, pour faire un panoramique sur une carte ou créer un bouton d'action flottant que l' utilisateur peut faire glisser n'importe où sur l'écran). Pour en savoir plus, consultez la section Faire glisser en deux dimensions avec Modifier.draggable2D.
  • Choisissez Modifier.anchoredDraggable lorsque : vous créez des composants interactifs personnalisés qui doivent s'ancrer entre deux positions discrètes ou plus (par exemple, un bottom sheet, un panneau de navigation ou une action de balayage pour révéler). Pour en savoir plus, consultez la section Faire glisser des composants entre des points d'ancrage avec anchoredDraggable.

Faire glisser le long d'un seul axe avec draggable

Le modificateur draggable est le point d'entrée de niveau supérieur des gestes de déplacement dans une orientation. Il indique également la distance de déplacement en pixels.

Notez que ce modificateur est semblable à scrollable, dans la mesure où il ne détecte que le geste. Vous devez conserver l'état et le représenter à l'écran par exemple en déplaçant l'élément à l'aide du offset modificateur :

@Composable
private fun DraggableText() {
    var offsetX by remember { mutableFloatStateOf(0f) }
    Text(
        modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .draggable(
                orientation = Orientation.Horizontal,
                state = rememberDraggableState { delta ->
                    offsetX += delta
                }
            ),
        text = "Drag me!"
    )
}

Si vous devez contrôler l'ensemble du geste de déplacement, utilisez plutôt le détecteur de gestes de déplacement, avec le modificateur pointerInput.

@Composable
private fun DraggableTextLowLevel() {
    Box(modifier = Modifier.fillMaxSize()) {
        var offsetX by remember { mutableFloatStateOf(0f) }
        var offsetY by remember { mutableFloatStateOf(0f) }

        Box(
            Modifier
                .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                .background(Color.Blue)
                .size(50.dp)
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consume()
                        offsetX += dragAmount.x
                        offsetY += dragAmount.y
                    }
                }
        )
    }
}

INSÉREZ DU TEXTE ALTERNATIF ICI
Figure 1. Élément d'interface utilisateur déplacé par un doigt

Faire glisser des composants entre des points d'ancrage avec anchoredDraggable

Le modificateur anchoredDraggable vous permet de faire glisser un composant entre un ensemble de positions prédéfinies appelées points d'ancrage. Lorsque l'utilisateur relâche le geste de glissement, le composant s'arrête à l'ancrage le plus proche. anchoredDraggable est utile pour créer des composants tels que des bottom sheets personnalisées, des curseurs complexes ou des panneaux de volet.

Surface d'API

L'API AnchoredDraggable se compose des parties principales suivantes :

  • Ancrages (DraggableAnchors) : générateur qui définit le mappage entre vos états personnalisés (par exemple, une classe enum pour Start, Center, End) et leurs valeurs de décalage en pixels correspondantes.
  • État (AnchoredDraggableState<T>) : conteneur d'état qui gère la position actuelle (décalage) du composant, l'ancrage sur lequel il est fixé et sa progression de glissement. Il doit être créé à l'aide de rememberSaveable et AnchoredDraggableState.Saver<T> pour conserver son état lors des modifications de configuration changes.
  • Modificateur de geste (Modifier.anchoredDraggable) : modificateur qui détecte les gestes de glissement le long d'une Orientation, met à jour l'état avec le delta de glissement et gère le geste de balayage pour animer le composant jusqu'à l'ancrage le plus proche lors de sa libération.

Exemple : Créer un composant SwipeToDismiss personnalisé

Cet exemple montre comment créer un composant SwipeToDismiss personnalisé (une bannière blanche) que l'utilisateur peut balayer vers la gauche pour le fermer ou faire glisser vers la droite pour voir un effet d'étirement de défilement hors limites.

  1. Définissez les valeurs d'ancrage. Définissez une énumération représentant les positions discrètes dans lesquelles le contenu déplaçable peut s'installer :

    private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }

  2. Instanciez et enregistrez AnchoredDraggableState. Utilisez rememberSaveable avec AnchoredDraggableState.Saver() pour conserver la position actuelle du composant lors de la recréation de l'activité et des modifications de configuration (par exemple, la rotation de l'écran ou les modifications de thème) :

    val state =
        rememberSaveable(saver = AnchoredDraggableState.Saver()) {
            AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting)
        }

  3. Appliquez les modificateurs layout, offset et draggable. Pour rendre le composant déplaçable et vous assurer que ses points d'ancrage s'adaptent à sa taille, appliquez les modificateurs suivants au contenu qui doit se déplacer visuellement :

    • Modifier.layout: les positions d'ancrage dépendent souvent de la taille d'un composant, qui n'est pas connue lors de la composition initiale. Utilisez le modificateur layout pour mesurer le composant, calculer la carte DraggableAnchors de manière dynamique en fonction de sa largeur et mettre à jour l'état à l'aide de state.updateAnchors().

    • Modifier.anchoredDraggable: se connecte à l'état, distribue les deltas de glissement dans l'orientation spécifiée et gère le geste de balayage pour animer le composant jusqu'à l'ancrage le plus proche lorsqu'il est libéré. Ajoutez ce modificateur au composant qui doit répondre aux gestes de glissement et transmettez lui le state et l'orientation du glissement (et, éventuellement, overscrollEffect).
    • Modifier.offset: lit le décalage de state.requireOffset() dans le lambda de décalage. La lecture du décalage dans ce lambda garantit que la valeur n'est lue que pendant la phase de placement, ce qui évite les recompositions inutiles pendant le glissement. Appliquez ce modificateur au contenu qui doit se déplacer visuellement.

    Box(
        modifier = Modifier
            .fillMaxWidth()
            // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase
            .offset { IntOffset(state.requireOffset().roundToInt(), 0) }
            .layout { measurable, constraints ->
                val placeable = measurable.measure(constraints)
                val anchors = DraggableAnchors {
                    AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat()
                    AnchoredDraggableSwipeDismissValue.Resting at 0f
                }
                state.updateAnchors(anchors)
                layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
            }
            // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll.
            .overscroll(overscrollEffect)
            .anchoredDraggable(
                state,
                Orientation.Horizontal,
                overscrollEffect = overscrollEffect
            )
    ) {
        dismissContent()
    }

  4. [Facultatif] Intégrez le défilement hors limites. L'API overscrollEffect implémente un effet visuel qui s'affiche lorsque les bords d'un conteneil de défilement sont atteints avec un défilement ou un balayage. Vous pouvez fournir un retour visuel lorsqu'un utilisateur fait glisser un élément au-delà des limites d'ancrage définies avec un effet de défilement hors limites (généralement un étirement ou une lueur).

    1. Distribuez : transmettez une instance overscrollEffect au anchoredDraggable modificateur. Cela distribue les deltas de glissement à l'état AnchoredDraggable ou à l'effet de défilement hors limites, selon que le geste se trouve à l'intérieur ou à l'extérieur des limites d'ancrage.
    2. Effectuez le rendu : appliquez Modifier.overscroll avec la même OverscrollEffect instance au composant qui doit afficher visuellement l'effet.

L'extrait suivant montre comment ajouter un effet d'étirement standard :

val overscrollEffect = rememberOverscrollEffect()
Box(
    modifier.fillMaxWidth()
) {
    Box(Modifier.fillMaxWidth()) {
        background()
    }
    Box(
        modifier = Modifier
            .fillMaxWidth()
            // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase
            .offset { IntOffset(state.requireOffset().roundToInt(), 0) }
            .layout { measurable, constraints ->
                val placeable = measurable.measure(constraints)
                val anchors = DraggableAnchors {
                    AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat()
                    AnchoredDraggableSwipeDismissValue.Resting at 0f
                }
                state.updateAnchors(anchors)
                layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
            }
            // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll.
            .overscroll(overscrollEffect)
            .anchoredDraggable(
                state,
                Orientation.Horizontal,
                overscrollEffect = overscrollEffect
            )
    ) {
        dismissContent()
    }
}

Résultat

L'extrait de code précédent génère une bannière blanche contenant le texte "Drag me to dismiss!" (Faites-moi glisser pour me fermer !) que l'utilisateur peut balayer vers la gauche pour la fermer (ou faire glisser vers la droite pour voir l'effet d'étirement de défilement) :

Vidéo 2. Implémentation d'un composant personnalisé "balayer pour fermer"

Exemple : Créer un volet de navigation personnalisé piloté par des gestes

Cet exemple montre comment créer un volet de navigation personnalisé piloté par des gestes. Lorsque l'utilisateur fait glisser l'écran d'avant-plan (ScreenContents) horizontalement, il glisse vers la droite, passe de 1.0f à 0.8f et arrondit ses angles pour révéler le volet de navigation sous-jacent.

  1. Définissez les états et les positions d'ancrage. Dans cet exemple, les ancrages sont les états ouvert et fermé du volet. Définissez une enum pour les états Open et Closed, et mappez-les sur des décalages en pixels à l'aide de DraggableAnchors :

    enum class DrawerState {
        Open,
        Closed
    }
    
        val anchors = DraggableAnchors {
            DrawerState.Open at drawerWidth
            DrawerState.Closed at 0f
        }
        val state = remember {
            AnchoredDraggableState(
                initialValue = DrawerState.Closed,
                anchors = anchors,
            )
        }

  2. Animez anchoredDraggableState lorsque l'utilisateur clique sur un élément. Placez HomeScreenDrawer en bas de Box et ScreenContents en haut. Le volet reste immobile, tandis que la couche supérieure se traduit et s'adapte par-dessus.

    Lorsqu'un utilisateur sélectionne un élément de volet, mettez à jour l'état de l'écran actif et fermez automatiquement le volet à l'aide de state.animateTo(DrawerState.Closed) :

    @Composable
    private fun AnchoredDraggableDrawerOnClickSample(
        modifier: Modifier = Modifier,
        state: AnchoredDraggableState<DrawerState>,
        coroutineScope: CoroutineScope
    ) {
        var screenState by remember { mutableStateOf(ScreenState.Home) }
    
        Box(modifier = modifier.fillMaxSize()) {
            HomeScreenDrawer(
                selectedScreen = screenState,
                onScreenSelected = { screen ->
                    screenState = screen
                    // Dismiss drawer when a destination is selected
                    coroutineScope.launch { state.animateTo(DrawerState.Closed) }
                }
            )
    
            // ScreenContents(...)
        }
    }

  3. Utilisez state.requireOffset() pour modifier plusieurs propriétés visuelles. Au lieu de décaler uniquement le contenu, vous pouvez modifier plusieurs propriétés avec Modifier.graphicsLayer pour animer simultanément la translation, l’échelle et le rayon d’angle en fonction de la progression du glissement :

    • Utilisez state.requireOffset() pour obtenir le décalage de glissement actuel.
    • Utilisez lerp() (interpolation linéaire) pour calculer l'échelle proportionnelle (1f à 0.8f) et le rayon d'angle (0.dp à 32.dp) par rapport au décalage et à la largeur du volet.
    • Modifier.graphicsLayer exécute les modifications lors de la phase de dessin, ce qui garantit des animations hautes performances sans déclencher de recompositions pendant le glissement.

    @Composable
    private fun AnchoredDraggableDrawerGraphicsLayerSample(
        state: AnchoredDraggableState<DrawerState>,
        drawerWidth: Float,
        selectedScreen: ScreenState,
        onDrawerClicked: () -> Unit
    ) {
        ScreenContents(
            selectedScreen = selectedScreen,
            onDrawerClicked = onDrawerClicked,
            modifier = Modifier
                .graphicsLayer {
                    this.translationX = state.requireOffset()
                    val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth)
                    this.scaleX = scale
                    this.scaleY = scale
                    val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth)
                    this.shape = RoundedCornerShape(cornerRadius.dp)
                    this.clip = true
                }
                .anchoredDraggable(state, Orientation.Horizontal)
        )
    }

  4. Déclenchez des animations programmatiques avec animateTo. Contrôlez le volet de manière programmatique (par exemple, lorsque vous appuyez sur l'icône de menu ou sélectionnez un élément de volet) en lançant state.animateTo() dans une portée de coroutine :

    fun toggleDrawerState(
        state: AnchoredDraggableState<DrawerState>,
        coroutineScope: CoroutineScope
    ) {
        coroutineScope.launch {
            if (state.currentValue == DrawerState.Open) {
                state.animateTo(DrawerState.Closed)
            } else {
                state.animateTo(DrawerState.Open)
            }
        }
    }

Résultat

Vidéo 3. L'écran de premier plan (Veille) est déplacé vers la droite, ce qui révèle le panneau de navigation sous-jacent.

Faire glisser en deux dimensions avec Modifier.draggable2D

Utilisez le modificateur draggable2D pour déplacer des éléments d'interface utilisateur individuels sur les axes X et Y simultanément.

Faire glisser un élément composable

Cet exemple montre le cas d'utilisation le plus courant de draggable2D, qui consiste à permettre à un utilisateur de sélectionner un élément d'interface utilisateur et de le repositionner n'importe où dans un conteneur parent.

@Composable
private fun DraggableComposableElement() {
    // 1. Track the position of the floating window
    var offset by remember { mutableStateOf(Offset.Zero) }

    Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) {
        Box(
            modifier = Modifier
                // 2. Apply the offset to the box's position
                .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
                // ...
                // 3. Attach the 2D drag logic
                .draggable2D(
                    state = rememberDraggable2DState { delta ->
                        // 4. Update the position based on the movement delta
                        offset += delta
                    }
                ),
            contentAlignment = Alignment.Center
        ) {
            Text("Video Preview", color = Color.White, fontSize = 12.sp)
        }
    }
}

Vidéo 4. Une petite boîte violette est repositionnée sur un arrière-plan gris, ce qui illustre le glissement 2D direct où l'élément cesse de bouger dès que l'utilisateur lève le doigt.

L'extrait de code précédent inclut les éléments suivants :

  • Suit la position de la boîte à l'aide d'un état offset.
  • Utilise le modificateur offset pour décaler la position du composant en fonction des deltas de glissement.
  • Comme il n'y a pas de prise en charge du balayage, la boîte cesse de bouger dès que l'utilisateur lève le doigt.

Faire glisser un composable enfant en fonction de la zone de glissement du parent

Cet exemple montre comment utiliser draggable2D pour créer une zone de saisie 2D dans laquelle un bouton de sélection est limité à une surface spécifique. Contrairement à l'exemple d'élément déplaçable, qui déplace le composant lui-même, cette implémentation utilise les deltas 2D pour déplacer un composable enfant "sélecteur" sur un sélecteur de couleurs :

@Composable
private fun ExampleColorSelector(
    // ...
)  {
    // 1. Maintain the 2D position of the selector in state.
    var selectorOffset by remember { mutableStateOf(Offset.Zero) }

    // 2. Track the size of the background container.
    var containerSize by remember { mutableStateOf(IntSize.Zero) }

    Box(
        modifier = Modifier
            .size(300.dp, 200.dp)
            // Capture the actual pixel dimensions of the container when it's laid out.
            .onSizeChanged { containerSize = it }
            .clip(RoundedCornerShape(12.dp))
            .background(
                brush = remember(hue) {
                    // Create a simple gradient representing Saturation and Value for the given Hue.
                    Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f)))
                }
            )
    ) {
        Box(
            modifier = Modifier
                .size(24.dp)
                .graphicsLayer {
                    // Center the selector on the finger by subtracting half its size.
                    translationX = selectorOffset.x - (24.dp.toPx() / 2)
                    translationY = selectorOffset.y - (24.dp.toPx() / 2)
                }
                // ...
                // 3. Configure 2D touch dragging.
                .draggable2D(
                    state = rememberDraggable2DState { delta ->
                        // 4. Calculate the new position and clamp it to the container bounds
                        val newX = (selectorOffset.x + delta.x)
                            .coerceIn(0f, containerSize.width.toFloat())
                        val newY = (selectorOffset.y + delta.y)
                            .coerceIn(0f, containerSize.height.toFloat())

                        selectorOffset = Offset(newX, newY)
                    }
                )
        )
    }
}

Vidéo 5. Dégradé de couleurs avec un bouton de sélection circulaire blanc qui peut être déplacé dans n'importe quelle direction, ce qui montre comment les deltas 2D sont limités aux limites du conteneur pour mettre à jour les valeurs de couleur sélectionnées.

L'extrait précédent inclut les éléments suivants :

  • Il utilise le modificateur onSizeChanged pour capturer les dimensions réelles du conteneur de dégradé. Le sélecteur sait exactement où se trouvent les bords.
  • Dans graphicsLayer, il ajuste translationX et translationY pour s'assurer que le sélecteur reste centré pendant le glissement.