Zweidimensionales Scrollen: scrollable2D, draggable2D

In Jetpack Compose sind scrollable2D und draggable2D Low-Level-Modifikatoren, die für die Verarbeitung von Zeigereingaben in zwei Dimensionen entwickelt wurden. Während die standardmäßigen 1D-Modifikatoren scrollable und draggable auf eine einzelne Ausrichtung beschränkt sind, verfolgen die 2D-Varianten Bewegungen gleichzeitig auf der X- und Y-Achse.

Der vorhandene scrollable-Modifikator wird beispielsweise für das Scrollen und Wischen in einer Richtung verwendet, während scrollable2d für das Scrollen und 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.

Abbildung 1. Eine Karte, die in zwei Richtungen geschwenkt wird.

Wählen Sie scrollable2D oder draggable2D aus.

Die Wahl der richtigen API hängt von den UI-Elementen ab, die Sie verschieben möchten, und vom bevorzugten physischen Verhalten dieser Elemente.

Modifier.scrollable2D: Verwenden Sie diesen Modifikator für einen Container, um Inhalte darin zu verschieben. Verwenden Sie es beispielsweise für Karten, Tabellen oder Fotobetrachter, bei denen der Inhalt des Containers sowohl horizontal als auch vertikal gescrollt werden muss. Sie bietet integrierte Unterstützung für das Wischen, sodass sich der Inhalt nach dem Wischen weiterbewegt, und sie wird mit anderen scrollbaren Komponenten auf der Seite koordiniert.

Modifier.draggable2D: Mit diesem Modifikator können Sie eine Komponente selbst verschieben. Es ist ein einfacher Modifikator, sodass die Bewegung genau dann stoppt, wenn der Finger des Nutzers stoppt. Fling wird nicht unterstützt.

Wenn Sie eine Komponente ziehbar 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 rohe 2D-Bewegungsdaten erfassen und den X,Y-Versatz 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()}",
                    // ...
                )
            }
        }
    }
}

Abbildung 2. Ein lila Feld, das die aktuellen X- und Y-Koordinatenverschiebungen verfolgt und anzeigt, während ein Nutzer den Mauszeiger über die Oberfläche zieht.

Das vorherige Snippet führt folgende Schritte aus:

  • Verwendet offset als Status, der die Gesamtstrecke enthält, die der Nutzer gescrollt hat.
  • In rememberScrollable2DState wird eine Lambda-Funktion definiert, um jedes Delta zu verarbeiten, das durch den Finger des Nutzers generiert wird. Der Code offset.value += delta aktualisiert den manuellen Status mit der neuen Position.
  • Die Text-Komponenten zeigen die aktuellen X- und Y-Werte des offset-Zustands an, die in Echtzeit aktualisiert werden, wenn der Nutzer zieht.

In einem großen Darstellungsbereich schwenken

In diesem Beispiel wird gezeigt, wie Sie erfasste scrollbare 2D-Daten verwenden und translationX und translationY auf Inhalte anwenden, 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
        )
    }
}

Abbildung 3. Ein bidirektionaler Schwenkbild-Darstellungsbereich, der mit Modifier.scrollable2D erstellt wurde.
Abbildung 4. Ein bidirektionaler Text-Viewport, der mit Modifier.scrollable2D erstellt wurde.

Das vorherige 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 an die Größe des übergeordneten Elements angepasst wird.
  • Der Modifikator clipToBounds() für den Container sorgt dafür, dass alle Teile des großen Inhalts, die sich außerhalb des Felds 600x400 befinden, ausgeblendet werden.
  • Im Gegensatz zu allgemeinen Komponenten wie LazyColumn werden die Inhalte bei scrollable2D nicht automatisch verschoben. Stattdessen müssen Sie die verfolgte offset auf Ihre Inhalte anwenden, entweder mit graphicsLayer-Transformationen oder Layout-Offsets.
  • Im graphicsLayer-Block verschieben translationX = offset.value.x und translationY = offset.value.y die Zeichenposition des Bildes oder Textes basierend auf der Bewegung deines Fingers, wodurch der visuelle Effekt des Scrollens entsteht.

Verschachteltes Scrollen mit scrollable2D implementieren

In diesem Beispiel wird gezeigt, wie eine bidirektionale Komponente in ein standardmäßiges eindimensionales übergeordnetes Element wie einen vertikalen Nachrichtenfeed integriert werden kann.

Beachten Sie bei der Implementierung von verschachteltem Scrollen die folgenden Punkte:

  • Der Lambda-Ausdruck für rememberScrollable2DState sollte nur das verbrauchte Delta zurückgeben, damit die übergeordnete Liste auf natürliche Weise übernimmt, 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 eine Grenze erreicht, wird der verbleibende Schwung an das übergeordnete Element weitergegeben, damit das Scrollen 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)
        )
    }
}

Abbildung 5: Ein lila Feld in einer vertikal scrollbaren Liste, das interne 2D-Bewegungen ermöglicht, aber die vertikale Scrollsteuerung an die übergeordnete Liste übergibt, sobald der interne Y-Offset des Felds die Grenze von 300 Pixeln erreicht.

Im vorherigen Snippet gilt Folgendes:

  • Die 2D-Komponente kann die Bewegung auf der X-Achse nutzen, um intern zu schwenken, und gleichzeitig die Bewegung auf der Y-Achse an die übergeordnete Liste senden, sobald die vertikalen Grenzen der untergeordneten Komponente erreicht sind.
  • Anstatt den Nutzer auf der 2D-Oberfläche zu halten, berechnet das System das verbrauchte Delta und gibt den Rest in der Hierarchie weiter. So kann der Nutzer weiter durch den Rest der Seite scrollen, ohne den Finger zu heben.

Modifier.draggable2D implementieren

Informationen zur Verwendung von Modifier.draggable2D finden Sie unter In zwei Dimensionen mit Modifier.draggable2D ziehen.