Es gibt mehrere Begriffe und Konzepte, die Sie kennen sollten, wenn Sie in einer Anwendung mit Gesten arbeiten. Auf dieser Seite werden die Begriffe „Zeiger“, „Zeigerereignisse“ und „Touch-Gesten“ erläutert und die verschiedenen Abstraktionsebenen für Touch-Gesten vorgestellt. Außerdem wird die Nutzung und Weitergabe von Ereignissen genauer erläutert.
Definitionen
Um die verschiedenen Konzepte auf dieser Seite zu verstehen, müssen Sie sich mit einigen der verwendeten Begriffe vertraut machen:
- Pointer: Ein physisches Objekt, mit dem Sie mit Ihrer Anwendung interagieren können.
Bei Mobilgeräten ist der am häufigsten verwendete Zeiger Ihr Finger, der mit dem Touchscreen interagiert. Alternativ können Sie auch einen Eingabestift verwenden.
Bei großen Bildschirmen kannst du eine Maus oder ein Trackpad verwenden, um indirekt mit dem Display zu interagieren. Ein Eingabegerät muss in der Lage sein, auf eine Koordinate zu „zeigen“, um als Zeiger zu gelten. Eine Tastatur kann also beispielsweise nicht als Zeiger betrachtet werden. In Compose wird der Zeigertyp in Zeigeränderungen mit
PointerTypeeinbezogen. - Zeigerereignis: Beschreibt eine Low-Level-Interaktion eines oder mehrerer Zeiger mit der Anwendung zu einem bestimmten Zeitpunkt. Jede Zeigerinteraktion, z. B. das Berühren des Bildschirms mit einem Finger oder das Ziehen einer Maus, löst ein Ereignis aus. In Compose sind alle relevanten Informationen für ein solches Ereignis in der Klasse
PointerEvententhalten. - Geste: Eine Abfolge von Zeigerereignissen, die als eine einzelne Aktion interpretiert werden können. Eine Tipp-Geste kann beispielsweise als eine Sequenz aus einem Down-Ereignis gefolgt von einem Up-Ereignis betrachtet werden. Es gibt gängige Gesten, die von vielen Apps verwendet werden, z. B. Tippen, Ziehen oder Transformieren. Sie können aber auch eigene benutzerdefinierte Gesten erstellen, wenn nötig.
Verschiedene Abstraktionsebenen
Jetpack Compose bietet verschiedene Abstraktionsebenen für die Verarbeitung von Touch-Gesten.
Auf der obersten Ebene befindet sich der Komponentensupport. Composables wie Button unterstützen automatisch Gesten. Wenn Sie benutzerdefinierten Komponenten Unterstützung für Touch-Gesten hinzufügen möchten, können Sie beliebigen Composables Touch-Gesten-Modifikatoren wie clickable hinzufügen. Wenn Sie eine benutzerdefinierte Geste benötigen, können Sie den Modifikator pointerInput verwenden.
In der Regel sollten Sie auf der höchsten Abstraktionsebene aufbauen, die die von Ihnen benötigte Funktionalität bietet. So profitieren Sie von den Best Practices, die in der Ebene enthalten sind. Button enthält beispielsweise mehr semantische Informationen, die für die Barrierefreiheit im Internet verwendet werden, als clickable, das wiederum mehr Informationen als eine reine pointerInput-Implementierung enthält.
Komponentensupport
Viele Standardkomponenten in Compose enthalten eine Art interne Gestenverarbeitung. Beispielsweise reagiert ein LazyColumn auf Ziehgesten, indem der Inhalt gescrollt wird, ein Button zeigt einen Welleneffekt, wenn Sie darauf drücken, und die Komponente SwipeToDismiss enthält eine Wischlogik, um ein Element zu schließen. Diese Art der Gestenverarbeitung funktioniert automatisch.
Neben der internen Gestenverarbeitung müssen bei vielen Komponenten auch die Aufrufer die Geste verarbeiten. Beispielsweise erkennt ein Button automatisch Berührungen und löst ein Click-Event aus. Sie übergeben einen onClick-Lambda-Ausdruck an Button, um auf die Geste zu reagieren. Auf ähnliche Weise fügen Sie einem Slider ein onValueChange-Lambda hinzu, um auf das Ziehen des Schiebereglers durch den Nutzer zu reagieren.
Wenn es zu Ihrem Anwendungsfall passt, sollten Sie Gesten bevorzugen, die in Komponenten enthalten sind, da sie standardmäßig Unterstützung für Fokus und Bedienungshilfen bieten und gut getestet sind. Beispielsweise wird ein Button auf besondere Weise markiert, damit Bedienungshilfen ihn korrekt als Schaltfläche und nicht nur als beliebiges anklickbares Element beschreiben:
// 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!") }
Weitere Informationen zur Barrierefreiheit in Compose finden Sie unter Barrierefreiheit in Compose.
Bestimmte Gesten mit Modifikatoren zu beliebigen Composables hinzufügen
Sie können Gestenmodifikatoren auf jede beliebige zusammensetzbare Funktion anwenden, damit sie auf Gesten reagiert. Sie können beispielsweise ein generisches Box Tippgesten verarbeiten lassen, indem Sie es zu clickable machen, oder ein Column vertikales Scrollen verarbeiten lassen, indem Sie verticalScroll anwenden.
Es gibt viele Modifikatoren für verschiedene Arten von Gesten:
- Tippen und Drücken mit den Modifikatoren
clickable,combinedClickable,selectable,toggleableundtriStateToggleableverarbeiten. - Scrollen mit den Modifikatoren
horizontalScroll,verticalScrollund dem allgemeinerenscrollable. - Ziehen mit den Modifikatoren
draggableundswipeableverarbeiten. - Multi-Touch-Gesten wie Schwenken, Drehen und Zoomen mit dem
transformable-Modifikator verarbeiten.
In der Regel sollten Sie vordefinierte Gestenmodifikatoren der benutzerdefinierten Gestenverarbeitung vorziehen.
Die Modifikatoren bieten zusätzliche Funktionen, die über die reine Verarbeitung von Zeigerereignissen hinausgehen.
Der Modifikator clickable fügt beispielsweise nicht nur die Erkennung von Drücken und Tippen hinzu, sondern auch semantische Informationen, visuelle Hinweise zu Interaktionen, Hovering, Fokus und Tastaturunterstützung. Im Quellcode von clickable können Sie nachsehen, wie die Funktionalität hinzugefügt wird.
Benutzerdefinierte Touchgeste mit dem pointerInput-Modifikator zu beliebigen Composables hinzufügen
Nicht jede Geste wird mit einem vordefinierten Gestenmodifikator implementiert. Sie können beispielsweise keinen Modifikator verwenden, um auf ein Ziehen nach langem Drücken, einen Klick mit gedrückter Strg-Taste oder ein Tippen mit drei Fingern zu reagieren. Stattdessen können Sie einen eigenen Gesten-Handler schreiben, um diese benutzerdefinierten Gesten zu erkennen. Mit dem Modifikator pointerInput können Sie einen Gesten-Handler erstellen, der Ihnen Zugriff auf die Rohzeigerereignisse gibt.
Der folgende Code überwacht Rohzeigerereignisse:
@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}" } } } } ) } }
Wenn Sie dieses Snippet aufschlüsseln, sind die Kernkomponenten:
- Der Modifikator
pointerInput. Sie übergeben einen oder mehrere Schlüssel. Wenn sich der Wert eines dieser Schlüssel ändert, wird der Lambda-Ausdruck für den Inhalt des Modifikators noch einmal ausgeführt. Im Beispiel wird ein optionaler Filter an die zusammensetzbare Funktion übergeben. Wenn sich der Wert dieses Filters ändert, sollte der Pointer-Event-Handler noch einmal ausgeführt werden, damit die richtigen Ereignisse protokolliert werden. awaitPointerEventScopeerstellt einen Koroutinenbereich, der verwendet werden kann, um auf Zeigerereignisse zu warten.awaitPointerEventhält die Koroutine an, bis ein weiteres Zeigerereignis auftritt.
Das Abhören von Roheingabeereignissen ist zwar leistungsstark, aber es ist auch komplex, eine benutzerdefinierte Geste auf Grundlage dieser Rohdaten zu schreiben. Zur Vereinfachung der Erstellung benutzerdefinierter Gesten stehen viele Dienstprogrammmethoden zur Verfügung.
Vollständige Gesten erkennen
Anstatt die Rohzeigerereignisse zu verarbeiten, können Sie auf bestimmte Gesten warten und entsprechend reagieren. Die AwaitPointerEventScope bietet Methoden zum Abhören von:
- Drücken, tippen, doppeltippen und lange drücken:
detectTapGestures - Drags:
detectHorizontalDragGestures,detectVerticalDragGestures,detectDragGesturesunddetectDragGesturesAfterLongPress - Transformationen:
detectTransformGestures
Dies sind Detektoren auf oberster Ebene. Sie können also nicht mehrere Detektoren innerhalb eines pointerInput-Modifikators hinzufügen. Im folgenden Snippet werden nur die Berührungen erkannt, nicht die Ziehbewegungen:
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" } } ) }
Intern blockiert die Methode detectTapGestures die Koroutine und der zweite Detektor wird nie erreicht. Wenn Sie einer komponierbaren Funktion mehrere Touchgesten-Listener hinzufügen müssen, verwenden Sie stattdessen separate pointerInput-Modifikatorinstanzen:
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" } } ) }
Ereignisse pro Geste verarbeiten
Per Definition beginnen Touch-Gesten mit einem Pointer-Down-Ereignis. Sie können die Hilfsmethode awaitEachGesture anstelle der while(true)-Schleife verwenden, die jedes Rohereignis durchläuft. Die Methode awaitEachGesture startet den enthaltenden Block neu, wenn alle Zeiger angehoben wurden, was bedeutet, dass die Geste abgeschlossen ist:
@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() } } } ) }
In der Praxis sollten Sie fast immer awaitEachGesture verwenden, es sei denn, Sie reagieren auf Zeigerereignisse, ohne Gesten zu identifizieren. Ein Beispiel hierfür ist hoverable, das nicht auf Pointer-Down- oder Pointer-Up-Ereignisse reagiert, sondern nur wissen muss, wann ein Pointer in seine Grenzen eintritt oder diese verlässt.
Auf ein bestimmtes Ereignis oder eine bestimmte Untergeste warten
Es gibt eine Reihe von Methoden, mit denen sich gängige Teile von Gesten identifizieren lassen:
- Mit
awaitFirstDownkannst du die Ausführung anhalten, bis ein Zeiger nach unten bewegt wird, oder mitwaitForUpOrCancellationwarten, bis alle Zeiger nach oben bewegt werden. - Erstellen Sie mit
awaitTouchSlopOrCancellationundawaitDragOrCancellationeinen Low-Level-Drag-Listener. Der Touch-Handler wird zuerst angehalten, bis der Zeiger den Touch-Slop erreicht, und dann, bis ein erstes Ziehereignis eintritt. Wenn Sie nur an Ziehbewegungen entlang einer einzelnen Achse interessiert sind, verwenden Sie stattdessenawaitHorizontalTouchSlopOrCancellationplusawaitHorizontalDragOrCancellationoderawaitVerticalTouchSlopOrCancellationplusawaitVerticalDragOrCancellation. - Aussetzen, bis
awaitLongPressOrCancellationlange gedrückt wird. - Mit der Methode
dragkönnen Sie Drag-Events kontinuierlich beobachten. MithorizontalDragoderverticalDragkönnen Sie Drag-Events auf einer Achse beobachten.
Berechnungen für Multi-Touchpoint-Ereignisse anwenden
Wenn ein Nutzer eine Multitouch-Geste mit mehr als einem Zeiger ausführt, ist es schwierig, die erforderliche Transformation anhand der Rohwerte zu verstehen.
Wenn der Modifikator transformable oder die Methoden detectTransformGestures für Ihren Anwendungsfall nicht genügend detaillierte Kontrolle bieten, können Sie die Rohereignisse abhören und Berechnungen darauf anwenden. Diese Hilfsmethoden sind calculateCentroid, calculateCentroidSize, calculatePan, calculateRotation und calculateZoom.
Ereignisweiterleitung und Trefferprüfung
Nicht jedes Zeigerereignis wird an jeden pointerInput-Modifikator gesendet. Die Ereignisverteilung funktioniert so:
- Zeigerereignisse werden an eine komponierbare Hierarchie gesendet. Sobald ein neuer Zeiger sein erstes Zeigerereignis auslöst, beginnt das System mit der Trefferprüfung der „geeigneten“ Composables. Ein komponierbare Funktion gilt als geeignet, wenn sie Funktionen zur Verarbeitung von Zeigereingaben hat. Die Trefferprüfung verläuft von oben nach unten im UI-Baum. Eine komponierbare Funktion wird „getroffen“, wenn das Zeigerereignis innerhalb der Grenzen dieser komponierbaren Funktion aufgetreten ist. Dieser Prozess führt zu einer Kette von Composables, die bei der Trefferprüfung positiv abschneiden.
- Wenn mehrere geeignete Composables auf derselben Ebene des Baums vorhanden sind, wird standardmäßig nur das Composable mit dem höchsten Z-Index „getroffen“. Wenn Sie beispielsweise zwei sich überschneidende
Button-Kompositionen zu einemBoxhinzufügen, empfängt nur die oben gezeichnete Komposition Zeigerereignisse. Sie können dieses Verhalten theoretisch überschreiben, indem Sie eine eigenePointerInputModifierNode-Implementierung erstellen undsharePointerInputWithSiblingsauf „true“ setzen. - Weitere Ereignisse für denselben Zeiger werden an dieselbe Kette von Composables gesendet und fließen gemäß der Logik für die Ereignisweitergabe. Das System führt für diesen Zeiger keine weiteren Trefferprüfungen durch. Das bedeutet, dass jede zusammensetzbare Funktion in der Kette alle Ereignisse für diesen Zeiger empfängt, auch wenn diese außerhalb der Grenzen dieser zusammensetzbaren Funktion auftreten. Nicht in der Kette enthaltene Composables erhalten nie Zeigerereignisse, auch wenn sich der Zeiger innerhalb ihrer Grenzen befindet.
Hover-Ereignisse, die durch das Bewegen der Maus oder des Eingabestifts ausgelöst werden, sind eine Ausnahme von den hier definierten Regeln. Hover-Ereignisse werden an alle Composables gesendet, die sie treffen. Wenn ein Nutzer den Mauszeiger von den Grenzen einer zusammensetzbaren Funktion zur nächsten bewegt, werden die Ereignisse nicht an die erste zusammensetzbare Funktion, sondern an die neue zusammensetzbare Funktion gesendet.
Ereignisverbrauch
Wenn mehreren komponierbaren Funktionen ein Touchgesten-Handler zugewiesen ist, sollten diese Handler nicht miteinander in Konflikt stehen. Sehen Sie sich zum Beispiel diese Benutzeroberfläche an:
Wenn ein Nutzer auf die Lesezeichen-Schaltfläche tippt, wird diese Geste vom Lambda onClick der Schaltfläche verarbeitet. Wenn ein Nutzer auf einen anderen Teil des Listenelements tippt, wird diese Geste von ListItem verarbeitet und der Artikel aufgerufen. In Bezug auf die Zeigereingabe muss die Schaltfläche dieses Ereignis verarbeiten, damit das übergeordnete Element weiß, dass es nicht mehr darauf reagieren muss. Dieses Verhalten ist in den Standardkomponenten und den gängigen Gestenmodifikatoren enthalten. Wenn Sie jedoch eine eigene benutzerdefinierte Geste schreiben, müssen Sie Ereignisse manuell verarbeiten. Dazu verwenden Sie die Methode PointerInputChange.consume:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Das Konsumieren eines Ereignisses verhindert nicht, dass das Ereignis an andere Composables weitergegeben wird. Stattdessen muss ein Composable konsumierte Ereignisse explizit ignorieren. Wenn Sie benutzerdefinierte Touch-Gesten schreiben, sollten Sie prüfen, ob ein Ereignis bereits von einem anderen Element verarbeitet wurde:
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 } } } }
Ereignisweitergabe
Wie bereits erwähnt, werden Zeigeränderungen an jede berührte zusammensetzbare Funktion weitergegeben.
Wenn es jedoch mehr als ein solches Compose-Element gibt, in welcher Reihenfolge werden die Ereignisse dann weitergegeben? Wenn Sie das Beispiel aus dem letzten Abschnitt nehmen, wird diese UI in den folgenden UI-Baum übersetzt, in dem nur ListItem und Button auf Zeigerereignisse reagieren:
Zeigerereignisse durchlaufen jede dieser Composables dreimal, während drei „Durchgängen“:
- Im ersten Durchlauf wird das Ereignis von oben nach unten durch die UI-Struktur geleitet. In diesem Ablauf kann ein übergeordnetes Element ein Ereignis abfangen, bevor das untergeordnete Element es verarbeiten kann. Beispielsweise müssen Kurzinfos ein langes Drücken abfangen, anstatt es an ihre untergeordneten Elemente weiterzugeben. In unserem Beispiel empfängt
ListItemdas Ereignis vorButton. - Im Hauptdurchlauf wird das Ereignis von den Blattknoten des UI-Baums bis zur Wurzel des UI-Baums weitergeleitet. In dieser Phase werden normalerweise Touch-Gesten verarbeitet. Sie ist der Standarddurchlauf beim Abhören von Ereignissen. Wenn Gesten in diesem Durchlauf verarbeitet werden, haben Blattknoten Vorrang vor ihren übergeordneten Knoten. Das ist für die meisten Gesten das logischste Verhalten. In unserem Beispiel empfängt der
Buttondas Ereignis vor demListItem. - Im letzten Durchlauf wird das Ereignis noch einmal von der Spitze des UI-Baums zu den Blattknoten weitergeleitet. So können Elemente, die sich weiter oben im Stack befinden, auf die Ereignisnutzung durch ihr übergeordnetes Element reagieren. Beispielsweise wird die Wellenanzeige einer Schaltfläche entfernt, wenn ein Drücken in ein Ziehen des scrollbaren übergeordneten Elements übergeht.
Visuell lässt sich der Ereignisablauf so darstellen:
Sobald eine Eingabeänderung verarbeitet wurde, werden diese Informationen ab diesem Punkt im Ablauf weitergegeben:
Im Code können Sie die gewünschte Karte/das gewünschte Ticket angeben:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
In diesem Code-Snippet wird von jedem dieser await-Methodenaufrufe dasselbe Ereignis zurückgegeben, obwohl sich die Daten zum Verbrauch möglicherweise geändert haben.
Gesten testen
In Ihren Testmethoden können Sie Zeigerereignisse manuell mit der Methode performTouchInput senden. So können Sie entweder vollständige Gesten auf höherer Ebene (z. B. Zusammenziehen oder langes Klicken) oder Gesten auf niedrigerer Ebene (z. B. Bewegen des Cursors um eine bestimmte Anzahl von Pixeln) ausführen:
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Weitere Beispiele finden Sie in der Dokumentation zu performTouchInput.
Weitere Informationen
Weitere Informationen zu Gesten in Jetpack Compose finden Sie in den folgenden Ressourcen:
Empfehlungen für Sie
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Barrierefreiheit in Compose
- Scrollen
- Tippen und drücken