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
SwipeToDismissBoxoder 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.draggableaus, 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.draggable2Daus, 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 MitModifier.draggable2Din zwei Dimensionen ziehen.
- 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 Wischen zum Anzeigen). Weitere Informationen finden Sie unter Komponenten zwischen Ankerpunkten mitanchoredDraggableziehen.
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 } } ) } }
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ürStart, 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 mitrememberSaveableundAnchoredDraggableState.Saver<T>erstellt werden, um den Status bei Konfigurations änderungen beizubehalten. - Gestenmodifikator (
Modifier.anchoredDraggable): Ein Modifikator, der Drag-Gesten entlang einerOrientationerkennt, 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.
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 }
Instanziieren und speichern
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) }
Die Modifikatoren
layout,offsetunddraggableanwenden. 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 Modifikatorlayout, um die Komponente zu messen, dieDraggableAnchors-Zuordnung dynamisch basierend auf 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 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 denstateund die Drag-orientationsowie optionaloverscrollEffect.Modifier.offset: Liest den Offset ausstate.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() }
[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.- Senden: Übergeben Sie eine
overscrollEffectInstanz an denanchoredDraggableModifikator. Dadurch werden Drag-Deltas entweder an denAnchoredDraggable-Status oder an den Overscroll-Effekt gesendet, je nachdem, ob sich die Geste innerhalb oder außerhalb der Ankergrenzen befindet. - Rendern: Wenden Sie das
Modifier.overscrollmit derselbenOverscrollEffectInstanz auf die Komponente an, die den Effekt visuell rendern soll.
- Senden: Übergeben Sie eine
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:
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.
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
OpenundClosedund ordnen Sie sie mitDraggableAnchorsPixel-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. Platzieren SieHomeScreenDrawerunten in derBoxundScreenContentsoben. 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(...) } }
Mit
state.requireOffset()mehrere visuelle Eigenschaften ändern. Anstatt nur den Inhalt zu verschieben, können Sie mitModifier.graphicsLayermehrere 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 (1fbis0.8f) und den Eckradius (0.dpbis32.dp) relativ zum Offset und zur Breite der Navigationsleiste zu berechnen. Modifier.graphicsLayerfü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) ) }
- Verwenden Sie
Programmatische Animationen mit
animateToauslösen. Steuern Sie die Navigationsleiste programmatisch (z. B. beim Tippen auf das Menüsymbol oder beim Auswählen eines Elements der Navigationsleiste), indem Siestate.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
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) } } }
Das vorherige Code-Snippet enthält Folgendes:
- Die Position der Box wird mit einem
offset-Status verfolgt. - Der Modifikator
offsetwird 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) } ) ) } }
Das vorherige Snippet enthält Folgendes:
- Der Modifikator
onSizeChangedwird verwendet, um die tatsächlichen Abmessungen des Farbverlaufs-Containers zu erfassen. Der Auswahlknopf weiß genau, wo sich die Ränder befinden. - In
graphicsLayerwerdentranslationXundtranslationYangepasst, damit der Auswahlknopf beim Ziehen zentriert bleibt.
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- Gesten
- Erweitertes Animationsbeispiel: Gesten {:#gesture-and-animation}
- Wertbasierte Animationen