Komponierbare Elemente ziehen

Die Möglichkeit, Komponenten physisch über einen Bildschirm zu ziehen, ist für moderne Android-Apps von grundlegender Bedeutung. Unabhängig davon, ob Sie einen benutzerdefinierten Schieberegler, eine Komponente zum Schließen durch Wischen oder einen interaktiven Canvas erstellen, bietet Jetpack Compose eine umfassende Reihe von APIs, mit denen Sie Drag-Ereignisse ganz einfach verarbeiten können.

Die Compose-APIs für das Ziehen abstrahieren die Komplexität von rohen Touch-Events. Anstatt Berührungskoordinaten manuell zu berechnen und Gestenlebenszyklen mit pointerInput zu verwalten, können Sie integrierte Modifikatoren verwenden, die direkt mit den Zustandsverwaltungs- und Animationssystemen von Compose verknüpft sind.

Auf dieser Seite wird beschrieben, wie und wann die einzelnen Dragging APIs verwendet werden.

Die richtige Drag-and-drop-API auswählen

Es gibt mehrere APIs, mit denen Sie ziehbare Inhalte implementieren können.

  • Wählen Sie Standard-Material-Komponenten (wie SwipeToDismissBox oder Navigationsleisten) aus, wenn: Sie das standardmäßige Wisch- oder Ziehverhalten von Material 3 benötigen (z. B. zum Löschen einer E-Mail, Archivieren einer Aufgabe oder Öffnen eines Seitenmenüs / einer Ansicht am unteren Rand). Sie bieten integrierte Verhaltensweisen, ohne dass eine benutzerdefinierte Status- oder Layoutverwaltung erforderlich ist.
  • Wählen Sie Modifier.draggable aus, wenn: Sie nur Rohdaten für Ziehbewegungen benötigen (z. B. Ziehen eines benutzerdefinierten Schiebereglers). Weitere Informationen finden Sie unter Mit „draggable“ entlang einer einzelnen Achse ziehen.
    • Wählen Sie Modifier.draggable2D aus, wenn: Sie eine Komponente benötigen, die sich gleichzeitig kontinuierlich über die X- und Y-Achse bewegt (z. B. beim Schwenken über eine Karte oder beim Erstellen eines unverankerten Aktionsbuttons, den der Nutzer an eine beliebige Stelle auf dem Bildschirm ziehen kann). Weitere Informationen finden Sie unter In zwei Dimensionen mit Modifier.draggable2D ziehen.
  • Wählen Sie Modifier.anchoredDraggable aus, wenn: Sie benutzerdefinierte interaktive Komponenten erstellen, die zwischen zwei oder mehr diskreten Positionen einrasten müssen (z. B. eine Ansicht am unteren Rand, eine Navigationsleiste oder eine Aktion zum Einblenden durch Wischen). Weitere Informationen finden Sie unter Komponenten mit anchoredDraggable zwischen Ankerpunkten ziehen.

Mit draggable entlang einer einzelnen Achse ziehen

Der Modifikator draggable ist der übergeordnete Einstiegspunkt für Ziehgesten in einer einzelnen Ausrichtung und gibt die Ziehdistanz in Pixeln an.

Dieser Modifikator ähnelt scrollable, da er nur die Geste erkennt. Sie müssen den Status beibehalten und auf dem Bildschirm darstellen, indem Sie das Element beispielsweise mit dem Modifikator offset verschieben:

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

Wenn Sie die gesamte Ziehgeste steuern möchten, können Sie stattdessen den Detektor für Ziehgesten über den Modifikator pointerInput verwenden.

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

ALT-TEXT HIER EINFÜGEN
Abbildung 1. Ein UI-Element, das mit einem Finger gezogen wird

Komponenten mit anchoredDraggable zwischen Ankerpunkten ziehen

Mit dem Modifikator anchoredDraggable können Sie eine Komponente zwischen einer Reihe vordefinierter Positionen, den sogenannten Ankerpunkten, ziehen. Wenn der Nutzer die Ziehgeste loslässt, wird die Komponente am nächstgelegenen Anker platziert. anchoredDraggable ist nützlich, um Komponenten wie benutzerdefinierte Ansichten am unteren Rand, komplexe Schieberegler oder Drawer-Bereiche zu erstellen.

API-Oberfläche

