Gesty

Warto znać kilka terminów i koncepcji podczas pracy nad obsługą gestów w aplikacji. Na tej stronie objaśniamy warunki wskaźników, zdarzeń wskaźnika i gestów oraz wprowadza różne abstrakcje poziomy gestów. Szczegółowo omawia też wykorzystywanie zdarzeń propagacja.

Definicje

Aby zrozumieć różne pojęcia występujące na tej stronie, musisz poznać pewne używanej terminologii:

  • Wskaźnik: fizyczny obiekt, którego możesz używać do interakcji z aplikacją. Na urządzeniach mobilnych wskaźnikiem są najczęściej ekranu dotykowego. Możesz też użyć rysika, aby zastąpić palec. W przypadku dużych ekranów do pośredniej interakcji możesz używać myszy lub trackpada. wyświetlacz. Urządzenie wejściowe musi być w stanie wskazać na koordynacji jest uważany za wskaźnik, dlatego klawiatury nie można traktować jako wskaźnik. W przypadku tworzenia wiadomości typ wskaźnika jest uwzględniany przy zmianach wskaźników za pomocą tagów PointerType
  • Zdarzenie wskaźnika: opisuje interakcję niskiego poziomu polegającą na użyciu co najmniej 1 wskaźnika. z aplikacją w danym momencie. Każda interakcja ze wskaźnikiem, np. umieszczenie palcem na ekranie lub przeciągnięcia myszą, wywoła zdarzenie. W wszystkie istotne informacje o takim zdarzeniu są zawarte w PointerEvent.
  • Gest: sekwencja zdarzeń wskaźnika, którą można zinterpretować jako pojedynczy działania. Na przykład gest dotknięcia może być traktowany jako sekwencja kliknięcia w dół, po którym następuje zdarzenie dodatkowe. Wiele gestów używa wielu gestów, np. do klikania, przeciągania i przekształcania, ale możesz też tworzyć własne w razie potrzeby gestu.

Różne poziomy abstrakcji

Jetpack Compose umożliwia obsługę gestów na różnych poziomach abstrakcji. Najwyższy poziom to obsługa komponentów. Kompozycje, takie jak Button automatycznie uwzględniać obsługę gestów. Aby dodać obsługę gestów do ustawień niestandardowych komponentów, możesz dodawać modyfikatory gestów, takie jak clickable, w dowolnym elementów kompozycyjnych. Jeśli potrzebujesz gestu niestandardowego, możesz użyć pointerInput.

Bazując na najwyższym poziomie abstrakcji, który zapewnia potrzebne funkcje. Dzięki temu możesz korzystać z dodanych sprawdzonych metod w warstwie. Na przykład pole Button zawiera więcej informacji semantycznych, które są używane do ułatwień dostępu niż clickable, który zawiera więcej informacji niż Implementacja pointerInput.

Obsługa komponentów

Wiele gotowych komponentów w funkcji tworzenia wiadomości zawiera swego rodzaju gest wewnętrzny z obsługą klienta. Na przykład LazyColumn reaguje na gesty przeciągania przez: podczas przewijania strony, wyświetlana jest ikona Button, która po naciśnięciu a komponent SwipeToDismiss zawiera funkcje logiczne przesuwania w celu zamknięcia . Ten rodzaj obsługi gestami działa automatycznie.

Oprócz wewnętrznej obsługi gestów, wiele elementów wymaga od rozmówcy i wykonać gest. Na przykład Button automatycznie wykrywa kliknięcia. i wywołuje zdarzenie kliknięcia. Podajesz parametr lambda onClick do: Button, zareaguje na gest. I w podobny sposób dodajesz funkcję lambda onValueChange do: Slider zareaguje na przeciąganie uchwytu suwaka przez użytkownika.

