Umożliwienie użytkownikom fizycznego przeciągania komponentów po ekranie jest podstawą nowoczesnych aplikacji na Androida. Niezależnie od tego, czy tworzysz niestandardowy suwak, komponent zamykany przez przesunięcie palcem czy interaktywny obszar roboczy, Jetpack Compose udostępnia kompleksowy zestaw interfejsów API, które ułatwiają obsługę zdarzeń przeciągania.
Interfejsy API przeciągania w Compose ukrywają złożoność surowych zdarzeń dotknięcia.
Zamiast ręcznie obliczać współrzędne dotyku i zarządzać cyklem życia gestów
za pomocą pointerInput, możesz używać wbudowanych modyfikatorów, które są bezpośrednio powiązane z systemami zarządzania stanem i animacji w Compose.
Na tej stronie opisujemy, jak i kiedy używać poszczególnych interfejsów API przeciągania.
Wybieranie odpowiedniego interfejsu API przeciągania
Do implementowania treści, które można przeciągać, możesz używać kilku interfejsów API.
- Wybierz standardowe komponenty Material (np.
SwipeToDismissBoxlub panele nawigacji), gdy: potrzebujesz domyślnego zachowania Material 3 podczas przesuwania lub przeciągania (np. usuwanie e-maila, archiwizowanie zadania lub otwieranie bocznego menu / plansz dolnych). Zapewniają one wbudowane zachowania bez konieczności zarządzania stanem lub układem. - Wybierz
Modifier.draggablegdy: potrzebujesz tylko surowych delty przeciągania (np. przeciąganie niestandardowego suwaka). Więcej informacji znajdziesz w artykule Przeciąganie wzdłuż jednej osi za pomocą modyfikatora draggable.- Wybierz
Modifier.draggable2Dgdy: potrzebujesz komponentu, który będzie się poruszać jednocześnie wzdłuż osi X i Y (np. przesuwanie mapy lub tworzenie pływającego przycisku polecenia (FAB), który użytkownik może przeciągnąć w dowolne miejsce na ekranie). Więcej informacji znajdziesz w artykule Przeciąganie w 2 wymiarach za pomocąModifier.draggable2D.
- Wybierz
- Wybierz
Modifier.anchoredDraggablegdy: tworzysz niestandardowe komponenty interaktywne, które muszą się zatrzaskiwać w 2 lub więcej dyskretnych pozycjach (np. plansza dolna, panel nawigacji lub działanie ujawniane przez przesunięcie palcem). Więcej informacji znajdziesz w artykule Przeciąganie komponentów między punktami zaczepienia za pomocąanchoredDraggable.
Przeciąganie wzdłuż jednej osi za pomocą modyfikatora draggable
Modyfikator draggable to punkt wejścia wysokiego poziomu do gestów przeciągania w
jednej orientacji. Zgłasza on odległość przeciągania w pikselach.
Warto pamiętać, że ten modyfikator jest podobny do modyfikatora scrollable, ponieważ wykrywa tylko gest. Musisz utrzymywać stan i reprezentować go na ekranie
np. przesuwając element za pomocą modyfikatora offset:
@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!" ) }
Jeśli chcesz kontrolować cały gest przeciągania, rozważ użycie detektora gestów przeciągania za pomocą modyfikatora pointerInput.
@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 } } ) } }
Przeciąganie komponentów między punktami zaczepienia za pomocą modyfikatora anchoredDraggable
Modyfikator anchoredDraggable umożliwia przeciąganie komponentu między zestawem
predefiniowanych pozycji nazywanych punktami zaczepienia. Gdy użytkownik zwolni gest przeciągania, komponent zatrzyma się w najbliższym punkcie zaczepienia. Modyfikator anchoredDraggable jest przydatny do tworzenia komponentów takich jak niestandardowe plansze dolne, złożone suwaki czy panele szuflad.
Powierzchnia interfejsu API
Interfejs API AnchoredDraggable składa się z tych głównych części:
- Punkty zaczepienia (
DraggableAnchors): narzędzie do tworzenia, które definiuje mapowanie między stanami niestandardowymi (np. klasą wyliczeniowąStart, Center, End) a odpowiadającymi im wartościami przesunięcia w pikselach. - Stan (
AnchoredDraggableState<T>): zmienna stanu, która zarządza bieżącą pozycją komponentu (przesunięciem), punktem zaczepienia, w którym się zatrzymał, oraz jego postępem przeciągania. Aby zachować stan podczas zmian konfiguracji należy go utworzyć za pomocąrememberSaveableiAnchoredDraggableState.Saver<T>. - Modyfikator gestu (
Modifier.anchoredDraggable): modyfikator, który wykrywa gesty przeciągania wzdłużOrientation, aktualizuje stan za pomocą delty przeciągania i obsługuje gest szybkiego przesunięcia, aby animować komponent do najbliższego punktu zaczepienia po zwolnieniu.
Przykład: tworzenie niestandardowego komponentu SwipeToDismiss
Ten przykład pokazuje, jak utworzyć niestandardowy komponent SwipeToDismiss (biały baner), który użytkownik może zamknąć, przesuwając palcem w lewo, lub przeciągnąć w prawo, aby zobaczyć efekt rozciągania overscroll.
Zdefiniuj wartości punktów zaczepienia. Zdefiniuj wyliczenie reprezentujące dyskretne pozycje, w których może się zatrzymać przeciągana treść:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Utwórz instancję i zapisz
AnchoredDraggableState. UżyjrememberSaveablezAnchoredDraggableState.Saver(), aby zachować bieżącą pozycję komponentu podczas ponownego tworzenia aktywności i zmian konfiguracji (np. obracania ekranu lub zmiany motywu):val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
Zastosuj modyfikatory
layout,offsetidraggable. Aby komponent można było przeciągać i aby jego punkty zaczepienia dostosowywały się do jego rozmiaru, zastosuj te modyfikatory do treści, która ma się wizualnie przesuwać:Modifier.layout: pozycje punktów zaczepienia często zależą od rozmiaru komponentu, który nie jest znany podczas początkowej kompozycji. Użyj modyfikatoralayout, aby zmierzyć komponent, dynamicznie obliczyć mapęDraggableAnchorsna podstawie jego szerokości i zaktualizować stan za pomocąstate.updateAnchors().
Modifier.anchoredDraggable: łączy się ze stanem, wysyła delty przeciągania w określonej orientacji i obsługuje gest szybkiego przesunięcia, aby animować komponent do najbliższego punktu zaczepienia po zwolnieniu. Dodaj ten modyfikator do komponentu, który ma reagować na gesty przeciągania, i przekaż mustateorazorientationprzeciągania (opcjonalnieoverscrollEffect).Modifier.offset: odczytuje przesunięcie zstate.requireOffset()w lambdzie przesunięcia. Odczytywanie przesunięcia w tej lambdzie zapewnia, że wartość jest odczytywana tylko podczas fazy umieszczania, co pozwala uniknąć niepotrzebnych rekompozycji podczas przeciągania. Zastosuj ten modyfikator do treści, która ma się wizualnie przesuwać.
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() }
[Opcjonalnie] Zintegruj efekt rozciągania. Interfejs API
overscrollEffectimplementuje efekt wizualny, który jest wyświetlany, gdy podczas przewijania lub szybkiego przesunięcia osiągnięto krawędzie kontenera przewijania. Możesz zapewnić wizualne informacje zwrotne, gdy użytkownik przeciągnie poza zdefiniowane granice punktu zaczepienia, za pomocą efektu overscroll (zwykle rozciągania lub poświaty).- Wysyłanie: przekaż instancję
overscrollEffectdoanchoredDraggablemodyfikatora. Wysyła to delty przeciągania do stanuAnchoredDraggablelub do efektu overscroll, w zależności od tego, czy gest znajduje się w granicach punktu zaczepienia, czy poza nimi. - Renderowanie: zastosuj
Modifier.overscrollz tą samąOverscrollEffectinstancją do komponentu, który ma wizualnie renderować efekt.
- Wysyłanie: przekaż instancję
Ten fragment kodu pokazuje, jak dodać standardowy efekt rozciągania:
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() } }
Wynik
Poprzedni fragment kodu powoduje wyświetlenie białego banera z tekstem „Przeciągnij, aby zamknąć!”, który użytkownik może zamknąć, przesuwając palcem w lewo (lub przeciągnąć w prawo, aby zobaczyć efekt rozciągania overscroll):
Przykład: tworzenie niestandardowego panelu nawigacji sterowanego gestami
Ten przykład pokazuje, jak utworzyć niestandardową szufladę nawigacyjną sterowaną gestami. Gdy użytkownik przeciąga w poziomie, ekran pierwszego planu (ScreenContents) przesuwa się w prawo, skaluje w dół z 1.0f do 0.8f i zaokrągla rogi, aby odsłonić panel nawigacji.
Zdefiniuj stany i pozycje punktów zaczepienia. W tym przykładzie punkty zaczepienia to stany otwarcia i zamknięcia szuflady. Zdefiniuj wyliczenie dla stanów
OpeniClosedi zmapuj je na przesunięcia w pikselach za pomocąDraggableAnchors: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, ) }
Animuj
anchoredDraggableStatepo kliknięciu elementu. UmieśćHomeScreenDraweru dołuBox, aScreenContentsna górze. Szuflada pozostaje nieruchoma, a górna warstwa przesuwa się i skaluje nad nią.Gdy użytkownik wybierze element szuflady, zaktualizuj stan aktywnego ekranu i automatycznie zamknij szufladę za pomocą
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(...) } }
Użyj
state.requireOffset()do zmiany wielu właściwości wizualnych. Zamiast tylko przesuwać treść, możesz zmieniać wiele właściwości za pomocąModifier.graphicsLayer, aby jednocześnie animować przesunięcie, skalę i promień zaokrąglenia na podstawie postępu przeciągania:- Użyj
state.requireOffset(), aby uzyskać bieżące przesunięcie przeciągania. - Użyj
lerp()(interpolacji liniowej), aby obliczyć proporcjonalną skalę (1fdo0.8f) i promień zaokrąglenia (0.dpdo32.dp) względem przesunięcia i szerokości szuflady. - Modyfikator
Modifier.graphicsLayerwykonuje zmiany w fazie rysowania, co zapewnia animacje o wysokiej wydajności bez wywoływania rekompozycji podczas przeciągania.
@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) ) }
- Użyj
Wywołuj animacje programowe za pomocą
animateTo. Programowo steruj szufladą (np. po kliknięciu ikony menu lub wybraniu elementu szuflady), uruchamiającstate.animateTo()w zakresie korutyny:fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
Wynik
Przeciąganie w 2 wymiarach za pomocą modyfikatora Modifier.draggable2D
Użyj modyfikatora draggable2D, aby przesuwać poszczególne elementy interfejsu jednocześnie wzdłuż osi X i Y.
Przeciąganie elementu kompozycyjnego
Ten przykład pokazuje najczęstszy przypadek użycia modyfikatora draggable2D – umożliwienie użytkownikowi wybrania elementu interfejsu i zmiany jego położenia w dowolnym miejscu w kontenerze nadrzędnym.
@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) } } }
Poprzedni fragment kodu zawiera te elementy:
- Śledzi pozycję pola za pomocą stanu
offset. - Używa modyfikatora
offset, aby przesunąć pozycję komponentu na podstawie delty przeciągania. - Ponieważ nie ma obsługi szybkiego przesunięcia, pole przestaje się poruszać, gdy tylko użytkownik podniesie palec.
Przeciąganie elementu kompozycyjnego podrzędnego na podstawie obszaru przeciągania elementu nadrzędnego
Ten przykład pokazuje, jak użyć modyfikatora draggable2D do utworzenia obszaru wprowadzania 2D, w którym pokrętło selektora jest ograniczone do określonej powierzchni. W przeciwieństwie do przykładu z elementem, który można przeciągać, gdzie przesuwa się sam komponent, ta implementacja używa delty 2D do przesuwania elementu kompozycyjnego podrzędnego „selektora” po selektorze kolorów:
@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) } ) ) } }
Poprzedni fragment kodu zawiera te elementy:
- Używa modyfikatora
onSizeChanged, aby przechwycić rzeczywiste wymiary kontenera gradientu. Selektor dokładnie wie, gdzie są krawędzie. - Wewnątrz
graphicsLayerdostosowujetranslationXitranslationY, aby selektor pozostawał wyśrodkowany podczas przeciągania.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Omówienie gestów
- Zaawansowany przykład animacji: gesty {:#gesture-and-animation}
- Animacje oparte na wartościach