Die AnchoredDraggable API besteht aus den folgenden Hauptteilen:

  • Anker (DraggableAnchors): Ein Builder, der die Zuordnung zwischen Ihren benutzerdefinierten Status (z.B. einer Enum-Klasse für Start, Center, End) und den entsprechenden Pixel-Offset-Werten definiert.
  • State (AnchoredDraggableState<T>): Ein State Holder, der die aktuelle Position (Offset) der Komponente, den Anker, an dem sie sich befindet, und den Fortschritt des Ziehvorgangs verwaltet. Sie sollte mit rememberSaveable und AnchoredDraggableState.Saver<T> erstellt werden, damit ihr Status bei Konfigurationsänderungen erhalten bleibt.
  • Gestenmodifikator (Modifier.anchoredDraggable): Ein Modifikator, der Ziehgesten entlang einer Orientation erkennt, den Status mit dem Ziehdelta aktualisiert und die Wischgeste verarbeitet, um die Komponente beim Loslassen zum nächsten Anker zu animieren.

Beispiel: Benutzerdefinierte SwipeToDismiss-Komponente erstellen

In diesem Beispiel wird gezeigt, wie Sie eine benutzerdefinierte SwipeToDismiss-Komponente (ein weißes Banner) erstellen, die der Nutzer durch Wischen nach links schließen oder nach rechts ziehen kann, um einen Overscroll-Stretch-Effekt zu sehen.

  1. Ankerwerte definieren Definieren Sie ein Enum, das die diskreten Positionen darstellt, an denen der ziehbare Inhalt platziert werden kann:

    private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }

  2. Instanziieren und speichern Sie AnchoredDraggableState. Verwenden Sie rememberSaveable mit AnchoredDraggableState.Saver(), um die aktuelle Position der Komponente bei der Neuerstellung von Aktivitäten und Konfigurationsänderungen (z.B. Bildschirmdrehung oder Designänderungen) beizubehalten:

    val state =
        rememberSaveable(saver = AnchoredDraggableState.Saver()) {
            AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting)
        }

  3. Wenden Sie die Modifikatoren layout, offset und draggable an. Damit die Komponente ziehbar ist und sich ihre Ankerpunkte an ihre Größe anpassen, wenden Sie die folgenden Modifikatoren auf den Inhalt an, der sich visuell bewegen soll:

    • Modifier.layout: Ankerpositionen hängen oft von der Größe einer Komponente ab, die bei der ersten Erstellung nicht bekannt ist. Verwenden Sie den Modifikator layout, um die Komponente zu messen, die DraggableAnchors-Karte dynamisch anhand ihrer Breite zu berechnen und den Status mit state.updateAnchors() zu aktualisieren.

    • Modifier.anchoredDraggable: Stellt eine Verbindung zum Status her, sendet Drag-Deltas in der angegebenen Ausrichtung und verarbeitet die Wischgeste, um die Komponente beim Loslassen zum nächsten Anker zu animieren. Füge diesen Modifikator der Komponente hinzu, die auf Ziehgesten reagieren soll, und übergib ihm den state und den Zieh-orientation (und optional overscrollEffect).
    • Modifier.offset: Liest den Offset aus state.requireOffset() im Offset-Lambda. Wenn der Offset innerhalb dieser Lambda-Funktion gelesen wird, wird der Wert nur während der Platzierungsphase gelesen. So werden unnötige Neuzusammensetzungen während des Ziehens vermieden. Wenden Sie diesen Modifikator auf den Inhalt an, der sich visuell bewegen soll.

    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. [Optional] Overscroll einbinden. Die overscrollEffect API implementiert einen visuellen Effekt, der angezeigt wird, wenn die Ränder eines scrollbaren Containers durch Scrollen oder Wischen erreicht wurden. Sie können visuelles Feedback geben, wenn ein Nutzer über die definierten Ankergrenzen hinaus zieht, indem Sie einen Overscroll-Effekt (normalerweise ein Dehnen oder Leuchten) verwenden.

    1. Dispatch: Übergeben Sie eine overscrollEffect-Instanz an den anchoredDraggable-Modifikator. Dadurch werden Zieh-Deltas entweder an den AnchoredDraggable-Zustand oder den Overscroll-Effekt gesendet, je nachdem, ob sich die Geste innerhalb oder außerhalb der Ankergrenzen befindet.
    2. Rendern: Wenden Sie den Modifier.overscroll mit derselben OverscrollEffect-Instanz auf die Komponente an, die den Effekt visuell rendern soll.

Das folgende Snippet zeigt, wie Sie einen Standard-Streckeffekt hinzufügen:

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

Ergebnis

Das vorherige Code-Snippet führt zu einem weißen Banner mit dem Text „Drag me to dismiss!“, das der Nutzer nach links wischen kann, um es zu schließen (oder nach rechts ziehen, um den Overscroll-Stretch-Effekt zu sehen):

Video 2: Eine benutzerdefinierte Implementierung der Komponente „Zum Schließen wischen“.

Beispiel: Benutzerdefinierte, gestengesteuerte Navigationsleiste erstellen

