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
SwipeToDismissBoxou 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.draggablelorsque : 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.draggable2Dlorsque : 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 avecModifier.draggable2D.
- Choisissez
- Choisissez
Modifier.anchoredDraggablelorsque : 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 avecanchoredDraggable.
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 } } ) } }
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 pourStart, 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 derememberSaveableetAnchoredDraggableState.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'uneOrientation, 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.
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 }
Instanciez et enregistrez
AnchoredDraggableState. UtilisezrememberSaveableavecAnchoredDraggableState.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) }
Appliquez les modificateurs
layout,offsetetdraggable. 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 modificateurlayoutpour mesurer le composant, calculer la carteDraggableAnchorsde manière dynamique en fonction de sa largeur et mettre à jour l'état à l'aide destate.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 lestateet l'orientationdu glissement (et, éventuellement,overscrollEffect).Modifier.offset: lit le décalage destate.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() }
[Facultatif] Intégrez le défilement hors limites. L'API
overscrollEffectimplé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).- Distribuez : transmettez une instance
overscrollEffectauanchoredDraggablemodificateur. Cela distribue les deltas de glissement à l'étatAnchoredDraggableou à l'effet de défilement hors limites, selon que le geste se trouve à l'intérieur ou à l'extérieur des limites d'ancrage. - Effectuez le rendu : appliquez
Modifier.overscrollavec la mêmeOverscrollEffectinstance au composant qui doit afficher visuellement l'effet.
- Distribuez : transmettez une instance
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) :
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.
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
OpenetClosed, et mappez-les sur des décalages en pixels à l'aide deDraggableAnchors: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, ) }
Animez
anchoredDraggableStatelorsque l'utilisateur clique sur un élément. PlacezHomeScreenDraweren bas deBoxetScreenContentsen 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(...) } }
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 avecModifier.graphicsLayerpour 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.graphicsLayerexé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) ) }
- Utilisez
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çantstate.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
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) } } }
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
offsetpour 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) } ) ) } }
L'extrait précédent inclut les éléments suivants :
- Il utilise le modificateur
onSizeChangedpour capturer les dimensions réelles du conteneur de dégradé. Le sélecteur sait exactement où se trouvent les bords. - Dans
graphicsLayer, il ajustetranslationXettranslationYpour s'assurer que le sélecteur reste centré pendant le glissement.
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Comprendre les gestes
- Exemple d'animation avancée : Gestes {:#gesture-and-animation}
- Animations basées sur la valeur