Pracując nad obsługą gestów w aplikacji, warto zrozumieć kilka terminów i koncepcji. Na tej stronie objaśniamy terminy, związane ze wskaźnikami, zdarzeniami i gestami, a także przedstawiamy różne poziomy abstrakcji używania gestów. Zajmuje się też głębszym omówieniem konsumpcji i propagacji zdarzeń.
Definicje
Aby zrozumieć różne pojęcia na tej stronie, musisz poznać używaną terminologię:
- Wskaźnik: obiekt fizyczny, którego możesz używać do interakcji z aplikacją.
W przypadku urządzeń mobilnych najczęstszym wskaźnikiem jest palec, który wchodzi w interakcję z ekranem dotykowym. Możesz też zastąpić palec rysikiem.
W przypadku dużych ekranów możesz pośrednio wchodzić w interakcje z wyświetlaczem za pomocą myszy lub trackpada. Aby można było uznać urządzenie za wskaźnik, urządzenie wejściowe musi być w stanie „wskazać” współrzędną. Nie można więc zaliczyć np. klawiatury do klawiatury. W narzędziu Compose typ wskaźnika jest uwzględniany w zmianach wskaźnika za pomocą
PointerType
. - Zdarzenie wskaźnika opisuje interakcję na niskim poziomie z jednym lub większą liczbą wskaźników z aplikacją w danym momencie. Każda interakcja ze wskaźnikiem, np. umieszczenie palca na ekranie lub przeciągnięcie myszą, wywołuje zdarzenie. W metodzie tworzenia wszystkie istotne informacje o takim zdarzeniu są zawarte w klasie
PointerEvent
. - Gest: sekwencja zdarzeń wskaźnika, które można zinterpretować jako pojedyncze działanie. Na przykład gest kliknięcia może być sekwencją zdarzenia w dół, po którym następuje zdarzenie w górę. W wielu aplikacjach są popularne gesty takie jak klikanie, przeciąganie czy przekształcanie. W razie potrzeby możesz też utworzyć własny gest niestandardowy.
Różne poziomy abstrakcji
Jetpack Compose zapewnia różne poziomy abstrakcji obsługi gestów.
Najwyższy poziom to obsługa komponentów. Funkcje kompozycyjne, takie jak Button
, automatycznie obsługują gesty. Aby dodać obsługę gestów do komponentów niestandardowych, możesz dodać do dowolnych funkcji kompozycyjnych modyfikatory gestów, takie jak clickable
. Jeśli potrzebujesz gestu niestandardowego, możesz też użyć modyfikatora pointerInput
.
Zasadniczo należy opierać się na najwyższym poziomie abstrakcji, który zapewnia
potrzebną funkcjonalność. W ten sposób korzystasz ze sprawdzonych metod
opisanych w warstwie. Na przykład element Button
zawiera więcej informacji semantycznych (ułatwiających dostęp) niż tag clickable
, który zawiera więcej informacji niż nieprzetworzona implementacja pointerInput
.
Obsługa komponentów
Wiele gotowych komponentów do tworzenia wiadomości obejmuje jakąś wewnętrzną obsługę gestów. Na przykład obiekt LazyColumn
reaguje na gesty przeciągania, przewijając jego zawartość, po naciśnięciu przycisku w dół Button
pokazuje falę, a komponent SwipeToDismiss
zawiera logikę przesuwania, która wyłącza element. Ten typ obsługi gestów działa automatycznie.
Oprócz wewnętrznej obsługi gestów wiele komponentów wymaga też od rozmówcy obsługi gestów. Na przykład element Button
automatycznie wykrywa kliknięcia i wywołuje zdarzenie kliknięcia. Przekazujesz lambda onClick
do interfejsu Button
, aby zareagować na ten gest. I podobnie dodajesz lambda onValueChange
do elementu Slider
, by określić, czy użytkownik przeciągnie uchwyt suwaka.
W miarę możliwości korzystaj z gestów zawartych w komponentach, ponieważ zawierają gotową obsługę funkcji skupienia i ułatwień dostępu, a także są dobrze przetestowane. Na przykład element Button
jest oznaczony w specjalny sposób, aby usługi ułatwień dostępu prawidłowo opisywały go jako przycisk, a nie tylko dowolny klikalny element:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
Więcej informacji o ułatwieniach dostępu w sekcji Utwórz znajdziesz w sekcji Ułatwienia dostępu.
Dodawanie określonych gestów do dowolnych funkcji kompozycyjnych z modyfikatorami
Możesz zastosować modyfikatory gestów do dowolnych funkcji kompozycyjnych, aby funkcja kompozycyjna nasłuchiwała gestów. Możesz na przykład zezwolić na ogólne gesty dotknięcia rączką Box
w postaci ikony clickable
, a Column
użyć przewijania w pionie, stosując verticalScroll
.
Istnieje wiele modyfikatorów do obsługi różnych typów gestów:
- Obsługuj dotknięcia i naciśnięcia z modyfikatorami
clickable
,combinedClickable
,selectable
,toggleable
itriStateToggleable
. - Obsługuj przewijanie za pomocą
horizontalScroll
,verticalScroll
i bardziej ogólnych modyfikatorówscrollable
. - Uchwyć przeciąganie za pomocą modyfikatorów
draggable
iswipeable
. - Obsługuj gesty wielodotykowe, takie jak przesuwanie, obracanie i powiększanie, z modyfikatorem
transformable
.
Z reguły korzystaj z gotowych modyfikatorów gestów, a nie obsługi niestandardowych gestów.
Modyfikatory zwiększają funkcjonalność poza obsługą zdarzeń wskaźnika.
Na przykład modyfikator clickable
nie tylko dodaje wykrywanie naciśnięć i kliknięć, ale też dodaje informacje semantyczne i wizualne informacje o interakcjach, najechanie kursorem, zaznaczenie i obsługę klawiatury. Możesz sprawdzić kod źródłowy clickable
, aby dowiedzieć się, jak ta funkcja jest dodawana.
Dodaj gest niestandardowy do dowolnych funkcji kompozycyjnych z modyfikatorem pointerInput
Nie każdy gest jest zaimplementowany z gotowym modyfikatorem gestów. Na przykład nie można używać modyfikatora do reagowania na przeciągnięcie po przytrzymaniu, kliknięciu z naciśniętym klawiszem Control lub kliknięciu 3 palcami. Możesz jednak wpisać własny moduł obsługi
gestów, by rozpoznawać te niestandardowe gesty. Możesz utworzyć moduł obsługi gestów z modyfikatorem pointerInput
, który daje dostęp do nieprzetworzonych zdarzeń wskaźnika.
Ten kod odsłuchuje nieprzetworzone zdarzenia wskaźnika:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
Po podzieleniu tego fragmentu kodu główne komponenty to:
- Modyfikator
pointerInput
. Przekazujesz do niego co najmniej 1 klucz. Gdy wartość jednego z tych kluczy ulegnie zmianie, funkcja lambda treści modyfikatora jest wykonywana ponownie. Przykład przekazuje opcjonalny filtr do funkcji kompozycyjnej. Jeśli wartość tego filtra ulegnie zmianie, należy ponownie uruchomić moduł obsługi zdarzeń wskaźnika, aby mieć pewność, że rejestrowane są odpowiednie zdarzenia. awaitPointerEventScope
tworzy zakres współużytkowania, którego można używać do oczekiwania na zdarzenia wskaźnika.awaitPointerEvent
zawiesza współprogram do czasu wystąpienia kolejnego zdarzenia wskaźnika.
Choć nasłuchiwanie nieprzetworzonych danych wejściowych ma duże znaczenie, utworzenie niestandardowego gestu na podstawie tych nieprzetworzonych danych jest też skomplikowane. Aby ułatwić tworzenie gestów niestandardowych, dostępne są różne metody narzędziowe.
Wykrywanie pełnych gestów
Zamiast obsługiwać nieprzetworzone zdarzenia wskaźnika, możesz wykrywać konkretne gesty i reagować na nie. AwaitPointerEventScope
udostępnia metody wykrywania:
- Naciśnij, kliknij, kliknij dwukrotnie i przytrzymaj:
detectTapGestures
- Przeciągnięcia:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
idetectDragGesturesAfterLongPress
- Przekształcanie:
detectTransformGestures
Są to wzorce do wykrywania treści najwyższego poziomu, więc nie można dodać wielu wzorców do wykrywania treści w ramach jednego modyfikatora pointerInput
. Ten fragment kodu wykrywa tylko kliknięcia, a nie przeciąganie:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
Wewnętrznie metoda detectTapGestures
blokuje współprogram, a drugi detektor nigdy nie jest osiągany. Jeśli chcesz dodać do funkcji kompozycyjnej więcej niż 1 detektor gestów, użyj osobnych wystąpień modyfikatora pointerInput
:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
Obsługa zdarzeń według gestu
Z definicji gesty zaczynają się od zdarzenia wskaźnika w dół. Zamiast pętli while(true)
, która przechodzi przez poszczególne nieprzetworzone zdarzenia, możesz używać metody pomocniczej awaitEachGesture
. Metoda awaitEachGesture
uruchamia ponownie zawarty blok po podniesieniu wszystkich wskaźników, co oznacza, że gest został zakończony:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
W praktyce prawie zawsze chcesz używać awaitEachGesture
, chyba że odpowiadasz na zdarzenia wskaźnika bez identyfikowania gestów. Przykładem może być mechanizm hoverable
, który nie reaguje na zdarzenia typu wskaźnik w dół ani w górę – wystarczy, że będzie wiedzieć, kiedy wskaźnik znajdzie się w jej granicach lub ją opuści.
Poczekaj na określone zdarzenie lub podrzędne gesty
Istnieje zestaw metod, które pomagają rozpoznawać typowe części gestów:
- Zawieś, dopóki wskaźnik nie ominie
awaitFirstDown
, lub poczekaj, aż wszystkie wskaźniki będą w górę –waitForUpOrCancellation
. - Utwórz niskopoziomowy detektor przeciągania za pomocą elementów
awaitTouchSlopOrCancellation
iawaitDragOrCancellation
. Moduł obsługi gestów najpierw zawiesza się, dopóki wskaźnik nie dotrze do obszaru dotykowego, a następnie zawiesza się do czasu wystąpienia pierwszego zdarzenia przeciągania. Jeśli chcesz korzystać tylko z przeciągania wzdłuż 1 osi, użyj znakówawaitHorizontalTouchSlopOrCancellation
plusawaitHorizontalDragOrCancellation
lubawaitVerticalTouchSlopOrCancellation
plusawaitVerticalDragOrCancellation
. - Zawieś, dopóki nie przytrzymasz elementu
awaitLongPressOrCancellation
. - Użyj metody
drag
, aby stale nasłuchiwać zdarzeń przeciągania, albohorizontalDrag
lubverticalDrag
, aby nasłuchiwać zdarzeń przeciągania po jednej osi.
Stosowanie obliczeń w przypadku zdarzeń obejmujących różne interakcje
Gdy użytkownik wykonuje gest wielodotykowy za pomocą więcej niż 1 wskaźnika, zrozumienie wymaganej przekształcenia na podstawie nieprzetworzonych wartości jest skomplikowane.
Jeśli modyfikator transformable
lub metody detectTransformGestures
nie dają wystarczającej kontroli w Twoim przypadku, możesz wychwytywać nieprzetworzone zdarzenia i stosować do nich obliczenia. Dostępne metody pomocnicze to calculateCentroid
, calculateCentroidSize
, calculatePan
, calculateRotation
i calculateZoom
.
Wysyłanie zdarzeń i testowanie trafień
Nie każde zdarzenie wskaźnika jest wysyłane do każdego modyfikatora pointerInput
. Wysyłanie zdarzeń działa w ten sposób:
- Zdarzenia wskaźnika są wysyłane do hierarchii kompozycyjnej. W momencie, gdy nowy wskaźnik aktywuje swoje pierwsze zdarzenie wskaźnika, system rozpoczyna testowanie działań funkcji kompozycyjnych „kwalifikujących się”. Funkcja kompozycyjna jest uznawana za kwalifikującą się, jeśli ma funkcje obsługi danych wejściowych wskaźnika. Testowanie działań przebiega od góry do dołu drzewa interfejsu. Element kompozycyjny to „działanie”, gdy zdarzenie wskaźnika wystąpiło w granicach tego elementu kompozycyjnego. W wyniku tego powstaje łańcuch elementów kompozycyjnych, które wykazują pozytywne wyniki.
- Jeśli na tym samym poziomie drzewa jest wiele odpowiednich funkcji kompozycyjnych, domyślnie tylko ten z najwyższym z-indeksem to „działanie”. Jeśli na przykład dodasz do elementu
Box
2 pokrywające się elementy kompozycyjneButton
, zdarzenia wskaźnika będą wysyłane tylko ten element narysowany u góry. Teoretycznie możesz zastąpić to zachowanie, tworząc własną implementacjęPointerInputModifierNode
i ustawiającsharePointerInputWithSiblings
na wartość Prawda. - Dalsze zdarzenia związane z tym samym wskaźnikiem są wysyłane do tego samego łańcucha obiektów kompozycyjnych i przebiegają zgodnie z logiką propagacji zdarzeń. System nie będzie więcej testował z nim trafień. Oznacza to, że każdy element kompozycyjny w łańcuchu otrzymuje wszystkie zdarzenia związane z tym wskaźnikiem, nawet jeśli występują one poza granicami funkcji kompozycyjnej. Obiekty kompozycyjne, które nie są w łańcuchu, nigdy nie otrzymują zdarzeń wskaźnika, nawet jeśli wskaźnik mieści się poza ich granicami.
Wyjątkiem od zdefiniowanych tutaj reguł są zdarzenia najechania kursorem, które są wywoływane po najechaniu kursorem myszy lub po najechaniu rysikiem. Zdarzenia najechania kursorem są wysyłane do wszystkich obsługiwanych przez nie funkcji kompozycyjnych. Gdy użytkownik najedzie kursorem na wskaźnik z zakresu jednego elementu kompozycyjnego do następnego, zamiast wysyłać zdarzenia do tego pierwszego elementu kompozycyjnego,
Spożycie zdarzeń
Jeśli więcej niż jeden element kompozycyjny ma przypisany moduł obsługi gestów, nie powinno to powodować konfliktu. Spójrzmy na przykład na ten interfejs:
Gdy użytkownik kliknie przycisk zakładki, lambda onClick
na tym przycisku obsługuje ten gest. Gdy użytkownik kliknie inną część elementu listy, uchwyt ListItem
przejdzie do artykułu. W przypadku wprowadzania wskaźnika przycisk musi konwertować to zdarzenie, aby jego element nadrzędny nie miał już na nie reagować. Gesty zawarte w gotowych komponentach i modyfikatory typowych gestów obejmują takie zachowanie, ale jeśli tworzysz własny gest niestandardowy, musisz przetwarzać zdarzenia ręcznie. Użyj do tego metody PointerInputChange.consume
:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Wykorzystanie zdarzenia nie zatrzymuje jego rozpowszechniania do innych funkcji kompozycyjnych. Funkcja kompozycyjna musi zamiast tego ignorować użyte zdarzenia. Pracując nad gestami niestandardowymi, sprawdź, czy zdarzenie nie zostało już przetworzone przez inny element:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
Rozpowszechnianie zdarzeń
Jak już wspomnieliśmy, zmiany wskaźników są przekazywane do każdego trafionego elementu kompozycyjnego.
Jeśli jednak istnieje więcej niż 1 taki obiekt kompozycyjny, w jakiej kolejności są propagowane zdarzenia? Jeśli wykorzystasz przykład z ostatniej sekcji, interfejs ten zostanie przekształcony w takie drzewo interfejsu, w którym tylko ListItem
i Button
odpowiadają na zdarzenia wskaźnika:
Zdarzenia wskaźnika przechodzą przez każdy z tych elementów kompozycyjnych 3 razy w ramach 3 „przepustek”:
- W przypadku karnetu początkowego zdarzenie płynie z góry drzewa interfejsu na dół. Ten proces umożliwia rodzicowi przechwycenie zdarzenia, zanim dziecko będzie mogło je wykorzystać. Na przykład etykiety muszą przechwycić i przechwycić przytrzymanie zamiast przekazywać je dzieciom. W tym przykładzie
ListItem
otrzymuje zdarzenie przed elementemButton
. - W karcie głównej zdarzenie przepływa od węzłów liści drzewa interfejsu do poziomu głównego drzewa interfejsu. Na tej fazie zwykle używamy gestów i jest to domyślna faza nasłuchiwania zdarzeń. Obsługa gestów w tym karnetie oznacza, że węzły liści mają pierwszeństwo przed elementami nadrzędnymi, co jest najbardziej logicznym zachowaniem w przypadku większości gestów. W naszym przykładzie
Button
otrzymuje zdarzenie przed elementemListItem
. - W przypadku przejścia końcowego zdarzenie przepływa jeszcze raz z góry drzewa interfejsu do węzłów liści. Dzięki temu elementy znajdujące się wyżej w stosie mogą reagować na zdarzenia przez ich element nadrzędny. Na przykład przycisk usuwa z niego falowanie, gdy jego naciśnięcie zmieni się w przeciąganie przewijanego elementu nadrzędnego.
Przepływ zdarzeń można odzwierciedlić w ten sposób:
Po wykorzystaniu zmiany danych wejściowych te informacje są przekazywane od tego punktu kolejnych etapów procesu:
W kodzie możesz określić kartę, która Cię interesuje:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
W tym fragmencie kodu każde z tych wywołań metody zwraca to samo zdarzenie, ale dane o wykorzystaniu mogły się zmienić.
Gesty testowe
W metodach testowania możesz ręcznie wysyłać zdarzenia wskaźnika za pomocą metody performTouchInput
. Dzięki temu możesz wykonywać gesty pełne wyższego poziomu (np. ściąganie palcami lub przytrzymanie) lub gesty niskiego poziomu (np. przesunięcie kursora o określoną liczbę pikseli):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Więcej przykładów znajdziesz w dokumentacji performTouchInput
.
Więcej informacji
Więcej informacji o gestach w Jetpack Compose znajdziesz w tych materiałach:
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony
- Ułatwienia dostępu przy tworzeniu wiadomości
- Przewijanie
- Kliknij i naciśnij