Jeśli ma to zastosowanie w Twoim przypadku, preferuj gesty zawarte w komponentach, ponieważ zawierają gotowe funkcje dotyczące skupienia i ułatwień dostępu. były dobrze przetestowane. Na przykład pole Button jest oznaczone w specjalny sposób, usługi ułatwień dostępu prawidłowo opisują go jako przycisk, a nie zwykły przycisk 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 funkcji Compose znajdziesz w sekcji Ułatwienia dostępu w Utwórz.

Dodawanie określonych gestów do dowolnych elementów kompozycyjnych z modyfikatorami

Możesz zastosować modyfikatory gestów do dowolnego elementu kompozycyjnego, aby lub funkcji kompozycyjnej, słuchając gestów. Możesz na przykład zezwolić na używanie ogólnego Box obsługuje gesty dotykania, ustawiając go jako clickable, lub pozostaw Column i obsługuje przewijanie w pionie, stosując verticalScroll.

Do obsługi różnych rodzajów gestów dostępnych jest wiele modyfikatorów:

Zazwyczaj wolą od razu gotowe modyfikatory gestów zamiast obsługi niestandardowych gestów. Modyfikatory zapewniają większą funkcjonalność oprócz obsługi zdarzeń wskaźnika. Na przykład modyfikator clickable dodaje nie tylko wykrywanie naciśnięć ale też dodaje informacje semantyczne i wizualne wskazówki dotyczące interakcji, najeżdżanie kursorem, zaznaczenie i obsługę klawiatury. Żeby zapoznać się z kodem źródłowym, clickable, aby zobaczyć, jak funkcja jest dodawana.

Dodaj niestandardowy gest do dowolnych elementów kompozycyjnych za pomocą modyfikatora pointerInput

Nie każdy gest można zastosować za pomocą gotowego modyfikatora gestów. Dla: nie można na przykład użyć modyfikatora, aby zareagować na przeciąganie po przytrzymaniu, z naciśniętym klawiszem Control lub 3 palcami. Zamiast tego możesz napisać własny gest do identyfikowania tych gestów niestandardowych. Moduł obsługi gestów możesz utworzyć w aplikacji modyfikator pointerInput, który zapewnia dostęp do surowego wskaźnika. zdarzeń.

Ten kod nasłuchuje nieprzetworzonych zdarzeń 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 główne komponenty to:

  • Modyfikator pointerInput. Musisz przekazać co najmniej 1 klucz. Gdy zmieni się wartość jednego z tych kluczy, wartość modyfikatora lambda wyniesie wykonane ponownie. Przykład przekazuje opcjonalny filtr do funkcji kompozycyjnej. Jeśli wartość tego filtra się zmieni, moduł obsługi zdarzeń wskaźnika powinien i sprawdzić, czy są rejestrowane odpowiednie zdarzenia.
  • awaitPointerEventScope tworzy zakres współrzędny, którego można użyć do poczekaj na zdarzenia wskaźnika.
  • awaitPointerEvent zawiesza współrzędną do następnego zdarzenia wskaźnika ma miejsce.

Odsłuchiwanie nieprzetworzonych zdarzeń wejściowych jest bardzo przydatne, ale zapis niestandardowy gest wygenerowany na podstawie nieprzetworzonych danych. Aby uprościć tworzenie list niestandardowych gestów, dostępnych jest wiele metod.

Wykrywanie pełnych gestów

Zamiast obsługiwać nieprzetworzone zdarzenia wskaźnika możesz nasłuchiwać określonych gestów i odpowiednio zareagować. AwaitPointerEventScope zapewnia metod nasłuchiwania:

Są to detektory najwyższego poziomu, więc nie możesz dodać ich wielu w jednym Modyfikator pointerInput. Ten fragment kodu wykrywa tylko kliknięcia, 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ółrzędną, a druga funkcja nie udało się uzyskać dostępu do wzorca. Jeśli chcesz dodać więcej niż jeden detektor gestów do funkcję kompozycyjną, użyj zamiast niej osobnych instancji 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

Zgodnie z definicją gesty zaczynają się od zdarzenia wskaźnika w dół. Za pomocą awaitEachGesture zamiast pętli while(true), która przez każde nieprzetworzone zdarzenie. Metoda awaitEachGesture uruchamia ponownie który zawiera blok po podniesieniu wszystkich wskaźników, co wskazuje, że gest to ukończono:

