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 de balayage pour ignorer ou un canevas interactif, Jetpack Compose fournit un ensemble complet d'API pour gérer facilement les événements de déplacement.

Les API de déplacement de Compose éliminent 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 d'état et d'animation de Compose.

Cette page explique comment et quand utiliser chaque API de déplacement.

Choisir l'API de déplacement appropriée

Vous pouvez utiliser plusieurs API pour implémenter du contenu déplaçable.

  • Choisissez des composants Material standards (comme SwipeToDismissBox ou des panneaux de navigation) lorsque vous avez besoin du comportement de balayage ou de déplacement par défaut de Material 3 (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 quand : vous n'avez besoin que de deltas de déplacement bruts (par exemple, pour faire glisser un curseur personnalisé). Pour en savoir plus, consultez Faire glisser le long d'un seul axe avec draggable.
  • Choisissez Modifier.anchoredDraggable quand : vous créez des composants interactifs personnalisés qui doivent s'ancrer entre deux positions distinctes ou plus (par exemple, une bottom sheet, un panneau de navigation ou une action de balayage pour afficher). Pour en savoir plus, consultez 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 via le modificateur offset :

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

INSERT ALT TEXT HERE
Figure 1. Élément d'interface utilisateur déplacé par un doigt

Faites 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 déplacement, le composant se positionne sur 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 type drawer.

Surface d'API

L'API AnchoredDraggable se compose des éléments principaux suivants :

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

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 des valeurs d'ancrage. Définissez une énumération représentant les positions discrètes que le contenu déplaçable peut adopter :

    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, rotation de l'écran ou changement 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, calculez la carte DraggableAnchors de manière dynamique en fonction de sa largeur et mettez à jour l'état à l'aide de state.updateAnchors().

    • Modifier.anchoredDraggable : se connecte à l'état, distribue les deltas de déplacement dans l'orientation spécifiée et gère le geste de balayage pour animer le composant vers l'ancrage le plus proche lorsqu'il est relâché. Ajoutez ce modificateur au composant qui doit répondre aux gestes de déplacement, puis transmettez-lui state et orientation (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 déplacement. 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 au-delà des limites. L'API overscrollEffect implémente un effet visuel qui s'affiche lorsque les bords d'un conteneur de défilement ont été atteints par 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. Dispatch : transmettez une instance overscrollEffect au modificateur anchoredDraggable. Cela distribue les deltas de déplacement à 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 de l'ancre.
    2. Affichage : appliquez le Modifier.overscroll avec la même instance OverscrollEffect au composant qui doit afficher visuellement l'effet.

L'extrait de code 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 avec le texte "Drag me to dismiss!" (Faites glisser pour ignorer). L'utilisateur peut balayer l'écran vers la gauche pour l'ignorer (ou vers la droite pour voir l'effet d'étirement du défilement excessif) :

Vidéo 2. Implémentation d'un composant personnalisé de balayage pour fermer la vue.

Exemple : Créer un panneau de navigation personnalisé basé sur les gestes

Cet exemple montre comment créer un panneau de navigation personnalisé basé sur les gestes. Lorsque l'utilisateur fait glisser son doigt horizontalement, l'écran de premier plan (ScreenContents) glisse vers la droite, passe de 1.0f à 0.8f et arrondit ses angles pour révéler le panneau de navigation sous-jacent.

  1. Définir les états et les positions des ancres Dans cet exemple, les ancres sont les états ouvert et fermé du panneau. Définissez une énumération pour les états Open et Closed, puis mappez-les aux décalages de 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. Animer anchoredDraggableState au clic sur un élément. Placez HomeScreenDrawer au bas de Box et ScreenContents en haut. Le panneau reste fixe, tandis que la couche supérieure se déplace et s'ajuste par-dessus.

    Lorsqu'un utilisateur sélectionne un élément du panneau, mettez à jour l'état de l'écran actif et fermez automatiquement le panneau à 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 simplement décaler 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 déplacement :

    • Utilisez state.requireOffset() pour obtenir le décalage de déplacement 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 panneau latéral.
    • 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 déplacement.

    @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 panneau de manière programmatique (par exemple, en appuyant sur l'icône de menu ou en sélectionnant un élément du panneau) en lançant state.animateTo() dans un champ d'application 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 (Sommeil) est déplacé vers la droite, ce qui affiche le panneau de navigation sous-jacent.

Faites glisser deux dimensions avec Modifier.draggable2D

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

Faire glisser un élément composable

Cet exemple illustre le cas d'utilisation le plus courant de draggable2D : 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 fond gris, illustrant le glissement 2D direct où l'élément s'arrête de bouger dès que l'utilisateur lève le doigt.

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

  • Suivre 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 déplacement.
  • Comme il n'y a pas de prise en charge du lancer, la boîte s'arrête de bouger dès que l'utilisateur lève le doigt.

Faire glisser un composable enfant en fonction de la zone de déplacement 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 "sélecteur" composable enfant dans un sélecteur de couleur :

@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, montrant comment les deltas 2D sont limités aux limites du conteneur pour mettre à jour les valeurs de couleur sélectionnées.

L'extrait de code 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 déplacement.