In Jetpack Compose sind scrollable2D und draggable2D Low-Level-Modifikatoren, die für die Verarbeitung von Zeigereingaben in zwei Dimensionen entwickelt wurden. Die standardmäßigen 1D-Modifikatoren scrollable und draggable sind auf eine einzelne Ausrichtung beschränkt, während die 2D-Varianten Bewegungen gleichzeitig auf der X- und der Y-Achse erfassen.
Der vorhandene Modifikator scrollable wird beispielsweise für das Scrollen und schnelle Wischen in einer Richtung verwendet, während scrollable2d für das Scrollen und schnelle Wischen in 2D verwendet wird. So können Sie komplexere Layouts erstellen, die sich in alle Richtungen bewegen, z. B. Tabellen oder Bildbetrachter. Der Modifikator scrollable2d unterstützt auch verschachteltes Scrollen in 2D-Szenarien.
Wählen Sie scrollable2D oder draggable2D aus.
Die Auswahl der richtigen API hängt von den UI-Elementen ab, die Sie verschieben möchten, und vom bevorzugten physischen Verhalten dieser Elemente.
Modifier.scrollable2D: Mit diesem Modifikator können Sie Inhalte in einem Container verschieben. Sie können es beispielsweise für Karten, Tabellen oder Bildbetrachter verwenden, bei denen der Inhalt des Containers sowohl horizontal als auch vertikal gescrollt werden muss. Sie bietet integrierte Unterstützung für das zügige Wischen, sodass sich der Inhalt nach einem Wischvorgang weiterbewegt. Außerdem wird sie mit anderen Scrollkomponenten auf der Seite koordiniert.
Modifier.draggable2D: Mit diesem Modifikator können Sie eine Komponente selbst verschieben. Es handelt sich um einen einfachen Modifikator, sodass die Bewegung genau dann endet, wenn der Finger des Nutzers stoppt. Fling wird nicht unterstützt.
Wenn Sie eine Komponente verschiebbar machen möchten, aber keine Unterstützung für Fling- oder verschachteltes Scrollen benötigen, verwenden Sie draggable2D.
2D-Modifikatoren implementieren
In den folgenden Abschnitten finden Sie Beispiele für die Verwendung der 2D-Modifikatoren.
Modifier.scrollable2D implementieren
Verwenden Sie diesen Modifikator für Container, in denen der Nutzer Inhalte in alle Richtungen verschieben muss.
2D-Bewegungsdaten erfassen
In diesem Beispiel wird gezeigt, wie Sie 2D-Rohdaten für Bewegungen erfassen und den X-,Y-Offset anzeigen:
@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()}", // ... ) } } } }
Das obige Snippet führt Folgendes aus:
- Verwendet
offsetals Status, der die Gesamtstrecke enthält, die der Nutzer gescrollt hat. - In
rememberScrollable2DStatewird eine Lambda-Funktion definiert, die jedes Delta verarbeitet, das durch den Finger des Nutzers generiert wird. Der Codeoffset.value += deltaaktualisiert den manuellen Status mit der neuen Position. - In den
Text-Komponenten werden die aktuellen X- und Y-Werte desoffset-Zustands angezeigt. Sie werden in Echtzeit aktualisiert, wenn der Nutzer das Element zieht.
Großen Darstellungsbereich schwenken
In diesem Beispiel wird gezeigt, wie erfasste scrollbare 2D-Daten verwendet und translationX und translationY auf Inhalte angewendet werden, die größer als der übergeordnete Container sind:
@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 erstellt wurde.Modifier.scrollable2D erstellt wurdeDas obige Snippet enthält Folgendes:
- Der Container hat eine feste Größe (
600x400dp), während der Inhalt eine viel größere Größe (1200x800dp) hat, damit er nicht auf die Größe des übergeordneten Elements skaliert wird. - Der Modifikator
clipToBounds()für den Container sorgt dafür, dass alle Teile des großen Inhalts, die sich außerhalb des600x400-Felds befinden, ausgeblendet werden. - Im Gegensatz zu Komponenten auf hoher Ebene wie
LazyColumnverschiebtscrollable2Dden Inhalt nicht automatisch. Stattdessen müssen Sie die verfolgtenoffsetauf Ihre Inhalte anwenden, entweder mitgraphicsLayer-Transformationen oder Layout-Offsets. - Im
graphicsLayer-Block verschiebentranslationX = offset.value.xundtranslationY = offset.value.ydie Zeichenposition des Bildes oder Textes entsprechend der Bewegung Ihres Fingers, wodurch der visuelle Effekt des Scrollens entsteht.
Verschachteltes Scrollen mit „scrollable2D“ implementieren
In diesem Beispiel wird gezeigt, wie eine bidirektionale Komponente in eine standardmäßige eindimensionale übergeordnete Komponente wie einen vertikalen Nachrichten-Feed eingebunden werden kann.
Beachten Sie bei der Implementierung von verschachteltem Scrollen die folgenden Punkte:
- Die Lambda-Funktion für
rememberScrollable2DStatesollte nur das verbrauchte Delta zurückgeben, damit die übergeordnete Liste automatisch übernommen wird, wenn das untergeordnete Element sein Limit erreicht. - Wenn ein Nutzer eine diagonale Wischbewegung ausführt, wird die 2D-Geschwindigkeit geteilt. Wenn das untergeordnete Element während der Animation an eine Grenze stößt, wird der verbleibende Impuls an das übergeordnete Element weitergegeben, damit der Bildlauf natürlich fortgesetzt wird.
@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) ) } }
Im vorherigen Snippet gilt Folgendes:
- Die 2D-Komponente kann Bewegungen auf der X-Achse nutzen, um intern zu schwenken, und gleichzeitig Bewegungen auf der Y-Achse an die übergeordnete Liste senden, sobald die eigenen vertikalen Grenzen des untergeordneten Elements erreicht sind.
- Anstatt den Nutzer auf der 2D-Oberfläche zu halten, berechnet das System das verbrauchte Delta und gibt den Rest an die nächsthöhere Ebene in der Hierarchie weiter. So kann der Nutzer auf der Seite weiter scrollen, ohne den Finger vom Display zu nehmen.
Modifier.draggable2D implementieren
Verwenden Sie den Modifikator draggable2D, um einzelne UI-Elemente zu verschieben.
Composable-Element ziehen
Dieses Beispiel zeigt den häufigsten Anwendungsfall für draggable2D: Ein Nutzer kann ein UI-Element auswählen und es an einer beliebigen Stelle in einem übergeordneten Container neu positionieren.
@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) } } }
Das obige Code-Snippet enthält Folgendes:
- Die Position des Felds wird mithilfe des Status
offsetverfolgt. - Verwendet den Modifikator
offset, um die Position der Komponente basierend auf den Drag-Deltas zu verschieben. - Da keine Fling-Unterstützung vorhanden ist, bleibt das Feld sofort stehen, wenn der Nutzer den Finger anhebt.
Untergeordnetes Composable-Element basierend auf dem Ziehbereich des übergeordneten Elements ziehen
In diesem Beispiel wird gezeigt, wie Sie mit draggable2D einen 2D-Eingabebereich erstellen, in dem ein Auswahlknopf auf eine bestimmte Oberfläche beschränkt ist. Im Gegensatz zum Beispiel mit dem ziehbaren Element, bei dem die Komponente selbst verschoben wird, werden in dieser Implementierung die 2D-Deltas verwendet, um einen untergeordneten zusammensetzbaren „Selektor“ in einer Farbauswahl zu verschieben:
@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) } ) ) } }
Das obige Snippet enthält Folgendes:
- Mit dem Modifikator
onSizeChangedwerden die tatsächlichen Abmessungen des Gradientencontainers erfasst. Der Selektor weiß genau, wo sich die Kanten befinden. - Im
graphicsLayerwerdentranslationXundtranslationYangepasst, damit der Selektor beim Ziehen zentriert bleibt.