Komponierbare Elemente ziehen

Nutzer müssen Komponenten in modernen Android-Apps physisch über den Bildschirm ziehen können. Ganz gleich, ob Sie einen benutzerdefinierten Schieberegler, eine Komponente zum Wischen zum Schließen oder einen interaktiven Canvas erstellen, Jetpack Compose bietet eine umfassende Reihe von APIs, mit denen Sie Drag-Ereignisse einfach verarbeiten können.

Die Compose-APIs für das Ziehen abstrahieren die Komplexität von Touch-Ereignissen. Anstatt Touch-Koordinaten manuell zu berechnen und Gesten lebenszyklen mit pointerInput zu verwalten, können Sie integrierte Modifikatoren verwenden, die direkt in die Statusverwaltung und Animationssysteme von Compose eingebunden sind.

Auf dieser Seite wird beschrieben, wie und wann die einzelnen APIs für das Ziehen verwendet werden.

Die richtige API für das Ziehen 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 Standardverhalten für das Wischen oder Ziehen von Material 3 benötigen (z. B. zum Löschen einer E-Mail, zum Archivieren einer Aufgabe oder zum Öffnen eines Seitenmenüs oder Ansichten am unteren Rand). Diese Komponenten bieten integrierte Verhaltensweisen, ohne dass eine benutzerdefinierte Status- oder Layoutverwaltung erforderlich ist.
  • Wählen Sie Modifier.draggable aus, wenn: Sie nur rohe Drag- Deltas benötigen (z. B. zum Ziehen eines benutzerdefinierten Schiebereglers). Weitere Informationen finden Sie unter Mit „draggable“ entlang einer einzelnen Achse ziehen.
    • Wählen Sie Modifier.draggable2D aus, wenn: Eine Komponente gleichzeitig auf der X- und der Y-Achse bewegt werden soll (z. B. zum Schwenken auf einer Karte oder zum Erstellen eines unverankerten Aktionsbuttons, den der Nutzer an eine beliebige Stelle auf dem Bildschirm ziehen kann). Weitere Informationen finden Sie unter Mit Modifier.draggable2D in zwei Dimensionen 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 Wischen zum Anzeigen). Weitere Informationen finden Sie unter Komponenten zwischen Ankerpunkten mit anchoredDraggable ziehen.

Mit draggable entlang einer einzelnen Achse ziehen

Der draggable Modifikator ist der Einstiegspunkt auf hoher Ebene für Drag-Gesten in einer einzelnen Ausrichtung und gibt die Drag-Distanz 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 offset Modifikator 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 Drag-Geste steuern müssen, verwenden Sie stattdessen den Drag Gestenerkenner über den pointerInput Modifikator.

@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

Mit anchoredDraggable Komponenten zwischen Ankerpunkten ziehen

Der anchoredDraggable-Modifikator ermöglicht es Ihnen, eine Komponente zwischen einer Reihe vordefinierter Positionen zu ziehen, die als Ankerpunkte bezeichnet werden. Wenn der Nutzer die Drag-Geste loslässt, wird die Komponente am nächsten Ankerpunkt platziert. anchoredDraggable ist nützlich, um Komponenten wie benutzerdefinierte Ansichten am unteren Rand, komplexe Schieberegler oder Schubladenbereiche zu erstellen.

API-Oberfläche

Die AnchoredDraggable-API besteht aus den folgenden Hauptteilen:

  • Ankerpunkte (DraggableAnchors): Ein Builder, der die Zuordnung zwischen Ihren benutzerdefinierten Status (z.B. eine Enum-Klasse für Start, Center, End) und den entsprechenden Pixel-Offsetwerten definiert.
  • Status (AnchoredDraggableState<T>): Ein Status-Holder, der die aktuelle Position (Offset) der Komponente, den Ankerpunkt, an dem sie platziert ist, und ihren Drag-Fortschritt verwaltet. Er sollte mit rememberSaveable und AnchoredDraggableState.Saver<T> erstellt werden, um den Status bei Konfigurations änderungen beizubehalten.
  • Gestenmodifikator (Modifier.anchoredDraggable): Ein Modifikator, der Drag-Gesten entlang einer Orientation erkennt, den Status mit dem Drag-Delta aktualisiert und die Fling-Geste verarbeitet, um die Komponente nach dem Loslassen zum nächsten Ankerpunkt 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 nach links wischen kann, um sie zu schließen, oder nach rechts ziehen kann, um einen Overscroll-Dehnungseffekt zu sehen.

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

    private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }

  2. Instanziieren und speichern 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. Die Modifikatoren layout, offset und draggable anwenden. Wenn die Komponente ziehbar sein soll und die Ankerpunkte an ihre Größe angepasst werden sollen, 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 Komposition nicht bekannt ist. Verwenden Sie den Modifikator layout, um die Komponente zu messen, die DraggableAnchors-Zuordnung dynamisch basierend auf 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 Fling-Geste, um die Komponente nach dem Loslassen zum nächsten Ankerpunkt zu animieren. Fügen Sie diesen Modifikator der Komponente hinzu, die auf Drag-Gesten reagieren soll, und übergeben Sie ihr den state und die Drag-orientation sowie optional overscrollEffect.
    • Modifier.offset: Liest den Offset aus state.requireOffset() in der Offset-Lambda-Funktion. Wenn der Offset in dieser Lambda-Funktion gelesen wird, wird der Wert nur während der Platzierungsphase gelesen. So werden unnötige Neukompositionen 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 Scroll- Containers mit einem Scrollen oder Fling erreicht wurden. Sie können visuelles Feedback geben, wenn ein Nutzer mit einem Overscroll-Effekt (in der Regel eine Dehnung oder ein Leuchten) über die definierten Ankergrenzen hinaus zieht.

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