@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 będziesz chcieć używać usługi awaitEachGesture, chyba że reaguje na zdarzenia wskaźnika bez identyfikowania gestów. Na przykład: hoverable, który nie reaguje na zdarzenia wskaźnika w dół ani w górę. musi wiedzieć, kiedy wskaźnik pojawi się na obszarze lub z niego wyjść.

Poczekaj na konkretne zdarzenie lub podgest

Dostępny jest zestaw metod, które pomagają rozpoznawać typowe części gestów:

Zastosuj obliczenia dotyczące zdarzeń wielokrotnego dotyku

Gdy użytkownik wykonuje gest wielodotykowy za pomocą więcej niż 1 wskaźnika, trudno jest zrozumieć wymagane przekształcenie na podstawie nieprzetworzonych wartości. Jeśli modyfikator transformable lub detectTransformGestures nie dają wystarczającej szczegółowej kontroli nad danym przypadkiem użycia, nasłuchiwać nieprzetworzonych zdarzeń i stosować na nich obliczenia. Te metody pomocnicze to calculateCentroid, calculateCentroidSize, calculatePan, calculateRotation i calculateZoom.

Wysyłka zdarzeń i testowanie trafień

Nie każde zdarzenie wskaźnika jest wysyłane do każdego modyfikatora pointerInput. Wydarzenie Wysyłka przebiega w ten sposób:

  • Zdarzenia wskaźnika są wysyłane do hierarchii kompozycyjnej. Moment, w którym nowy wskaźnik uruchamia pierwsze zdarzenie wskaźnika, system rozpoczyna testowanie działań „odpowiednie” elementów kompozycyjnych. Utwór kompozycyjny jest uznawany za odpowiedni, jeśli ma możliwości obsługi danych wejściowych wskaźnika. Przepływy testowania trafień dostępne od góry interfejsu użytkownika aż po drzewo graniczne. Element kompozycyjny to „działanie” kiedy wystąpiło zdarzenie wskaźnika. w granicach tego elementu kompozycyjnego. W efekcie powstaje łańcuch elementów kompozycyjnych, które osiągają pozytywne wyniki.
  • Domyślnie, gdy na tym samym poziomie drzewo, tylko funkcja kompozycyjna o najwyższej wartości z-index to „hit”. Dla: Jeśli np. dodasz do elementu Box dwie nakładające się dwie kompozycje Button, zostaną dodane tylko to wyświetlane na górze otrzymuje wszystkie zdarzenia wskaźnika. Teoretycznie można zastąp to działanie, tworząc własne PointerInputModifierNode implementacji i ustawianie parametru sharePointerInputWithSiblings na „true”.
  • Kolejne zdarzenia dla tego samego wskaźnika są wysyłane do tego samego łańcucha elementów kompozycyjnych i działają zgodnie z logiką propagacji zdarzeń. System nie wykonuje więcej testów trafień dla tego wskaźnika. Oznacza to, że każdy funkcja kompozycyjna w łańcuchu odbiera wszystkie zdarzenia dla tego wskaźnika, nawet jeśli które występują poza granicami danego kompozycyjnego. Elementy kompozycyjne, które nie są w łańcuchu nigdy nie odbiera zdarzeń wskaźnika, nawet jeśli wskaźnik jest poza ich granicami.

Zdarzenia najechania kursorem, wywoływane po najechaniu myszą lub rysikiem, stanowią wyjątek reguł zdefiniowanych w tym miejscu. Zdarzenia najechania są wysyłane do każdego tworzonego elementu kompozycyjnego. No więc gdy użytkownik najedzie kursorem na obszar między jednym a drugim, zamiast wysyłać zdarzenia do pierwszego elementu kompozycyjnego, są one wysyłane do funkcji element kompozycyjny.

Wykorzystanie zdarzeń

