Dans Jetpack Compose, scrollable2D et draggable2D sont des modificateurs de bas niveau conçus pour gérer l'entrée du pointeur en deux dimensions. Alors que les modificateurs 1D standards scrollable et draggable sont limités à une seule orientation, les variantes 2D suivent les mouvements sur les axes X et Y simultanément.
Par exemple, le modificateur scrollable existant est utilisé pour le défilement et le balayage à une seule orientation, tandis que scrollable2d est utilisé pour le défilement et le balayage en 2D. Cela vous permet de créer des mises en page plus complexes qui se déplacent dans toutes les directions, comme des feuilles de calcul ou des visionneuses d'images. Le modificateur scrollable2d est également compatible avec le défilement imbriqué dans les scénarios 2D.
Sélectionnez scrollable2D ou draggable2D.
Le choix de la bonne API dépend des éléments d'interface utilisateur que vous souhaitez déplacer et du comportement physique préféré pour ces éléments.
Modifier.scrollable2D : utilisez ce modificateur sur un conteneur pour déplacer du contenu à l'intérieur. Par exemple, utilisez-le avec des cartes, des feuilles de calcul ou des visionneuses de photos, où le contenu du conteneur doit défiler dans les deux sens (horizontal et vertical). Il inclut une prise en charge intégrée du défilement rapide, de sorte que le contenu continue de défiler après un balayage, et il se coordonne avec les autres composants de défilement de la page.
Modifier.draggable2D : utilisez ce modificateur pour déplacer un composant lui-même. Il s'agit d'un modificateur léger, de sorte que le mouvement s'arrête exactement lorsque le doigt de l'utilisateur s'arrête. Il n'inclut pas la prise en charge de la diffusion.
Si vous souhaitez rendre un composant déplaçable, mais que vous n'avez pas besoin de la prise en charge du défilement rapide ou imbriqué, utilisez draggable2D.
Implémenter des modificateurs 2D
Les sections suivantes fournissent des exemples d'utilisation des modificateurs 2D.
Implémenter Modifier.scrollable2D
Utilisez ce modificateur pour les conteneurs dans lesquels l'utilisateur doit déplacer du contenu dans toutes les directions.
Capturer des données de mouvement 2D
Cet exemple montre comment capturer des données brutes de mouvement 2D et afficher le décalage X,Y :
@Composable private fun Scrollable2DSample() { // 1. Manually track the total distance the user has moved in both X and Y directions var offset by remember { mutableStateOf(Offset.Zero) } Box( modifier = Modifier .fillMaxSize() // ... contentAlignment = Alignment.Center ) { Box( modifier = Modifier .size(200.dp) // 2. Attach the 2D scroll logic to capture XY movement deltas .scrollable2D( state = rememberScrollable2DState { delta -> // 3. Update the cumulative offset state with the new movement delta offset += delta // Return the delta to indicate the entire movement was handled by this box delta } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { // 4. Display the current X and Y values from the offset state in real-time Text( text = "X: ${offset.x.roundToInt()}", // ... ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "Y: ${offset.y.roundToInt()}", // ... ) } } } }
L'extrait précédent effectue les opérations suivantes :
- Utilise
offsetcomme état qui contient la distance totale parcourue par l'utilisateur. - Dans
rememberScrollable2DState, une fonction lambda est définie pour gérer chaque delta généré par le doigt de l'utilisateur. Le codeoffset.value += deltamet à jour l'état manuel avec la nouvelle position. - Les composants
Textaffichent les valeurs X et Y actuelles de cet étatoffset, qui sont mises à jour en temps réel lorsque l'utilisateur fait glisser l'élément.
Faire un panoramique dans une grande fenêtre d'affichage
Cet exemple montre comment utiliser les données de défilement 2D capturées et appliquer un translationX et un translationY à un contenu plus grand que son conteneur parent :
@Composable private fun Panning2DImage() { // Manually track the total distance the user has moved in both X and Y directions val offset = remember { mutableStateOf(Offset.Zero) } // Define how gestures are captured. The lambda is called for every finger movement val scrollState = rememberScrollable2DState { delta -> offset.value += delta delta } // The Viewport (Container): A fixed-size box that acts as a window into the larger content Box( modifier = Modifier .size(600.dp, 400.dp) // The visible area dimensions // ... // Hide any parts of the large content that sit outside this container's boundaries .clipToBounds() // Apply the 2D scroll modifier to intercept touch and fling gestures in all directions .scrollable2D(state = scrollState), contentAlignment = Alignment.Center, ) { // The Content: An image given a much larger size than the container viewport Image( painter = painterResource(R.drawable.cheese_5), contentDescription = null, modifier = Modifier .requiredSize(1200.dp, 800.dp) // Manual Scroll Effect: Since scrollable2D doesn't move content automatically, // we use graphicsLayer to shift the drawing position based on the tracked offset. .graphicsLayer { translationX = offset.value.x translationY = offset.value.y }, contentScale = ContentScale.FillBounds ) } }
Modifier.scrollable2D.Modifier.scrollable2D.L'extrait de code précédent inclut les éléments suivants :
- Le conteneur est défini sur une taille fixe (
600x400dp), tandis que le contenu est défini sur une taille beaucoup plus grande (1200x800dp) pour éviter qu'il ne soit redimensionné à la taille de son parent. - Le modificateur
clipToBounds()sur le conteneur garantit que toute partie du grand contenu qui se trouve en dehors de la boîte600x400est masquée. - Contrairement aux composants de haut niveau tels que
LazyColumn,scrollable2Dne déplace pas automatiquement le contenu pour vous. Vous devez plutôt appliquer leoffsetsuivi à votre contenu, soit à l'aide de transformationsgraphicsLayer, soit à l'aide de décalages de mise en page. - Dans le bloc
graphicsLayer,translationX = offset.value.xettranslationY = offset.value.ydéplacent la position de dessin de l'image ou du texte en fonction du mouvement de votre doigt, ce qui crée l'effet visuel de défilement.
Implémenter le défilement imbriqué avec scrollable2D
Cet exemple montre comment un composant bidirectionnel peut être intégré à un parent unidimensionnel standard, comme un flux d'actualités vertical.
Voici quelques points à garder à l'esprit lorsque vous implémentez le défilement imbriqué :
- Le lambda pour
rememberScrollable2DStatene doit renvoyer que le delta consommé, pour permettre à la liste parente de prendre le relais naturellement lorsque l'enfant atteint sa limite. - Lorsqu'un utilisateur effectue un balayage diagonal, la vitesse 2D est partagée. Si l'enfant atteint une limite pendant l'animation, l'élan restant est propagé au parent pour que le défilement se poursuive naturellement.
@Composable private fun NestedScrollable2DSample() { var offset by remember { mutableStateOf(Offset.Zero) } val maxScrollDp = 250.dp val maxScrollPx = with(LocalDensity.current) { maxScrollDp.toPx() } Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .background(Color(0xFFF5F5F5)), horizontalAlignment = Alignment.CenterHorizontally ) { Text( "Scroll down to find the 2D Box", modifier = Modifier.padding(top = 100.dp, bottom = 500.dp), style = TextStyle(fontSize = 18.sp, color = Color.Gray) ) // The Child: A 2D scrollable box with nested scroll coordination Box( modifier = Modifier .size(250.dp) .scrollable2D( state = rememberScrollable2DState { delta -> val oldOffset = offset // Calculate new potential offset and clamp it to our boundaries val newX = (oldOffset.x + delta.x).coerceIn(-maxScrollPx, maxScrollPx) val newY = (oldOffset.y + delta.y).coerceIn(-maxScrollPx, maxScrollPx) val newOffset = Offset(newX, newY) // Calculate exactly how much was consumed by the child val consumed = newOffset - oldOffset offset = newOffset // IMPORTANT: Return ONLY the consumed delta. // The remaining (unconsumed) delta propagates to the parent Column. consumed } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { val density = LocalDensity.current Text("2D Panning Zone", color = Color.White.copy(alpha = 0.7f), fontSize = 12.sp) Spacer(Modifier.height(8.dp)) Text("X: ${with(density) { offset.x.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) Text("Y: ${with(density) { offset.y.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) } } Text( "Once the Purple Box hits Y: 250 or -250,\nthis parent list will take over the vertical scroll.", textAlign = TextAlign.Center, modifier = Modifier.padding(top = 40.dp, bottom = 800.dp), style = TextStyle(fontSize = 14.sp, color = Color.Gray) ) } }
Dans l'extrait précédent :
- Le composant 2D peut consommer le mouvement de l'axe X pour effectuer un panoramique en interne tout en distribuant simultanément le mouvement de l'axe Y à la liste parente une fois que les propres limites verticales de l'enfant sont atteintes.
- Au lieu de piéger l'utilisateur dans la surface 2D, le système calcule le delta consommé et transmet le reste à la hiérarchie. Cela permet à l'utilisateur de continuer à faire défiler le reste de la page sans lever le doigt.
Implémenter Modifier.draggable2D
Utilisez le modificateur draggable2D pour déplacer des éléments d'interface utilisateur individuels.
Faire glisser un élément composable
Cet exemple illustre le cas d'utilisation le plus courant pour 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) } } }
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éplacer la position du composant en fonction des deltas de déplacement. - Comme il n'y a pas de prise en charge du geste de 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 déplacement du parent
Cet exemple montre comment utiliser draggable2D pour créer une zone de saisie 2D dans laquelle un sélecteur 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" 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) } ) ) } }
L'extrait de code 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 déplacement.