Das folgende Snippet zeigt, wie Sie einen Standard-Dehnungseffekt 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 kann, um den Overscroll-Dehnungseffekt zu sehen:

Video 2. Eine benutzerdefinierte Implementierung der Komponente zum Wischen zum Schließen.

Beispiel: Benutzerdefinierte, gestengesteuerte Navigationsleiste erstellen

In diesem Beispiel wird gezeigt, wie Sie eine benutzerdefinierte, gestengesteuerte Navigationsleiste erstellen. Wenn der Nutzer horizontal zieht, gleitet der Vordergrundbildschirm (ScreenContents) nach rechts, wird von 1.0f auf 0.8f verkleinert und die Ecken werden abgerundet, um die darunter liegende Navigationsleiste sichtbar zu machen.

  1. Ankerstatus und -positionen definieren. In diesem Beispiel sind die Ankerpunkte die geöffneten und geschlossenen Status der Navigationsleiste. Definieren Sie eine Enum-Klasse für die Status Open und Closed und ordnen Sie sie mit 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. Platzieren Sie HomeScreenDrawer unten in der Box und ScreenContents oben. Die Navigationsleiste bleibt an Ort und Stelle, während die oberste Ebene darüber verschoben und skaliert wird.

    Wenn ein Nutzer ein Element der Navigationsleiste auswählt, aktualisieren Sie den Status des aktiven Bildschirms und schließen Sie die Navigationsleiste 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() mehrere visuelle Eigenschaften ändern. Anstatt nur den Inhalt zu verschieben, können Sie mit Modifier.graphicsLayer mehrere Eigenschaften ändern, um Verschiebung, Skalierung und Eckradius gleichzeitig basierend auf dem Drag-Fortschritt zu animieren:

    • Verwenden Sie state.requireOffset(), um den aktuellen Drag-Offset abzurufen.
    • Verwenden Sie lerp() (lineare Interpolation), um die proportionale Skalierung (1f bis 0.8f) und den Eckradius (0.dp bis 32.dp) relativ zum Offset und zur Breite der Navigationsleiste zu berechnen.
    • Modifier.graphicsLayer führt Änderungen in der Zeichenphase aus. So werden leistungsstarke Animationen ermöglicht, ohne dass während des Ziehens Neukompositionen 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. Steuern Sie die Navigationsleiste programmatisch (z. B. beim Tippen auf das Menüsymbol oder beim Auswählen eines Elements der Navigationsleiste), indem Sie state.animateTo() in einem Coroutinenbereich 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 (Sleep) wird nach rechts gezogen, wodurch die darunter liegende Navigationsleiste sichtbar wird.

Mit Modifier.draggable2D in zwei Dimensionen ziehen

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

Komponierbares Element ziehen

In diesem Beispiel wird der häufigste Anwendungsfall für draggable2D gezeigt: Ein Nutzer kann ein UI-Element auswählen und 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. Eine kleine violette Box wird auf einem grauen Hintergrund neu positioniert. Dies zeigt das direkte 2D-Ziehen, bei dem sich das Element nicht mehr bewegt, sobald der Finger des Nutzers angehoben wird.

Das vorherige Code-Snippet enthält Folgendes:

  • Die Position der Box wird mit einem offset-Status verfolgt.
  • Der Modifikator offset wird verwendet, um die Position der Komponente basierend auf den Drag-Deltas zu verschieben.
  • Da keine Fling-Unterstützung vorhanden ist, bewegt sich die Box nicht mehr, sobald der Nutzer den Finger anhebt.

Komponierbares untergeordnetes Element basierend auf dem Drag-Bereich 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 bewegt wird, werden in dieser Implementierung die 2D-Deltas verwendet, um ein komponierbares untergeordnetes Element („selector“) über eine Farbauswahl zu bewegen:

@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 kreisförmigen Auswahlknopf, der in jede Richtung gezogen werden kann. Dies zeigt, wie 2D-Deltas auf die Grenzen des Containers beschränkt werden, um die ausgewählten Farbwerte zu aktualisieren.

Das vorherige Snippet enthält Folgendes:

  • Der Modifikator onSizeChanged wird verwendet, um die tatsächlichen Abmessungen des Farbverlaufs-Containers zu erfassen. Der Auswahlknopf weiß genau, wo sich die Ränder befinden.
  • In graphicsLayer werden translationX und translationY angepasst, damit der Auswahlknopf beim Ziehen zentriert bleibt.