Jeśli więcej niż jeden element kompozycyjny ma przypisany moduł obsługi gestów, moduły obsługi nie powinny kolidować. Przyjrzyjmy się na przykład temu interfejsowi:

Element listy z obrazem, kolumną z 2 tekstami i przyciskiem.

Gdy użytkownik kliknie przycisk zakładki, funkcja lambda onClick tego przycisku zajmie się gest. Gdy użytkownik kliknie inną część elementu listy, ListItem wykonuje ten gest i przechodzi do artykułu. Jeśli chodzi o dane wejściowe, przycisk musi przetwarzać to zdarzenie, tak by jego element nadrzędny wie, że nie może już na nią nie zareagować. Gesty występujące w gotowych komponentach oraz typowe modyfikatory gestów obejmują ten sposób użycia, ale jeśli własnego gestu, musisz przetwarzać zdarzenia ręcznie. Zrób to 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 propagacji do innych elementów kompozycyjnych. O funkcja kompozycyjna musi bezpośrednio ignorować wykorzystane zdarzenia. Podczas pisania za pomocą niestandardowych gestów, należy sprawdzić, czy zdarzenie nie zostało już wykorzystane przez inne 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
            }
        }
    }
}

Propagacja zdarzeń

Jak już wspomnieliśmy, zmiany wskaźników są przekazywane do każdego elementu kompozycyjnego, w którym działa. Jeśli jednak istnieje więcej niż jeden taki element kompozycyjny, w jakiej kolejności propagować? W przykładzie z poprzedniej sekcji interfejs ten zmieni się przedstawione poniżej drzewo interfejsu, w którym odpowiadają tylko ListItem i Button zdarzenia wskaźnika:

Struktura drzewa. Górna warstwa to ListItem, druga warstwa zawiera obraz, kolumnę i przycisk, a kolumna jest podzielona na dwa wiersze. Wyróżnione są elementy ListItem i Button.

Zdarzenia wskaźnika przechodzą przez każdy z tych elementów kompozycyjnych 3 razy: "karty":

  • W polu Początkowe potwierdzenie zdarzenie przepływa z góry drzewa interfejsu do w dół. Dzięki temu rodzic może przechwycić zdarzenie, zanim dziecko zacznie ich spożyć. Na przykład etykietki muszą przechwytywać i przytrzymać, zamiast przekazywać ją dzieciom. W naszym Na przykład ListItem otrzymuje zdarzenie przed Button.
  • W ramach przekazu głównego zdarzenie przepływa od węzłów liści drzewa interfejsu do pierwiastka drzewa UI. To faza, w której zwykle używasz gestów. jest domyślnym biletem podczas nasłuchiwania zdarzeń. Obsługa gestów na tej karcie co oznacza, że węzły liści mają pierwszeństwo przed węzłami nadrzędnymi, czyli w przypadku większości gestów. W naszym przykładzie Button otrzymuje wydarzenie przed ListItem.
  • W ramach przebiegu ostatniego zdarzenie pojawia się jeszcze raz od góry interfejsu użytkownika. aż do węzłów liści. Dzięki temu elementy znajdujące się wyżej na stosie reagowanie na wykorzystanie zdarzeń przez rodzica. Na przykład przycisk usuwa wskazuje fazę, gdy naciśnięcie zmieni się w przeciągnięcie elementu nadrzędnego, który można przewijać.

Przebieg zdarzeń można przedstawić wizualnie w taki sposób:

Po przyjęciu zmiany danych wejściowych te informacje są przekazywane w dalszym ciągu:

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 to samo zdarzenie jest zwracane przez każde oczekiwania na wywołania metod, chociaż dane o wykorzystaniu mogą mieć została zmieniona.

Przetestuj gesty

W metodach testowania możesz ręcznie wysyłać zdarzenia wskaźnika za pomocą performTouchInput. Dzięki temu możesz podejmować wyższe działania pełne gesty (np. ściąganie palców lub długie kliknięcie) albo 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 artykułach: zasoby:

. .