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
SwipeToDismissBoxoder 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.draggableaus, 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.draggable2Daus, 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 mitModifier.draggable2Dziehen.
- Wählen Sie
- Wählen Sie
Modifier.anchoredDraggableaus, 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 mitanchoredDraggablezwischen 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 } } ) } }
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ürStart, 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 mitrememberSaveableundAnchoredDraggableState.Saver<T>erstellt werden, damit ihr Status bei Konfigurationsänderungen erhalten bleibt. - Gestenmodifikator (
Modifier.anchoredDraggable): Ein Modifikator, der Ziehgesten entlang einerOrientationerkennt, 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.
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 }
Instanziieren und speichern Sie
AnchoredDraggableState. Verwenden SierememberSaveablemitAnchoredDraggableState.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) }
Wenden Sie die Modifikatoren
layout,offsetunddraggablean. 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 Modifikatorlayout, um die Komponente zu messen, dieDraggableAnchors-Karte dynamisch anhand ihrer Breite zu berechnen und den Status mitstate.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 denstateund den Zieh-orientation(und optionaloverscrollEffect).Modifier.offset: Liest den Offset ausstate.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() }
[Optional] Overscroll einbinden. Die
overscrollEffectAPI 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.- Dispatch: Übergeben Sie eine
overscrollEffect-Instanz an denanchoredDraggable-Modifikator. Dadurch werden Zieh-Deltas entweder an denAnchoredDraggable-Zustand oder den Overscroll-Effekt gesendet, je nachdem, ob sich die Geste innerhalb oder außerhalb der Ankergrenzen befindet. - Rendern: Wenden Sie den
Modifier.overscrollmit derselbenOverscrollEffect-Instanz auf die Komponente an, die den Effekt visuell rendern soll.
- Dispatch: Übergeben Sie eine
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):
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.
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
OpenundClosedund ordnen Sie sie mithilfe vonDraggableAnchorsPixel-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, ) }
anchoredDraggableStatebeim Klicken auf ein Element animieren PlatziereHomeScreenDrawerunten imBoxundScreenContentsoben. 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(...) } }
Mit
state.requireOffset()können Sie mehrere visuelle Eigenschaften ändern. Anstatt nur den Inhalt zu verschieben, können Sie mitModifier.graphicsLayermehrere 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 (1fbis0.8f) und den Eckenradius (0.dpbis32.dp) relativ zum Offset und zur Breite der Einblendung zu berechnen. Modifier.graphicsLayerfü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) ) }
- Verwenden Sie
Programmatische Animationen mit
animateToauslö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 Siestate.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
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) } } }
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) } ) ) } }
Das vorherige Snippet enthält Folgendes:
- Der Modifikator
onSizeChangedwird verwendet, um die tatsächlichen Abmessungen des Farbverlaufscontainers zu erfassen. Die Auswahl weiß genau, wo die Kanten sind. - In der
graphicsLayerwerdentranslationXundtranslationYangepasst, damit die Auswahl beim Ziehen zentriert bleibt.
Empfehlungen für Sie
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Gesten
- Beispiel für erweiterte Animation: Gesten {:#gesture-and-animation}
- Wertbasierte Animationen