In diesem Beispiel wird gezeigt, wie Sie eine benutzerdefinierte, gestengesteuerte Navigationsleiste erstellen. Wenn der Nutzer horizontal zieht, wird der Bildschirm im Vordergrund (ScreenContents) nach rechts verschoben, von 1.0f auf 0.8f verkleinert und die Ecken werden abgerundet, um die darunter liegende Navigationsleiste anzuzeigen.

  1. Ankerstatus und -positionen definieren In diesem Beispiel sind die Anker der geöffnete und der geschlossene Zustand der Einblendung. Definieren Sie ein Enum für die Zustände Open und Closed und ordnen Sie sie mithilfe von DraggableAnchors Pixel-Offsets zu:

    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. anchoredDraggableState beim Klicken auf ein Element animieren Platziere HomeScreenDrawer unten im Box und ScreenContents oben. Die Ansicht bleibt stationär, während die oberste Ebene darüber verschoben und skaliert wird.

    Wenn ein Nutzer ein Drawer-Element auswählt, aktualisiere den Status des aktiven Bildschirms und schließe den Drawer automatisch mit 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. Mit state.requireOffset() können Sie mehrere visuelle Eigenschaften ändern. Anstatt nur den Inhalt zu verschieben, können Sie mit Modifier.graphicsLayer mehrere Eigenschaften ändern, um die Verschiebung, die Skalierung und den Eckenradius gleichzeitig basierend auf dem Ziehfortschritt zu animieren:

    • Verwenden Sie state.requireOffset(), um den aktuellen Ziehversatz abzurufen.
    • Verwende lerp() (lineare Interpolation), um die proportionale Skalierung (1f bis 0.8f) und den Eckenradius (0.dp bis 32.dp) relativ zum Offset und zur Breite der Einblendung zu berechnen.
    • Modifier.graphicsLayer führt Änderungen in der Zeichenphase aus, um leistungsstarke Animationen zu ermöglichen, ohne dass während des Ziehens Neuzusammensetzungen ausgelöst werden.

    @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. Programmatische Animationen mit animateTo auslösen Sie können die Seitenleiste programmatisch steuern (z. B. wenn Sie auf das Menüsymbol tippen oder ein Element in der Seitenleiste auswählen), indem Sie state.animateTo() innerhalb eines Coroutine-Bereichs starten:

    fun toggleDrawerState(
        state: AnchoredDraggableState<DrawerState>,
        coroutineScope: CoroutineScope
    ) {
        coroutineScope.launch {
            if (state.currentValue == DrawerState.Open) {
                state.animateTo(DrawerState.Closed)
            } else {
                state.animateTo(DrawerState.Open)
            }
        }
    }

Ergebnis

Video 3. Der Vordergrundbildschirm (Schlaf) wird nach rechts gezogen, sodass die darunter liegende Navigationsleiste sichtbar wird.

Ziehen Sie zwei Dimensionen mit Modifier.draggable2D in die Visualisierung.

Verwenden Sie den Modifikator draggable2D, um einzelne UI-Elemente gleichzeitig auf der X- und Y-Achse zu verschieben.

Ziehen Sie ein zusammensetzbares Element

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

Video 4. Ein kleines lila Feld wird auf einem grauen Hintergrund neu positioniert. Dies zeigt das direkte 2D-Ziehen, bei dem sich das Element nicht mehr bewegt, sobald der Nutzer den Finger hebt.

Das obige Code-Snippet enthält Folgendes:

  • Die Position des Felds wird mit einem offset-Zustand verfolgt.
  • Verwendet den Modifikator offset, um die Position der Komponente basierend auf den Zieh-Deltas zu verschieben.
  • Da keine Fling-Unterstützung vorhanden ist, bewegt sich das Feld nicht mehr, sobald der Nutzer den Finger hebt.

Untergeordnetes Compose-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 für das ziehbare Element, bei dem die Komponente selbst verschoben wird, werden bei dieser Implementierung die 2D-Deltas verwendet, um eine untergeordnete zusammensetzbare „Auswahl“ über eine 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)
                    }
                )
        )
    }
}

Video 5: Ein Farbverlauf mit einem weißen runden Auswahlknopf, der in jede Richtung gezogen werden kann. Er zeigt, wie 2D-Deltas an die Grenzen des Containers gebunden werden, um ausgewählte Farbwerte zu aktualisieren.

Das vorherige Snippet enthält Folgendes:

  • Der Modifikator onSizeChanged wird verwendet, um die tatsächlichen Abmessungen des Farbverlaufscontainers zu erfassen. Die Auswahl weiß genau, wo die Kanten sind.
  • In der graphicsLayer werden translationX und translationY angepasst, damit die Auswahl beim Ziehen zentriert bleibt.