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 do przesuwania w celu odrzucenia 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 upraszczają złożoność nieprzetworzonych zdarzeń dotknięcia.
Zamiast ręcznie obliczać współrzędne dotknięcia i zarządzać cyklami ż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 do przeciągania.
Wybieranie odpowiedniego interfejsu API do przeciągania
Do implementacji treści, które można przeciągać, możesz użyć kilku interfejsów API.
- Wybierz standardowe komponenty Material (takie jak
SwipeToDismissBoxlub panele nawigacji), gdy: potrzebujesz domyślnego zachowania Material 3 podczas przesuwania lub przeciągania (np. usuwania e-maila, archiwizowania zadania lub otwierania menu bocznego / plansz dolnych). Zapewniają wbudowane zachowania bez konieczności zarządzania niestandardowym stanem lub układem. - Kiedy wybrać
Modifier.draggable: gdy potrzebujesz tylko nieprzetworzonych wartości delta przeciągania (np. przeciąganie niestandardowego suwaka). Więcej informacji znajdziesz w artykule Przeciąganie wzdłuż jednej osi za pomocą elementu przeciągalnego.- Kiedy wybrać
Modifier.draggable2D: gdy komponent musi poruszać się płynnie wzdłuż osi X i Y jednocześnie (np. podczas przesuwania mapy lub tworzenia pływającego przycisku polecenia, 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.
- Kiedy wybrać
- Kiedy wybrać
Modifier.anchoredDraggable: gdy tworzysz niestandardowe komponenty interaktywne, które muszą przechodzić między co najmniej 2 oddzielnymi pozycjami (np. plansza dolna, panel nawigacji lub działanie polegające na przesunięciu palcem w celu wyświetlenia). Więcej informacji znajdziesz w artykule Przeciąganie komponentów między punktami zakotwiczenia za pomocąanchoredDraggable.
Przeciągnij wzdłuż jednej osi za pomocą klawisza draggable
Modyfikator draggable to punkt wejścia wysokiego poziomu do gestów przeciągania w jednej orientacji, który raportuje odległość przeciągania w pikselach.
Warto zauważyć, że ten modyfikator jest podobny do scrollable, ponieważ wykrywa tylko gest. Musisz zachować stan i wyświetlić go na ekranie, na przykład przenoszą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 gestu 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ągaj komponenty między punktami zakotwiczenia za pomocą klawisza anchoredDraggable
Modyfikator anchoredDraggable umożliwia przeciąganie komponentu między zestawem wstępnie zdefiniowanych pozycji nazywanych punktami zakotwiczenia. Gdy użytkownik puści komponent, zatrzyma się on na najbliższej kotwicy. anchoredDraggable jest przydatny do tworzenia komponentów takich jak niestandardowe plansze dolne, złożone suwaki czy panele wysuwane.
Interfejs API
Interfejs AnchoredDraggable API składa się z tych głównych części:
- Anchors (
DraggableAnchors): kreator, który definiuje mapowanie między stanami niestandardowymi (np. klasą wyliczeniową dlaStart, Center, End) a odpowiadającymi im wartościami przesunięcia pikseli. - State (
AnchoredDraggableState<T>): zmienna stanu, która zarządza bieżącą pozycją komponentu (przesunięciem), kotwicą, do której jest przypisany, i postępem przeciągania. Należy go utworzyć za pomocąrememberSaveableiAnchoredDraggableState.Saver<T>, aby zachować jego stan w przypadku zmian konfiguracji. - Modyfikator gestów (
Modifier.anchoredDraggable): modyfikator, który wykrywa gesty przeciągania wzdłużOrientation, aktualizuje stan za pomocą przesunięcia i obsługuje gest szybkiego przesunięcia, aby po zwolnieniu animować komponent do najbliższego punktu zakotwiczenia.
Przykład: tworzenie niestandardowego komponentu SwipeToDismiss
Ten przykład pokazuje, jak utworzyć niestandardowy komponent SwipeToDismiss (biały baner), który użytkownik może przesunąć w lewo, aby go zamknąć, lub przeciągnąć w prawo, aby zobaczyć efekt rozciągnięcia przy przewijaniu poza zakres.
Zdefiniowanie wartości kotwic. Zdefiniuj typ wyliczeniowy reprezentujący dyskretne pozycje, w których można umieścić przeciągany element:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Utwórz instancję i zapisz
AnchoredDraggableState. UżyjrememberSaveablezAnchoredDraggableState.Saver(), aby zachować bieżącą pozycję komponentu podczas odtwarzania 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 zakotwiczenia dostosowywały się do jego rozmiaru, zastosuj do treści, które mają się wizualnie przesuwać, te modyfikatory:Modifier.layout: pozycje kotwic często zależą od rozmiaru komponentu, który nie jest znany podczas początkowego tworzenia 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 zmiany przeciągania w określonej orientacji i obsługuje gest szybkiego przesunięcia, aby animować komponent do najbliższej kotwicy po zwolnieniu. Dodaj ten modyfikator do komponentu, który powinien reagować na gesty przeciągania, i przekaż mustatei przeciągnijorientation(oraz opcjonalnieoverscrollEffect).Modifier.offset: odczytuje przesunięcie zstate.requireOffset()wewnątrz funkcji lambda przesunięcia. Odczytanie przesunięcia w tej funkcji lambda gwarantuje, że wartość będzie odczytywana tylko podczas fazy umieszczania, co pozwala uniknąć niepotrzebnych rekompozycji podczas przeciągania. Zastosuj ten modyfikator do treści, które powinny się wizualnie przemieszczać.
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 przewijanie poza zakres. Interfejs API
overscrollEffectimplementuje efekt wizualny, który wyświetla się, gdy krawędzie przewijanego kontenera zostaną osiągnięte przez przewijanie lub szybkie przesunięcie. Możesz przekazać użytkownikowi wizualną informację zwrotną, gdy przeciągnie on element poza zdefiniowane granice zakotwiczenia, za pomocą efektu przewijania poza zakres (zwykle rozciągnięcia lub poświaty).- Dispatch: przekaż instancję
overscrollEffectdo modyfikatoraanchoredDraggable. Spowoduje to wysłanie zmian przeciągania do stanuAnchoredDraggablelub efektu overscroll, w zależności od tego, czy gest znajduje się wewnątrz, czy na zewnątrz granic kotwicy. - Renderowanie: zastosuj
Modifier.overscrollz tą samą instancjąOverscrollEffectdo komponentu, który powinien wizualnie renderować efekt.
- Dispatch: przekaż instancję
Poniższy 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
Powyższy fragment kodu powoduje wyświetlenie białego banera z tekstem „Przeciągnij mnie, aby zamknąć!”, który użytkownik może przesunąć w lewo, aby go zamknąć (lub przeciągnąć w prawo, aby zobaczyć efekt rozciągania przy przewijaniu):
Przykład: tworzenie niestandardowego panelu nawigacji sterowanego gestami
Ten przykład pokazuje, jak utworzyć niestandardowy panel nawigacji sterowany gestami. Gdy użytkownik przeciąga palcem w poziomie, ekran na pierwszym planie (ScreenContents) przesuwa się w prawo, zmniejsza się z 1.0f do 0.8f i zaokrągla rogi, aby odsłonić panel nawigacji pod spodem.
Zdefiniuj stany i pozycje kotwicy. W tym przykładzie kotwicami są stany otwarcia i zamknięcia panelu. Zdefiniuj typ wyliczeniowy dla stanów
OpeniClosed, a następnie zmapuj je na przesunięcia pikseli 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, ) }
Animowanie elementu
anchoredDraggableStatepo kliknięciu UmieśćHomeScreenDrawerna doleBox, aScreenContentsna górze. Panel boczny pozostaje nieruchomy, a górna warstwa przesuwa się i skaluje nad nim.Gdy użytkownik wybierze element panelu, zaktualizuj stan aktywnego ekranu i automatycznie zamknij panel 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(), aby zmienić wiele właściwości wizualnych. Zamiast tylko przesuwać treść, możesz zmienić wiele właściwości za pomocąModifier.graphicsLayer, aby jednocześnie animować przesunięcie, skalę i promień narożnika 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ę (od1fdo0.8f) i promień narożnika (od0.dpdo32.dp) względem przesunięcia i szerokości panelu. Modifier.graphicsLayerwprowadza zmiany w fazie rysowania, co zapewnia wysoką wydajność animacji 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
Uruchamianie animacji automatycznych za pomocą
animateTo. Steruj panelem boczny programowo (np. po kliknięciu ikony menu lub wybraniu elementu panelu bocznego), uruchamiającstate.animateTo()w zakresie współprogramu: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ągnij w 2 wymiarach za pomocą Modifier.draggable2D
Użyj modyfikatora draggable2D, aby jednocześnie przenosić poszczególne elementy interfejsu wzdłuż osi X i Y.
Przeciąganie elementu kompozycyjnego
Ten przykład pokazuje najczęstszy przypadek użycia draggable2D – umożliwienie użytkownikowi wybrania elementu interfejsu i umieszczenia go 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 zmian przeciągania. - Ponieważ nie ma obsługi rzutu, pole przestaje się poruszać w momencie, gdy użytkownik podnosi palec.
Przeciąganie elementu kompozycyjnego podrzędnego na podstawie obszaru przeciągania elementu nadrzędnego
Ten przykład pokazuje, jak za pomocą draggable2D utworzyć dwuwymiarowy obszar wejściowy, w którym pokrętło wyboru jest ograniczone do określonej powierzchni. W przeciwieństwie do przykładu z elementem przeciąganym, który przesuwa sam komponent, ta implementacja używa przesunięć 2D do przesuwania podrzędnego „selektora” funkcji kompozycyjnej 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) } ) ) } }
Powyższy fragment kodu zawiera te elementy:
- Używa modyfikatora
onSizeChangeddo przechwytywania rzeczywistych wymiarów kontenera gradientu. Selektor dokładnie wie, gdzie są krawędzie. - Wewnątrz elementu
graphicsLayerdostosowuje właściwościtranslationXitranslationY, aby selektor pozostawał wyśrodkowany podczas przeciągania.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Informacje o gestach
- Przykład zaawansowanej animacji: gesty {:#gesture-and-animation}
- Animacje oparte na wartościach